线程结构相当于服务器程序的灵魂,一个好的服务器程序必须线程结构清析且线程利用率高。下面主要以伪代码的形式列举一些常用的线程结构。
1 单业务处理线程结构
int main() { Init(); while (queue.GetMessage(timeout, message)) // 这里的队列要支持多个线程写,一个线程读 { DispatchMessage(message); DetectTimer(); // 如果需要定时器的话 } DeInit(); return 0; }
windows窗口程序及我见过的大部分网游服务器都使用这种线程结构。因为只有一个业务处理主线程,在进行业务处理的时候,不需要考虑线程同步。当然这种服务器并不只是只有一个线程在工作,通常网络和DB(如果有的话)会使用独立的线程,例如当网络收到一个消息后,就Enqueue到队列中,这时queue.GetMessage返回,由DispatchMessage找到对应的处理函数去做具体的处理。
2 多业务处理线程
int main() { Init(); for (1 to 10) { StartThread(&ThreadFunction); } WaitForExit(); DeInit(); return 0; } int ThreadFunction() { while (queue.GetMessage(timeout, message)) // 队列要支持多写多读 { DispatchMessage(message); } return 0; }
网络线程收到消息后,Enqueue到队列,其中一个线程的GetMessage会返回并处理。DB模块通常使用这种线程结构。
3 并发单线程
class WorkThread { public: void Start(); private: void AsnycRead(int clientId); void AsnycWrite(int clientId, Message* message); void OnRead(int clientId, Message* message); void OnWriteComplite(int clientId); }; int main() { for (1 to 10) { new WorkThread(); } WaitForExit(); return 0; }
我其实没找到一个好的名称和伪代码去描述这种线程结构,多加点文字说明吧。这种线程结构通常在程序启动时开启多个线程(通常是CPU核芯数),每个线程完整的加载其需要的配置,每个线程都是独立的且功能都是一样的,线程与线程之间没有仍何交互,用户代码中没有队列。一个连接建立好后,始终在同一个线程空间中运行,因此也没有任何线程同步。这种线程序结构非常适合网关类、转发类等慢操作比较少的服务器,能充分利用多核的CPU资源,提高服务效率。使用iocp及boost.asio很容易实现这种线程结构。
4 线程池
int main() { Init(); for (1 to 100) { ThreadPool.Add(new WorkThread()); } WaitTask(); WaitForExit(); DeInit(); return 0; } void WaitTask() { WorkThread t = ThreadPool.GetFree(); t.Wakeup(); // 唤醒子线程后立即返回,子线程等待客户端连接,连接成功后先Wakeup下一个线程去等待客户端连接, 由本线程执行任务 }
曾经比较火的Leader/Follower模式,据说tomcat用的就是这个,但个人觉得这种结构效率并不高,因为这种模式网络IO通常用同步,大大降低了线程的利用率。
结束语
以上只上对线程结构的简化描述,实际用到的服务器并不会只这么简单,可能是多种结构的组合,也可能是上面没提到的线程结构。但高并发高效率的服务器要点其实很简单:提高每个线程的利用率,尽量避免忙等、睡眠及过多的线程切换,同时减少加锁及各种队列的入列出列。业务处理方面可将业务细化,由单服务器处理转成多服务器处理,能并发的就并发……不过这超出了本文的范围,有机会再详述。
附感言
在08年之前,boost还不是那么流行,我通常会使用自己实现的线程相关的基础库,如Mutex, Condition, Thread, SharedPtr, MessageQueue——当然基本上也是从boost中抄过来的。时间飞奔,2016年的现在,C11也包含了以上大部分库,还剩MessageQueue实现起来也是分分钟的事……时代更美好了!