run loop 再次理解和总结

本文详细介绍了RunLoop的概念,包括其本质、功能及工作原理。解释了RunLoop模式及其应用场景,还深入探讨了事件源与定时器如何产生事件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

run loop是什么?

RunLoop 的本质就是一个函数,其内部是一个 do-while 循环。
线程需要调用runloop这个函数,来处理传入的事件。线程会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

run loop的功能

处理事件,包括由输入源传来的事件,和定时器传来的事件。
产生有关运行行为的notifications。可以在线程中给runloop添加监听者。
run loop循环都在干些什么,看下图,建议就大概看看留个印象就好,然后继续往下看。
这里写图片描述

run loop Mode是什么?

run loop Mode,运行模式,是一组事件源集合和一组监听者集合。这里所说的事件源是事件的来源,不是事件本身。
运行run loop的时候需要显式或隐式地指定运行模式。runloop只会处理与当前运行模式相关的事件源传来的事件,只会通知与当前运行模式相关的监听者。此时,与当前运行模式不相关的事件发生后需要等待,直到run loop切换到与其关联的模式才能被执行。
我们可以往run loop的某一个运行模式中添加事件源和监听着。
模式有哪些?对应的名称及使用场景是什么?

1、四个常规模式

(1)默认模式 NSDefaultRunLoopMode 大多数操作在该模式下完成,建议在该模式下启动runloop,以及配置输入源。
(2)连接模式 NSConnectionReplyMode Cocoa用这种模式来和NSConnection对象一起监视连接响应。不建议使用这个模式。
(3)模态模式 NSModalPanelRunLoopMode Cocoa使用这个模式来标识意图模态面板的事件。
(4)事件追踪模式 NSEventTrackingRunLoopMode 在鼠标拖拽等用户交互过程中,Cocoa使用这个模式对输入事件进行限制。

2、一个特殊模式

普通模式 NSRunLoopCommonModes 普通模式是以上模式中常用的几个模式的集合,默认情况下包括默认模式,模态模式和事件追踪模式。这个普通模式存在的意义在于它的同步功能。同步功能是指,可以将任意模式添加到这个普通模式中,然后只要将事件源或者监听者添加到这个普通模式中,就相当于添加(同步)到了普通模式中包含的各个模式中。也就是说,这个普通模式只是一个概念,一个标识,并不是一个真正的模式,run loop不会以普通模式运行,只会运行在以上的常规模式。

产生事件的两个源头

下面分别介绍产生事件的两个源头,一个是输入源,一个是定时器。

1、输入源

输入源分三类,一种是基于端口的输入源,一种是自定义的输入源,一种是来自Cocoa层的输入源。
(1)基于端口的输入源
要新建基于端口的输入源会用到Cocoa 层的NSPort,或者Core Foundation中的CFMachPortRef、CFMessagePortRef、CFSocketRef。这里不多说。
(2)自定义的输入源
要新建自定义的输入源需要用到CFRunLoopSource。这里也不多说。
(3)来自Cocoa层的输入源
要创建来自Cocoa层的输入源,系统已经提供了几个方法,就在NSObject中定义,方法名以performSelector开头。使用这几个方法,只要我们提供需要执行的selector,就可以为我们创建输入源(实际上也是自定义输入源),并为我们添加到指定的线程当中,在线程中被串行地执行的,并且在执行完毕后,输入源会自动从run loop中移除。需要注意的是,这一切正常发生的前提是,事件源所添加到的目标线程,需要有一个正在运行的run loop。换句话说,一个线程只能对应一个run loop,但线程创建之后,run loop还没有在运行(run loop函数还没有被调用),因此往往需要显式地启动该线程的runloop(调用run loop函数)。唯一一个不需要手动启动的runloop是主线程对应的runloop,它在程序启动的时候就启动了,只要程序启动完毕就可以使用这个runloop了。runloop会在一次循环中处理完所有排着队的输入源的selector,而不是每次执行一个selector,直到处理完毕。

2、定时器

定时器产生的事件,就是定时器所设定的启动时间到达,run loop处理这个事件的实际就是执行定时器所绑定的selector。定时器在创建的时候需要指定它要加入的运行模式,就是run loop mode。将定时器加入到run loop之后,要等到这个run loop运行在定时器创建时指定的运行模式,这个定时器才会被启动。如果定时器加入到run loop的时候,这个run loop正好处在合适的运行模式,但是正在处理其他事件,那么run loop会在下一次循环时再启动这个定时器。当然,可以将定时器加入到普通模式,NSRunLoopCommonModes中,也就相当于将定时器加入到了多个模式中,那么不论run loop正运行在普通模式中的哪一个模式,定时器的事件都能得到处理。不过,和上面第(2)、(3)种输入源一样,这一切的前提是线程已经进入了run loop循环。那么,说了那么多,有些操作就变得很有用了,比如获取当前线程的run loop,启动run loop等。这些在下面继续讲。

使用run loop

上面说到run loop本质上是一个函数,函数内部是一个do-while循环,但它也有相关的属性,比如运行模式等,也有启动和停止的需求,因此它被封装成了CFRunLoop,其指针类型为CFRunLoopRef。NSRunloop是对CFRunLoop进行了进一步的封装。下面就介绍操作run loop的方法(针对CFRunLoop和NSRunloop的方法都有)。
1、获取当前线程的runloop:
需要获取线程a的runloop需要在线程a中调用以下方法。
方法一:[NSRunloop currentRunloop];
方法二:CFRunLoopGetCurrent(void);
2、开启runloop:
方法一:无条件开启,即调用run方法。
方法二:设定runloop开启时间
3、结束runloop
方法一:设定runloop的失效时间
方法二:直接停止runloop,调用CFRunLoopStop();

以上内容,参考了苹果官网的Thread Programming Guide,以及一个大神的博客,好崇拜Ta。

<think>我们正在讨论的是如何正确结束由`asyncio.run()`创建的异步事件循环。实际上,`asyncio.run()`函数已经为我们处理了事件循环的创建关闭。但是,用户可能想要了解在运行完异步任务后,如何确保事件循环被正确关闭,或者是否有手动关闭的必要。 根据Python官方文档,`asyncio.run()`函数会创建一个新的事件循环,并在运行传入的协程后关闭该事件循环。因此,在调用`asyncio.run()`之后,事件循环已经被关闭,我们不需要手动关闭。 但是,有时我们可能需要在同一个线程再次运行异步代码,那么再次调用`asyncio.run()`是安全的,因为它会创建一个新的事件循环。 然而,如果我们在使用`asyncio.run()`之后尝试再次使用同一个事件循环(例如通过`asyncio.get_event_loop()`),则可能会遇到问题,因为原来的事件循环已经关闭。在这种情况下,我们应该总是使用`asyncio.run()`来运行异步主函数,而避免直接操作事件循环。 另外,如果我们在程序中使用了其他方式创建事件循环(比如使用`asyncio.get_event_loop()`或`asyncio.new_event_loop()`),那么我们需要确保在程序结束时关闭事件循环。但是,使用`asyncio.run()`则无需担心这一点。 下面是一个简单的示例,展示如何使用`asyncio.run()`: ```python import asyncio async def main(): print("Hello") await asyncio.sleep(1) print("World") asyncio.run(main()) ``` 在这个例子中,`asyncio.run(main())`会运行`main`协程,并在完成后关闭事件循环。 如果我们有多个异步任务要运行,我们可以这样: ```python import asyncio async def task1(): print("Task1 start") await asyncio.sleep(1) print("Task1 end") return 1 async def task2(): print("Task2 start") await asyncio.sleep(2) print("Task2 end") return 2 async def main(): # 使用gather并发运行多个任务 results = await asyncio.gather(task1(), task2()) print(results) # [1, 2] asyncio.run(main()) ``` 注意:在`main`函数中,我们使用`asyncio.gather`来并发运行两个任务。 关于关闭事件循环,我们还需要注意: 1. 在`asyncio.run()`内部,当主协程完成后,它会取消所有剩余的任务(如果有的话),然后关闭事件循环。 2. 如果我们在任务中创建了需要在退出时清理的资源(如网络连接、文件句柄等),我们应该使用`try...finally`块或异步上下文管理器来确保资源被正确释放。 3. 如果我们需要在程序中使用多个`asyncio.run()`调用(通常不推荐,因为每次调用都会创建关闭一个新的事件循环),那么这是允许的,但可能会带来性能开销。 4. 在异步任务中,如果遇到未处理的异常,事件循环会停止并传播异常,然后`asyncio.run()`会将异常抛出到调用方。 因此,总结来说,使用`asyncio.run()`时,我们不需要手动关闭事件循环,因为它已经自动处理了。我们只需要确保在`asyncio.run()`中运行的主协程能够正常完成(或处理异常)即可。 如果用户之前使用的是低级别的API(如`loop.run_until_complete()`),那么需要手动关闭循环。例如: ```python loop = asyncio.get_event_loop() try: loop.run_until_complete(main()) finally: loop.close() ``` 但在现代Python异步编程中,推荐使用`asyncio.run()`,因为它更简洁且安全。 最后,关于用户提到的“asyncio.run shutdown method”,实际上`asyncio.run()`函数内部已经实现了关闭逻辑,没有单独的shutdown方法。我们只需要按照上述方式使用即可。 相关问题: 1. 在异步任务中如何处理未完成的任务? 2. 如何在使用`asyncio.run()`时处理资源清理? 3. 多次调用`asyncio.run()`是否安全? 4. 在异步程序中如何优雅地处理信号(如Ctrl+C)?</think># 如何正确关闭 `asyncio.run()` 创建的事件循环 `asyncio.run()` 是 Python 3.7+ 中推荐的异步程序入口点,它会自动创建并管理事件循环的生命周期。理解其关闭机制对于编写健壮的异步程序至关重要。 ## `asyncio.run()` 的自动关闭机制 `asyncio.run()` 会自动处理事件循环的创建关闭: 1. **创建新事件循环**:每次调用都会创建一个全新的事件循环 2. **运行主协程**:执行传入的协程(如 `main()`) 3. **自动关闭**:主协程完成后,自动关闭事件循环 4. **清理资源**:取消所有剩余任务,等待执行器关闭 ```python import asyncio async def main(): print("主协程开始") await asyncio.sleep(1) print("主协程结束") # 自动创建、运行关闭事件循环 asyncio.run(main()) # 无需手动关闭 ``` ## 手动关闭场景与最佳实践 ### 场景1:需要提前终止事件循环 ```python async def main(): try: # 长时间运行的任务 await asyncio.sleep(3600) except asyncio.CancelledError: print("任务被取消") # 在另一个线程中终止事件循环 def stop_loop(): loop = asyncio.get_running_loop() # 取消所有任务并停止循环 loop.call_soon_threadsafe(loop.stop) asyncio.run(main()) ``` ### 场景2:优雅关闭后台任务 ```python async def background_task(): try: while True: print("后台任务运行中...") await asyncio.sleep(1) except asyncio.CancelledError: print("后台任务优雅关闭") # 执行清理操作 await asyncio.sleep(0.5) # 模拟清理 async def main(): # 创建后台任务 task = asyncio.create_task(background_task()) # 主程序逻辑 await asyncio.sleep(3) # 取消后台任务 task.cancel() try: await task # 等待任务处理取消请求 except asyncio.CancelledError: pass # 预期中的取消 asyncio.run(main()) ``` ### 场景3:使用信号处理优雅关闭 ```python import signal async def main(): loop = asyncio.get_running_loop() # 设置信号处理 for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown())) # 主程序逻辑 while True: print("程序运行中...") await asyncio.sleep(1) async def shutdown(): print("\n接收到关闭信号,开始优雅关闭...") # 取消所有任务 tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] for task in tasks: task.cancel() # 等待任务完成取消 await asyncio.gather(*tasks, return_exceptions=True) # 停止事件循环 loop = asyncio.get_running_loop() loop.stop() asyncio.run(main()) ``` ## 常见错误与解决方案 ### 错误1:在 `asyncio.run()` 后手动关闭 ```python # 错误:无需手动关闭 loop = asyncio.get_event_loop() try: loop.run_until_complete(main()) finally: loop.close() # 冗余操作,可能导致错误 ``` **解决方案**:直接使用 `asyncio.run(main())`,无需额外操作[^1]。 ### 错误2:多次调用 `asyncio.run()` ```python # 错误:嵌套调用 asyncio.run() async def task(): asyncio.run(another_task()) # 会创建新的事件循环 asyncio.run(task()) ``` **解决方案**:在已有事件循环中使用 `create_task()`: ```python async def task(): asyncio.create_task(another_task()) ``` ### 错误3:未处理任务取消 ```python async def main(): task = asyncio.create_task(long_running_task()) # 直接退出而不处理任务取消 ``` **解决方案**:确保取消所有任务并等待它们完成: ```python async def main(): task = asyncio.create_task(long_running_task()) await asyncio.sleep(1) task.cancel() try: await task except asyncio.CancelledError: pass # 正常处理取消 ``` ## 与旧版事件循环管理的对比 | **特性** | `asyncio.run()` | 手动管理事件循环 | |----------|----------------|------------------| | **事件循环创建** | 自动创建新循环 | 需手动调用 `get_event_loop()` 或 `new_event_loop()` | | **关闭机制** | 自动关闭 | 需手动调用 `loop.close()` | | **异常处理** | 自动传播异常 | 需手动处理 | | **任务清理** | 自动取消所有任务 | 需手动清理 | | **线程安全** | 是 | 需额外处理 | | **推荐使用** | Python 3.7+ | 旧版兼容 | ## 最佳实践总结 1. **优先使用 `asyncio.run()`** 作为程序入口点 2. **避免手动关闭** `asyncio.run()` 创建的事件循环 3. **处理取消异常** 确保后台任务能优雅关闭 4. **使用信号处理** 实现程序的优雅终止 5. **清理资源** 在任务取消时释放所有资源 6. **避免嵌套调用** 在已有事件循环中使用 `create_task()` 通过遵循这些实践,您可以确保异步程序正确启动关闭,避免资源泄漏意外行为[^1][^2][^3]。 --- ## 相关问题 1. `asyncio.run()` `loop.run_until_complete()` 有何区别? 2. 如何在异步任务中正确处理资源清理? 3. 异步程序如何实现热重载功能? 4. 使用 `asyncio.run()` 时如何处理未捕获的异常? 5. 在长时间运行的异步服务中如何实现优雅重启?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值