背景
在 Nanobot 从 AgentLoop 启动看怎么驱动大模型运行中提到协程和线程的区别,
通过Nanobot(channels.start_all)这里了解一下协程一些基本操作.
shell
协程是用户态的轻量级"微线程",切换由用户程序控制,没有内核态切换,开销极小;线程是内核态的资源单元,切换由操作系统调度,开销较大.
线程切换需要涉及用户态到内核态的转换,上下文包括内核栈、硬件寄存器等,保存和恢复资源较多
channels.start_all
python
async def start_all(self) -> None:
"""Start all channels and the outbound dispatcher."""
if not self.channels:
logger.warning("No channels enabled")
return
# Start outbound dispatcher
self._dispatch_task = asyncio.create_task(self._dispatch_outbound())
# Start channels
tasks = []
for name, channel in self.channels.items():
logger.info("Starting {} channel...", name)
tasks.append(asyncio.create_task(self._start_channel(name, channel)))
# Wait for all to complete (they should run forever)
await asyncio.gather(*tasks, return_exceptions=True)
总体上的channel的启动代码如上。
在至少有一个已注册频道时,先起 _dispatch_outbound 消费总线出站消息,再为每个频道 create_task(start),
最后用 gather(..., return_exceptions=True) 一直等到这些长任务结束(正常即进程生命周期);无任何启用频道则直接返回。
总体上来说:
shell
1. 使用 async def 定义协程
2. 使用 asyncio.run 运行协程
-
asyncio.create_task(self._dispatch_outbound())
它将协程(coroutine)封装为任务(Task)对象,并立即调度该任务在事件循环中并在后台执行,从而允许在等待其完成的同时执行其他代码
dispatch_outbound是一个 async 死循环,在 start_all() 里被 asyncio.create_task 单独跑,从 MessageBus 的出站队列取 OutboundMessage,
按 msg.channel 找到对应 BaseChannel,再真正发到 Telegram/Discord 等pythonasync def _dispatch_outbound(self) -> None: """Dispatch outbound messages to the appropriate channel.""" logger.info("Outbound dispatcher started") while True: try: msg = await asyncio.wait_for( self.bus.consume_outbound(), timeout=1.0 ) if msg.metadata.get("_progress"): if msg.metadata.get("_tool_hint") and not self.config.channels.send_tool_hints: continue if not msg.metadata.get("_tool_hint") and not self.config.channels.send_progress: continue channel = self.channels.get(msg.channel) if channel: await self._send_with_retry(channel, msg) else: logger.warning("Unknown channel: {}", msg.channel) except asyncio.TimeoutError: continue except asyncio.CancelledError: break-
asyncio.wait_for(self.bus.consume_outbound(), timeout=1.0)
这个是 asyncio 库中用于给异步操作(协程或任务)设置超时限制的函数。如果 aw 在 timeout 秒内未完成,则会触发 asyncio.TimeoutError 异常,并取消该任务
用来消费出站的消息,并发动到对应的Channel中,如feishu等
这里的_send_with_retry最终会调用channel.send,这里我们以feishu的处理逻辑为例:loop = asyncio.get_running_loop() ... if ext in self._IMAGE_EXTS: key = await loop.run_in_executor(None, self._upload_image_sync, file_path) if key: await loop.run_in_executor( None, _do_send, "image", json.dumps({"image_key": key}, ensure_ascii=False), )- asyncio.get_running_loop()
在异步函数(async def)或回调函数内部,获取当前线程的事件循环对象,以便绑定定时器、socket 或执行其他循环级操作
这是为了后续获取线程池的方便 - loop.run_in_executor(None,...)
通过协程将任务委托给线程池 (ThreadPoolExecutor) 执行,从而避免阻塞核心的异步事件循环,因为协程是在同一个线程中运行的
这里的第一个参数为None,则使用事件循环默认的线程池,而且这里用了 await,所以进行同步等待
- asyncio.get_running_loop()
-
-
asyncio.create_task(self._start_channel(name, channel))
这里启动对应的 Channel,以feishu为例:pythonasync def start(self) -> None: """Start the Feishu bot with WebSocket long connection.""" self._loop = asyncio.get_running_loop() ... builder = lark.EventDispatcherHandler.builder( self.config.encrypt_key or "", self.config.verification_token or "", ).register_p2_im_message_receive_v1( self._on_message_sync ) ... def run_ws(): import time import lark_oapi.ws.client as _lark_ws_client ws_loop = asyncio.new_event_loop() asyncio.set_event_loop(ws_loop) # Patch the module-level loop used by lark's ws Client.start() _lark_ws_client.loop = ws_loop try: while self._running: try: self._ws_client.start() except Exception as e: logger.warning("Feishu WebSocket error: {}", e) if self._running: time.sleep(5) finally: ws_loop.close() self._ws_thread = threading.Thread(target=run_ws, daemon=True) self._ws_thread.start() logger.info("Feishu bot started with WebSocket long connection") logger.info("No public IP required - using WebSocket to receive events") # Keep running until stopped while self._running: await asyncio.sleep(1)- self._loop = asyncio.get_running_loop()
这里也是获得当前线程的事件循环对象,和上面send的时候获取的是一个。 - _loop.is_running 和 asyncio.run_coroutine_threadsafe(self._on_message(data), self._loop)
这个主要出现在_on_message_sync方法中,
is_running() 用于检查当前线程中正在运行的事件循环是否处于运行状态(而非已停止或关闭)
asyncio.run_coroutine_threadsafe(coro, loop) 用于在主线程或其他工作线程中安全地提交协程到异步事件循环(loop)中执行的函数 - threading.Thread(target=run_ws, daemon=True)
启动线程,这里的run_ws方法是在另一个线程中跑,这里会单独启动协程,这是为了在独立守护线程里跑,而收消息同步回调再通过 self._loop 把异步处理 _on_message 投到主事件循环- ws_loop = asyncio.new_event_loop()
用于显式创建并返回一个新的事件循环对象的函数。它不会自动将新循环设置为当前线程的默认循环,主要用于需要手动管理循环、在不同线程中创建独立循环,或避免默认循环策略限制的场景 - asyncio.set_event_loop(ws_loop)
用于将指定的事件循环实例(loop)设置为当前OS线程的默认事件循环 - ws_loop.close()
asyncio.new_event_loop().close 释放其底层资源(如 socket、文件描述符),防止资源泄露
- ws_loop = asyncio.new_event_loop()
- self._loop = asyncio.get_running_loop()
-
asyncio.gather
用于并发运行多个可等待对象(如协程或任务)并收集结果的实用函数。它按参数顺序返回所有结果列表,await 时暂停直到所有任务完成
其中return_exceptions = True表示异常会被视为正常结果放入结果列表