使用asyncio开发WebSocket连接器,手动调用可获取行情但加while循环后数据为空?
问题根因
你猜测的方向完全正确,问题核心是同步while循环的阻塞直接卡死了整个异步事件循环,完整逻辑链如下:
- Jupyter环境本身内置了常驻运行的asyncio事件循环,你调用
asyncio.get_event_loop()拿到的就是这个正在调度任务的循环,start()创建的websocket连接协程会被丢进循环的待执行队列,由后台循环调度执行。所以你手动切换单元格执行get_quotes()的时候,事件循环一直保持运行,websocket能正常收消息、更新行情,读出来的数据是正常的。 - 当你运行自己写的同步
while(True) + time.sleep(3)代码时,time.sleep是完全阻塞线程的系统调用,会直接占死主线程,asyncio事件循环根本拿不到CPU执行权——这时候你之前创建的websocket连接协程连建立连接、发订阅消息、接收行情的机会都没有,自然quotes一直是空的。
原代码还有几个附带的缺陷,在非Jupyter场景下会直接导致功能失效:
- 非Jupyter环境下只调用
create_task创建协程任务,但从来没有让事件循环持续运行,整个WS连接协程根本不会被执行,程序直接就退出了。 __init__里传入的client参数没有被任何逻辑使用,属于冗余参数。- 没有任何异常处理和重连逻辑,公网WS连接一旦因为网络抖动、服务端策略断连,程序就会静默失去数据。
- 直接高频修改空DataFrame的第0行,在每秒几十上百条的高频行情流下,性能开销极高,会直接拖慢整个事件循环。
修正方案
不要把同步阻塞逻辑和异步事件循环混在同一条执行路径上,把所有轮询读数据的逻辑也改成协程,交给事件循环统一调度。改造后的完整代码如下:
import pandas as pd import json import websockets import asyncio class BinanceQuotesWS: def __init__(self, pair): # 用原生变量存最新行情,避免高频修改DataFrame的性能损耗 self.last_quote = {"Timestamp": None, "Price": None} self.pair = pair self.socket = 'wss://fstream.binance.com/ws' self.websocket = None self.is_running = False def get_quotes(self): # 需要返回DataFrame结构的时候再临时生成 return pd.DataFrame([self.last_quote]) async def connect(self): """建立WS连接,内置异常重连逻辑""" self.is_running = True while self.is_running: try: self.websocket = await websockets.connect(self.socket) await self.subscribe_quotes() except Exception: # 连接断开后等2秒重连,避免频繁请求触发服务端限流 await asyncio.sleep(2) async def subscribe_quotes(self): """发送订阅并持续接收行情""" subscribe_message = { "method": "SUBSCRIBE", "params": [self.pair.lower()+"@trade"], "id": 1 } await self.websocket.send(json.dumps(subscribe_message)) async for msg in self.websocket: msg = json.loads(msg) if 'p' in msg: # 直接更新原生变量,不触碰DataFrame结构 self.last_quote["Timestamp"] = msg['E'] self.last_quote["Price"] = float(msg['p']) async def poll_quotes(self, interval=3): """异步轮询行情逻辑,替代原来的同步while循环""" while self.is_running: quotes = self.get_quotes() print(quotes) # 用asyncio.sleep替代time.sleep,不会阻塞事件循环调度 await asyncio.sleep(interval) def start(self): """启动入口:把WS连接、行情轮询逻辑注册到事件循环并发执行""" async def main(): # 两个协程并行运行,事件循环会自动交替调度,不会互相阻塞 task_ws = asyncio.create_task(self.connect()) task_poll = asyncio.create_task(self.poll_quotes()) await asyncio.gather(task_ws, task_poll) asyncio.run(main())
使用方式非常简单,直接启动即可,不需要额外写同步循环:
temp_ws = BinanceQuotesWS(pair='BTCUSDT') temp_ws.start()
异步场景通用优化建议
- 绝对禁止在异步事件循环的执行路径上调用同步阻塞方法:包括
time.sleep、requests库的HTTP请求、无异步版本的数据库操作、重计算的pandas逻辑,这类调用一执行就会把整个事件循环卡死,所有异步任务都会停摆。如果一定要运行同步逻辑,必须丢到asyncio.to_thread中异步执行,不要占用事件循环的主线程。 - 高频写入场景不要频繁修改大体积可变对象:WS行情流是典型的高频写入场景,每次给DataFrame新增/修改行都会触发内存重新分配,性能开销远大于用原生字典、列表存数据的方式,只有在需要做数据分析、输出结构化结果的时候再转成DataFrame。
- 公网WS连接必须加自动重连机制:网络抖动、服务端主动断连都是常态,没有重连逻辑的连接器运行几分钟就会静默失去数据。
- 非Jupyter环境下统一用
asyncio.run()作为事件循环的启动入口,不要依赖老旧的asyncio.get_event_loop()方法,避免跨环境的兼容性问题。
内容的提问来源于stack exchange,提问作者romanzh
相关产品推荐
相关产品推荐

