多线程交互的思考

最近开始写多线程的程序,整理一下对多线程代码的思考。

用多线程的时机

多线程为任务划分提供了天然的要求。如果能抽象出来流水线的业务模型,便有了多线程的用武之地。

线程间交互

虽说操作系统或、C库或者语言给应用层提供了多种线程交互的方式,包括但不限于锁、信号量,但线程之间最好还是不要有什么交互,这样才是最安全的。

退而求其次,多个线程之间只有一个交互点:无锁队列,这种编程模型交互很清晰,也不容易出错。一个好的无锁fifo实现是必要的。 多线程天生适合这种pipeline的方式处理数据。

无锁队列

无锁队列只需要关注memory order就能实现交互,犯不着把进程切出去,独占CPU。看着好浪费呀。确实,对于time critical的应用,只能busy loop。

无锁队列也有很多种,从使用上来看,队列里放定长数据还是不定长数据,其实现方法是完全不一样的。定长item的要简单一点,in/out节点可以用2个atomic来指代下标就可以了。不定长的实现我现在还没有见到过,但不定长的话,怎么解决回绕的问题,想必也是有点头疼的。

为了避免思考回绕怎怎么解决,我倒是想到了可以采用指针+memory pool的方式,这对内存管理又提出了很高的要求。

所以还是结合业务来看,如果能接受一定量的空间冗余,把自己的msg都包到一个union里,这倒是不错的选择。要知道,定长的也不一定慢,我自己在机器上测过一些数据,找个时间系统的来重新测试一遍。

多线程下的内存管理

pipeline工作模式下内存管理有一个思路,就是线程间只传递指针,具体的数据结构在最开始的线程中进行分配(可以用pool的方式来优化),也只能在最开始的线程进行释放。线程间有2个queue,前向+后向。每个线程用完后决定继续往后面传递还是回传到上一个线程。增加了一点程序编写的复杂度,换来对内存管理的简化。

日志

多线程框架下,日志也是需要考量的一个重要组件。对C++来说,根据编译条件开启、关闭部分代码可以达到加速的效果,虽然很微小,但还是要做。日志就是属于这样的一种代码。

除了boost.log/spdlog/glog等比较老派的日志库,后面出现的NanoLog算是开辟了新纪元,二进制日志保证了吞吐,强类型日志保证了速度。但是format本身的确是一个耗时的操作,实际操作中打日志一般会放在单独的线程中进行。

网络栈

网络协议本身只是一种约定而已,搞明白协议要干啥后,实现协议也只是一种相对简单的体力活。

比如,抓包要用packet socket,如果protocol指定为0,那么在应用层是不会收到任何包的;这个在man page里没有提及,也是在看内核的时候想到的。 可以用mmap的方式来抓包,这也是libpcap用的方法。这居然还对应着不同的版本,每个版本用的内存模型和header结构也不一样。在看这些代码的时候,仿佛看到了各个大公司里的某些个小人物,也就几个人吧,推动着世界上某个重要东西的发展。我能想象到一个黄昏的下午,一个程序员改好代码,写好文档,躺下休息的情况。在这个场景后,世界上的抓包程序的写法就定了,造物主也无法再改变什么。

想要更高的性能,就需要绕过kernel,从网卡上自己收。反正也是要低延时了,抛开一切自己搞,while true receive frame,送到队列里交给下个线程处理。市面上常见的方法有DPDK,主要对接的是Intel自己的网卡。还有一些其他的专门做低延迟的网卡,比如solarflare和exanic,有自己单独的lib和kernel module。