异步框架+POLL混合方案应对ESP32 MPY多任务+TCP多连接

ESP32 MPY 多任务异步框架,需要支持多个ModbusTCP连接,使用POLL同步IO多路复用实现这个目的,引出了"混合方案",这种模式在嵌入式系统中很常见,既能利用传统socket编程,又能获得异步并发的优势。

1、请问下面ESP32 MPY的POLL IO多路复用测试代码"events = poller.poll(1000) # 1秒超时",这是异步协程吗?是否在1000ms内会主动让出CPU使用权?

复制代码
import socket, select, machine
 
server_socket = socket.socket()
try:
    server_socket.bind(('0.0.0.0', 8080)) # 0000绑定任意可用IP
except Exception as e:
    machine.reset()
server_socket.listen(3)  # 允许3个挂起连接
print(server_socket.fileno()) # 54
poller = select.poll() # select.select没有select.poll高级
poller.register(server_socket, select.POLLIN)
 
clients = {}  # {fd: (conn, address)} 文件描述用二元组socket.accept()
 
while True:
    events = poller.poll(1000)  # 1秒超时
    #print(events) # [] --> [(<socket>, 1)]有事件发生就处理
    for s, event in events:
        #print(fd) # <socket>
        if s == server_socket:  # 新连接 .fileno()
            conn, addr = server_socket.accept()
            print("新客户端:", addr) # ('192.168.2.104', 53064)
            conn.setblocking(False)
            poller.register(conn, select.POLLIN)
            clients[conn] = (conn, addr)
            conn.send(b"Connected!\r\n")
        else:  # 客户端数据
            conn, addr = clients[s]
            data = conn.recv(512)
            if data:
                print(f"{addr} 发送:", data)
                conn.send(data)  # Echo 回传
            else:  # 断开
                print(f"{addr} 断开")
                poller.unregister(conn)
                conn.close()
                del clients[conn]

这段代码是 同步阻塞 的 I/O 多路复用,不是异步协程。在MicroPython中,select.poll()是一个同步的I/O多路复用机制,它会阻塞直到有事件发生或者超时。具体来说,代码中的poller.poll(1000)会阻塞最多1000毫秒(1秒),直到有注册的文件描述符(在这里是socket)有事件发生(比如可读)。在阻塞期间,它不会主动让出CPU使用权。也就是说,在这1秒内,如果没有任何事件发生,程序会一直等待,直到超时后才继续执行后面的代码。如果有事件发生,它会立即返回事件列表,然后处理这些事件。因此,这段代码是同步的,而不是异步的。在同步代码中,如果没有事件发生,程序会阻塞在poll()调用上,不会执行其他任务。在异步协程中,我们通常使用asyncio库,其中会有事件循环,并且使用await来让出CPU,以便在等待I/O时执行其他协程。

虽然这段代码是同步的,但它通过select.poll 实现了单线程处理多个连接,可以在一个线程中同时监听多个socket,避免了为每个连接创建线程的开销。仍然是同步编程模型,只是通过轮询实现了伪并发。关于CPU使用率:在 poll(1000) 阻塞期间,线程是休眠状态,CPU 可以执行其他任务,**但在MicroPython的单线程环境中,如果没有其他线程,CPU实际上在等待 I/O 事件。**这比传统的 accept() 阻塞要高效,因为可以同时等待多个连接

2、使用poll.poll(1)同步阻塞1ms,将这段poll内容改写为asyncio异步函数调用可否?因为我还有其他异步任务(modbus库、传感器数据采集)需要执行。

poll.poll(1) + await asyncio.sleep_ms(0) 确实可以实现伪并发。

复制代码
# 方案1:混合模式 - poll在异步协程中
'''
poll_task函数中每处理完一次轮询就让出CPU(通过await asyncio.sleep(0))。这样,其他协程(如other_tasks)就有机会运行。但是,这种方式仍然有轮询开销,且不是事件驱动的。真正的异步IO是事件驱动的,当没有事件时,CPU是休眠的。而上面的混合方案中,即使没有事件,也会每1毫秒轮询一次,然后让出CPU,这会导致事件循环频繁唤醒,增加功耗和CPU占用。因此,我仍然推荐使用asyncio的异步TCP服务器,它是事件驱动的,在没有事件时不会占用CPU。请根据你的实际需求选择方案。如果你的其他异步任务也是基于asyncio的,那么使用asyncio的TCP服务器是最佳选择。如果你的其他任务无法改为异步,那么你可能需要考虑使用多线程或上面的混合方案。
'''
import socket, select, machine
import uasyncio as asyncio

class PollBasedServer:
    def __init__(self, port=8080):
        self.server_socket = socket.socket()
        self.server_socket.bind(('0.0.0.0', port))
        self.server_socket.listen(3)
        self.server_socket.setblocking(False)  # 非阻塞
        
        self.poller = select.poll()
        self.poller.register(self.server_socket, select.POLLIN)
        self.clients = {}
        
    async def run(self):
        """异步运行poll服务器"""
        print("Poll服务器启动...")
        while True:
            try:
                # 只等待10ms,不会长时间阻塞
                events = self.poller.poll(10)
                
                for s, event in events:
                    if s == self.server_socket:
                        await self.handle_new_connection() # 虽然是同步的,但保持await风格
                    else:
                        await self.handle_client_data(s)   # 虽然是同步的,但保持await风格
                
                # 让出CPU给其他异步任务
                # 协作式多任务的核心机制
                # 在同步代码混合异步框架时必不可少
                # 真正的协程切换点
                await asyncio.sleep_ms(0)
                
            except Exception as e:
                print("服务器错误:", e)
    
    '''
    函数内部并没有使用异步的IO操作,它们使用的是非阻塞socket的同步读写。所以,这两个函数实际上不会挂起,而是同步执行。
    这样,如果有一个客户端发送大量数据,那么处理这些数据可能会占用较长时间,从而阻塞其他协程。
    为了解决这个问题,考虑在数据处理时也使用异步方式,或者确保数据处理时间很短,然后通过频繁的await asyncio.sleep_ms(0)来让出控制权。
    '''
    async def handle_new_connection(self):
        """处理新连接"""
        conn, addr = self.server_socket.accept()
        print("新客户端:", addr)
        conn.setblocking(False)
        self.poller.register(conn, select.POLLIN)
        self.clients[conn] = (conn, addr)
        conn.send(b"Connected!\r\n")
    
    async def handle_client_data(self, sock):
        """处理客户端数据"""
        conn, addr = self.clients[sock]
        try:
            data = conn.recv(512)
            if data:
                print(f"{addr} 发送:", data)
                conn.send(data)  # Echo 确保数据处理时间很短
            else:  # 断开
                print(f"{addr} 断开")
                self.poller.unregister(conn)
                conn.close()
                del self.clients[conn]
        except:
            # 连接可能已断开
            if sock in self.clients:
                del self.clients[sock]

# 其他异步任务示例
async def modbus_task():
    """模拟Modbus任务"""
    while True:
        print("Modbus任务执行中...")
        # 这里调用你的Modbus库
        await asyncio.sleep(5)  # 每5秒执行一次

async def sensor_task():
    """模拟传感器采集任务"""
    while True:
        print("采集传感器数据...")
        # 这里调用传感器采集代码
        await asyncio.sleep(5)  # 每5秒执行一次

async def main():
    # 创建poll服务器实例
    server = PollBasedServer(8080)
    
    # 同时运行多个异步任务
    await asyncio.gather(
        server.run(),     # poll服务器
        modbus_task(),    # Modbus任务
        sensor_task(),    # 传感器任务
    )

# 启动事件循环
asyncio.run(main())

测试结果:

复制代码
MicroPython v1.27.0 on 2025-12-09; Generic ESP32 module with ESP32
Type "help()" for more information.
>>> %Run -c $EDITOR_CONTENT
MPY: soft reboot
    ESP32 IP           NETMASK         GATEWAY            DNS
('192.168.1.119', '255.255.255.0', '192.168.1.1', '8.8.8.8')
Signal strength: -47
('192.168.1.119', '255.255.255.0', '192.168.1.1', '8.8.8.8')
WebREPL server started on http://192.168.1.119:8266/
Started webrepl in normal mode
Poll服务器启动...
Modbus任务执行中...
采集传感器数据...
新客户端: ('192.168.1.110', 63382)
('192.168.1.110', 63382) 发送: b'3333333333333333'
Modbus任务执行中...
采集传感器数据...
Modbus任务执行中...
采集传感器数据...
新客户端: ('192.168.1.110', 63398)
('192.168.1.110', 63398) 发送: b'111111111111'
('192.168.1.110', 63382) 发送: b'3333333333333333'
新客户端: ('192.168.1.110', 63405)
Modbus任务执行中...
采集传感器数据...
('192.168.1.110', 63398) 发送: b'111111111111'
('192.168.1.110', 63405) 发送: b'2222222222222'
('192.168.1.110', 63382) 发送: b'3333333333333333'
('192.168.1.110', 63398) 发送: b'111111111111'
('192.168.1.110', 63405) 发送: b'2222222222222'
('192.168.1.110', 63382) 发送: b'3333333333333333'
('192.168.1.110', 63398) 发送: b'111111111111'
('192.168.1.110', 63405) 发送: b'2222222222222'
('192.168.1.110', 63382) 发送: b'3333333333333333'
Traceback (most recent call last):
  File "<stdin>", line 107, in <module>
  File "asyncio/core.py", line 1, in run
  File "asyncio/core.py", line 1, in run_until_complete
  File "<stdin>", line 31, in run
KeyboardInterrupt: 
>>> 

微观执行流程:

假设有3个任务:A(poll服务器)、B(Modbus)、C(传感器)

时间线:

  1. 任务A执行 → poll.poll(1)等待1ms

  2. 任务A执行 → 处理事件(如果有)

  3. 任务A执行 → 遇到 await asyncio.sleep_ms(0)

  4. ↑ 事件循环切换:保存任务A状态,切换到任务B

  5. 任务B执行 → 执行一部分Modbus操作

  6. 任务B遇到 await 或完成一段 → 切换回事件循环

  7. 事件循环切换:可能切换到任务C

  8. 任务C执行 → 采集传感器数据

  9. 任务C完成一段 → 切换回事件循环

  10. 事件循环发现任务A就绪 → 切换回任务A

  11. 任务A从 await asyncio.sleep_ms(0) 后面继续执行

其他方案2:更简洁的异步TCP服务器。如果你的需求不复杂,可以直接用asyncio的TCP服务器。

异步框架+POLL混合方案非常合适:

保持高性能:POLL的快速响应

支持并发:异步框架的多任务能力

资源高效:适合ESP32限制

代码可控:同步处理逻辑清晰

易于调试:问题定位简单

这种架构在工业控制、物联网网关等场景中非常实用,既能处理高速网络通信,又能并行执行Modbus、传感器采集等任务。

锦上添花:

  1. 动态调整POLL频率

    async def poll_server_task():
    server = PollTCPServer(8080)

    复制代码
     poll_interval = 1  # 默认1ms
     
     while True:
         server.poll_events()
         
         # 根据负载动态调整
         client_count = len(server.clients)
         if client_count == 0:
             await asyncio.sleep_ms(10)  # 无客户端时降低频率
         elif client_count > 5:
             await asyncio.sleep_ms(0)   # 高负载时立即让出
         else:
             await asyncio.sleep_ms(poll_interval)

2、批量处理优化

复制代码
def poll_events(self):
    """批量处理优化版本"""
    events = self.poller.poll(0)
    
    # 按事件类型分组处理
    new_connections = []
    data_events = []
    error_events = []
    
    for fd, event in events:
        if fd == self.server:
            new_connections.append(fd)
        elif event & select.POLLIN:
            data_events.append(fd)
        elif event & select.POLLERR:
            error_events.append(fd)
    
    # 批量处理(更高效)
    for fd in new_connections:
        self._accept_connection()
    
    for fd in data_events:
        self._receive_data(fd)
    
    for fd in error_events:
        self._close_connection(fd)

3、添加看门狗保护

相关推荐
梦帮科技2 小时前
Scikit-learn特征工程实战:从数据清洗到提升模型20%准确率
人工智能·python·机器学习·数据挖掘·开源·极限编程
xqqxqxxq2 小时前
Java 集合框架之线性表(List)实现技术笔记
java·笔记·python
verbannung2 小时前
Python进阶: 元类与属性查找理解
python
想用offer打牌2 小时前
LLM参数: Temperature 与 Top-p解析
人工智能·python·llm
小智RE0-走在路上3 小时前
Python学习笔记(6)--列表,元组,字符串,序列切片
笔记·python·学习
feeday3 小时前
Python 删除重复图片 优化版
开发语言·python
ss2733 小时前
Java线程池全解:工作原理、参数调优
java·linux·python
于是我说3 小时前
一份Python 面试常见问题清单 覆盖从初级到高级
开发语言·python·面试
BoBoZz193 小时前
RotationAroundLine 模型的旋转
python·vtk·图形渲染·图形处理