本次项目为Qt聊天室第三次开发的经验总结
项目地址:
https://gitee.com/how-to-name/qt—simple-online-chat-room GitHub – RH211-sys/qt—simple-online-chat-room · GitHub
快速理解事件循环
可以借助epoll的执行过程来快速理解事件循环机制,其实就是如下:
拥有事件循环这一属性的线程先进入阻塞态
当事件发生时(网络数据到达,超时等),唤醒该线程
处理事件:调用对应的处理函数
处理完成后进入阻塞态,回到步骤1
事件发生后,内核把"发生事件的对象"整理成一张就绪列表直接交给了线程——线程醒来直接读这张列表,不需要回容器里逐个找哪个对象发生了事件
事件循环对socket管理
如何良好的管理socket在网络项目中是一个非常重要的事情,好的管理方式往往能提高开发效率,提高可维护性
对socket管理的解决方案反思
在该Qt项目中,我前后尝试了三个阶段的解决方案
所有线程都能对socket进行读写
所有线程都能对socket进行加锁读写
主线程对socket读写(网络IO),其他线程进行本地IO和其他操作
方案1的思考来源:为了能快速的处理网络IO,想着使用多线程处理网络数据。
缺陷:容易出现竞态,会出现多个线程同时对同一个socket读/写的情况,且后续了解到Qt不推荐跨线程直接操作socket对象
方案2的思考来源:为了避免竞态,只能牺牲一些性能,加锁来同步处理网络数据
缺陷:频繁的消息转发/文件传输,容易导致锁的开销比较大,且后续了解到Qt不推荐跨线程直接操作socket对象,尽管保证了同步
方案3的思考来源:减少锁开销,用主线程处理网络IO
优势:实现网络IO和本地IO的解耦,且性能比方案2提升很大 潜在缺陷:单线程管理可能不支持大量客户端的管理
更好的方案(更适合大体量的网络项目规模)
使用IO线程组对socket分组管理(每个线程负载均匀的socket)
可以简单理解为如下模型:
用IO线程组管理socket
任务队列所存储的任务类型:
1. 新增socket(按最少连接数分配到线程)
2. 删除socket
3. 跨线程写请求(其他线程要往某个socket写数据时,投递"数据 + 目标socket",由该socket所属线程执行write/flush)
线程池中的每个线程:
每个线程单独用epoll去管理socket,线程之间的socket集不重叠
事件发生:
1. readReady: 线程对发生事件的socket进行读操作
2. 其他线程发来往某个socket的写操作请求:线程对发生事件的socket进行写操作,并flush
3. xxxx
事件处理完后(没有任务),进入阻塞态,如此循环
这其实也是多 Reactor 多线程模型。
理解了这个后,对多线程/线程池模型的认识会更加清晰
实际项目中的情况是: 主线程统一管理大部分网络 I/O,聊天被单独派到子线程A 读写(唯一例外,锁保护)
因为聊天消息内容很少,且性能影响不高,故无需过多考虑,就没有进行太多改动
线程池落盘原理(生产-消费队列)
本次项目的线程池用于处理网络传输的文件,把客户端传来的文件写到服务端的磁盘中
我使用了两个任务队列
队列A(线程池任务队列):客户端开始传输文件时先发 header(文件名、大小等),主线程解析完 header 后创建一个落盘任务,提交进队列A。
队列B(每个任务自带的数据队列):之后客户端只发数据,主线程每次从 socket 读出数据块,追加进该任务自己的队列B。
线程池分配一个空闲线程执行该任务:调用run() 进入循环:等自己的队列B有数据 → 取块写盘; 主线程读到结束标志后调 finish() 通知 → 线程写完队列剩余数据、结束任务(回调主线程收尾)→ 线程回到池中空闲,等待队列A分配下一个任务。
网硕互联帮助中心







评论前必须登录!
注册