串口读写非线程安全,需加锁或单线程封装;推荐std::mutex保护read/write,高频率场景用生产者-消费者模型;windows宜用waitformultipleobjects事件机制,linux用select/poll;注意句柄/fd生命周期、二进制数据用std::vector、配置需一次性完成。

串口读写本身不是线程安全的,得自己加锁
Windows 的 CreateFile 打开的串口句柄、Linux 的 open() 返回的 fd,底层驱动不保证并发读写安全。多个线程直接调用 ReadFile/WriteFile 或 read()/write() 会丢数据、读到乱序字节,甚至触发 ERROR_IO_PENDING(Windows)或 EAGAIN(Linux)这类非预期错误。
最稳妥的做法是:把串口 I/O 封装成一个单线程服务,其他线程通过队列发读/写请求;或者在读写操作外围加同一把互斥锁。
- 推荐用
std::mutex+std::lock_guard包裹每次read()和write()调用,简单有效 - 如果读写频率高、延迟敏感,别用锁,改用生产者-消费者模型:一个专属 I/O 线程轮询串口,其余线程只往
std::queue或moodycamel::ConcurrentQueue推任务 - 注意:Windows 下
SetCommTimeouts设置的超时会影响阻塞行为,锁住期间若读超时,整个临界区会被拖慢——所以超时值要设小(比如 10–50ms),避免锁争用
Windows 下用 WaitForMultipleObjects 等串口事件,比轮询更省资源
直接 ReadFile 阻塞或短间隔 sleep 轮询,CPU 占用高且响应滞后。Windows 提供串口事件通知机制,配合 WaitForMultipleObjects 可以让线程挂起等待数据到达,唤醒后立刻读。
关键步骤:
- 调用
SetCommMask开启EV_RXCHAR(有数据可读)和EV_ERR(错误事件) - 用
WaitForMultipleObjects等待串口句柄 + 其他线程信号(如退出事件),避免死等 - 触发后调用
ClearCommError检查实际字节数,再用ReadFile读取——不能假设一次读完所有缓存数据
Linux 没原生事件机制,得用 select() 或 poll() 监听 fd 是否可读,原理类似但 API 更轻量。
std::thread 启动读线程时,必须确保串口句柄/fd 在线程间正确传递
常见错误是把局部打开的串口句柄传给子线程,主线程一退出,句柄被关闭,子线程 ReadFile 直接失败返回 0 或 -1。
- Windows:句柄默认不可继承,创建线程前用
SetHandleInformation设HANDLE_FLAG_INHERIT,或更稳妥地——把句柄作为参数传入线程函数,并在线程内保持其生命周期 - Linux:fd 默认可继承,但要注意
fork()会复制 fd,而std::thread不涉及 fork,直接传int fd即可;不过得确保主线程不提前close(fd) - 强烈建议封装成类,串口资源作为成员变量,读写线程用
std::shared_ptr持有该对象,靠引用计数保活
二进制数据读写别用 std::string 当容器,容易截断
串口常传含 \x00 的二进制帧,用 std::string 存会导致 data().size() 返回错误长度,std::string 构造函数遇到 \x00 就截断。
- 统一用
std::vector<uint8_t></uint8_t>当缓冲区类型,读写都基于.data()和.size() - Windows 下
ReadFile第三个参数是LPDWORD,必须传非空地址,否则崩溃;Linux 下read()返回值是 ssize_t,要检查是否为 -1 并判errno == EINTR再重试 - 写之前确认串口已配置好波特率、校验位等——这些设置不是线程安全的,必须在打开后、任何读写前一次性完成
真正麻烦的从来不是多线程本身,而是串口硬件状态不可控:拔插、驱动重置、缓冲区溢出。线程里做读写,得随时准备处理 INVALID_HANDLE_VALUE 或 fd == -1 这种底层失效,而不是只盯着锁和队列。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











