linux 消息队列 线程?多线程和消息队列的区别
linux线程查询指令linux线程查询
怎么在linux系统下查看网卡状态信息?
方法一:
ethtooleth0采用此命令可以查看到网卡相关的技术指标。
(不一定所有网卡都支持此命令)
ethtool-ieth1加上-i参数查看网卡驱动。
可以尝试其它参数查看网卡相关技术参数。
方法二:
也可以通过dmesg|grepeth0等看到网卡名字(厂家)等信息。
通过查看/etc/sysconfig/network-scripts/ifcfg-eth0可以看到当前的网卡配置包括IP、网关地址等信息。
当然也可以通过ifconfig命令查看。
Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。Linux操作系统诞生于1991年10月5日(这是第一次正式向外公布时间)。Linux存在着许多不同的Linux版本,但它们都使用了Linux内核。Linux可安装在各种计算机硬件设备中,比如手机、平板电脑、路由器、视频游戏控制台、台式计算机、大型机和超级计算机。严格来讲,Linux这个词本身只表示Linux内核,但实际上人们已经习惯了用Linux来形容整个基于Linux内核,并且使用GNU工程各种工具和数据库的操作系统。
linux查看活跃线程命令?
可以执行ps-ef进行查看
Linux多线程通信?
PIPE和FIFO用来实现进程间相互发送非常短小的、频率很高的消息;
这两种方式通常适用于两个进程间的通信。
共享内存用来实现进程间共享的、非常庞大的、读写操作频率很高的数据(配合信号量使用);
这种方式通常适用于多进程间通信。
其他考虑用socket。这里的“其他情况”,其实是今天主要会碰到的情况:
分布式开发。
在多进程、多线程、多模块所构成的今天最常见的分布式系统开发中,
socket是第一选择
。消息队列,现在建议不要使用了----因为找不到使用它们的理由。在实际中,我个人感觉,PIPE和FIFO可以偶尔使用下,共享内存都用的不多了。在效率上说,socket有包装数据和解包数据的过程,所以理论上来说socket是没有PIPE/FIFO快,不过现在计算机上真心不计较这么一点点速度损失的。你费劲纠结半天,不如我把socket设计好了,多插一块CPU来得更划算。另外,进程间通信的数据一般来说我们都会存入数据库的,这样万一某个进程突然死掉或者整个服务器死了,也不至于丢失重要数据、便于回滚到之前的状态。从这个角度考虑,适用共享内存的情况也更少了,所以socket使用得更多。再多说一点关于共享内存的:共享内存的效率确实高,但它的重点在“共享”二字上。如果的确有好些进程共享一大块数据(如果把每个进程都看做是类的对象的话,那么共享数据就是这个类的static数据成员),那么共享内存就是一个不二的选择了。但是在面向对象的今天,我们更多的时候是多线程+锁+线程间共享数据。因此共享进程在今天使用的也越来越少了。不过,在面对一些极度追求效率的需求时,共享内存就会成为唯一的选择,比如高频交易系统。除此以外,一般是不需要特意使用共享内存的。另外,
PIPE和共享内存是不能跨LAN的
(FIFO可以但FIFO只能用于两个进程通信)
。
如果你的分布式系统随着需求的增加而越来越大所以你想把不同的模块放在不同机器上而你之前开发的时候用了PIPE或者共享内存,那么你将不得不对代码进行大幅修改......同时,即使FIFO可以跨越LAN,其代码的可读性、易操作性和可移植性、适应性也远没有socket大。这也就是为什么一开始说socket是第一选择的原因。最后还有个信号简单说一下。
请注意,是信号,不是信号量。
信号量是用于同步线程间的对象的使用的(建议题主看我的答案,自认为比较通俗易懂:
semaphore和mutex的区别?-Linux-知乎
)。信号也是进程间通信的一种方式。比如在Linux系统下,一个进程正在执行时,你用键盘按Ctrl+c,就是给这个进程发送了一个信号。进程在捕捉到这个信号后会做相应的动作。虽然信号是可以自定义的,但这并不能改变信号的局限性:
不能跨LAN、信息量极其有限
。在现代的分布式系统中,通常都是
消息驱动:
即进程受到某个消息后,通过对消息的内容的分析然后做相应的动作。如果你把你的分布式系统设置成信号驱动的,这就表示你收到一个信号就要做一个动作而一个信号的本质其实就是一个数字而已。这样系统稍微大一点的话,系统将变得异常难以维护;甚至在很多时候,信号驱动是无法满足我们的需求的。因此现在我们一般也不用信号了。因此,请记住:
除非你有非常有说服力的理由,否则请用socket。
顺便给你推荐个基于socket的轻量级的消息库:ZeroMQ。
linux下,如何查看工控机的串口被哪个线程占用,能否使该线程强制释放串口?
在串口的驱动程序注册的open函数里加入这样一句话:printk("process%dhasopenttyn",current->pid);可以判断出来哪个进程打开了串口设备,或者是否有进程打开串口current->pid的值表示进程号!
ringbuffer 消息队列 内存池 性能优化利器
最近在研究srsLTE的代码,其中就发现一个有意思的数据结构------ringbuffer。
虽然,这是一个很基本的数据结构,但时,它在LTE这种通信协议栈系统中却大行其道,也是很容易被协议开发人员忽略的。在整个通信协议的开发团队中,一般会有一个平台中间件的团队,他们的任务是给业务部门提供高性能、高可靠性的中间件代码,如内存池、线程池、消息通信机制、日志系统等等。这篇文章就来讨论下这个简约而不简单的ringbuffer。
环形缓冲器(ringr buffer),也称作圆形队列(circular queue),循环缓冲区(cyclic buffer),圆形缓冲区(circula buffer),是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流。
在通信程序中,经常使用环形缓冲器作为数据结构来存放通信中发送和接收的数据。环形缓冲区是一个先进先出的循环缓冲区,可以向通信程序提供对缓冲区的互斥访问。
圆形缓冲区的一个有用特性是:当一个数据元素被用掉后,其余数据元素不需要移动其存储位置。相反,一个非圆形缓冲区(例如一个普通的队列)在用掉一个数据元素后,其余数据元素需要向前搬移。换句话说,圆形缓冲区适合实现先进先出缓冲区,而非圆形缓冲区适合后进先出缓冲区。
圆形缓冲区适合于事先明确了缓冲区的最大容量的情形。扩展一个圆形缓冲区的容量,需要搬移其中的数据。因此一个缓冲区如果需要经常调整其容量,用链表实现更为合适。
写操作覆盖圆形缓冲区中未被处理的数据在某些情况下是允许的。特别是在多媒体处理时。例如,音频的生产者可以覆盖掉声卡尚未来得及处理的音频数据。
一般的,圆形缓冲区需要4个指针:
下例为一个满的缓冲区的读写指针:
缓冲区是满、或是空,都有可能出现读指针与写指针指向同一位置,有多种策略用于检测缓冲区是满、或是空。常用的做法是总是保持一个存储单元为空,缓冲区中总是有一个存储单元保持未使用状态。缓冲区最多存入 size-1个数据。如果读写指针指向同一位置,则缓冲区为空。如果写指针位于读指针的相邻后一个位置,则缓冲区为满。这种策略的优点是简单、鲁棒;缺点是语义上实际可存数据量与缓冲区容量不一致,测试缓冲区是否满需要做取余数计算。
出色的KFIFO
kfifo是一种"First In First Out“数据结构,它采用了前面提到的环形缓冲区来实现,提供一个无边界的字节流服务。采用环形缓冲区的好处为,当一个数据元素被用掉后,其余数据元素不需要移动其存储位置,从而减少拷贝提高效率。更重要的是,kfifo采用了并行无锁技术,kfifo实现的单生产/单消费模式的共享队列是不需要加锁同步的。
熟悉Linux内核的读者应该对kfifo.c和kfifo.h并不陌生.kfifo经过简单改进就可以在用户态进行使用,笔者在实际项目中多次使用,经过实践,代码是稳定、可靠、高效的。
相关视频推荐
用户层网络缓冲区设计-ringbuffer、chainbuffer
池式组件为性能飙升提供技术保障-线程池,内存池,异步请求池,数据库连接池,无锁队列的ringbuffer
学习地址:c/c++ linux服务器开发/后台架构师
需要C/C++ Linux服务器架构师学习资料加qun 579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
ringbuffer的一个天生的高性能的消息队列,特别是在单生产/单消费的模式下,它是无锁的,这点非常重要。之前的文章曾介绍过,LTE的协议栈实现对时序是敏感的,这意味着代码的执行不能有阻塞的风险,而线程间的通信几乎是协议栈中必须的基本功能。因此,用ringbuffer去实现一个高性能的消息队列是一种非常理想的方案。当然,由于不同的线程的运行模型不同,例如PDCP线程属于包驱动的线程,大部分时间它是属于阻塞的,当有数据到达,如RRC可以通过消息队列给PDCP发送一个消息,这个时候需要唤醒PDCP进行处理,这个是属于线程同步的技术范畴,可以通过MUTEX、信号量等方案去实现。如果你的系统的Linux(rt-patch),eventfd也是不错的选择,eventfd优势是可以使用poll、select、epoll等操作,这样协议栈的线程实现的方式上较为简洁,关键是eventfd性能也非常的快。
当然这里需要划一个重点,不同线程间需要独立的消息队列,来保证FIFO的无锁特性,当然缺点是会浪费一些内容,但是这在协议栈的开发中往往不是什么大的问题,性能和稳定永远是第一位的。由于FIFO通常是固定大小的数据结构不太适合可变消息的发送,这里的技巧是队列里面只放消息的指针,消息的内容通常是在内存池中申请不同大小的结构。
srsLTE代码的实现PDCP和RLC并不一定是以单独的线程运行的,但是在实际项目中,为了性能的考虑,通常是需要线程化的,且上下行也要线程化,且绑定不同的CPU核,来保证性能。
下图是PDCP和RLC线程的消息队列实例:
内存池
内存池在通信协议栈和很多的软件中都是常用的技术,它的好处是除了可以避免内存碎片,更重要的一点是,内存是预先申请的,并且自我管理,在申请和释放的效率更快,这对协议栈的实现是十分重要的。
内存池的实现在方式都是大同小异的,通常将内存分为8字节、16字节、32字节… 1K等大小不同的内存块,并通过链表的方式进行管理。具体的实现方式可以自行到github上搜索,实现方式都是类似的。
那么,ringbuffer和内存池有什么关系呢?实际上,ringbuffer和内存池的实现并无直接的关系,但是内存池在实现上有个至关重要的问题,那就内存的申请和释放可能不是在同一个线程中。简单的说就是,内存的申请和释放可能存在竞争的情况,通常的做法是进行加锁进行保护。但是加锁的操作可能对协议的时序产生不确定的影响,这对时序要求较高的协议实现(如CMAC)是无法接受的。
ringbuffer的优秀的特性又一次被应用的淋漓尽致,做法也是相当的简单,就是使用ringbuffer单生产/单消费的模式的无锁特性,释放的线程可以将需要释放的地址使用ringbuffer发送给申请的线程,由申请的线程进行内存的释放,这就就不需要加锁的操作,因为同一个线程不会出现并发的链表操作。
下图是结合了消息队列和内存池技术的一次应用,该方案是十分经典和有效的,在很多大型的通信系统中都能看到这种方案的影子:
本文是结合笔者的实际项目经验,介绍了ringbuffer在协议栈软件开发中的一些应用和技巧,主要是ringbuffer单生产/单消费的模式的无锁特性在内存池内存释放和消息队列中的应用技巧。如果读者也有类似的性能方面的系统需求,可以不妨试试 ringbuffer,性能超乎你的想象,且没有特别复杂的算法和CPU指令集的限制。
如何看懂《Linux多线程服务端编程
一:进程和线程
每个进程有自己独立的地址空间。“在同一个进程”还是“不在同一个进程”是系统功能划分的重要决策点。《Erlang程序设计》[ERL]把进程比喻为人:
每个人有自己的记忆(内存),人与人通过谈话(消息传递)来交流,谈话既可以是面谈(同一台服务器),也可以在电话里谈(不同的服务器,有网络通信)。面谈和电话谈的区别在于,面谈可以立即知道对方是否死了(crash,SIGCHLD),而电话谈只能通过周期性的心跳来判断对方是否还活着。
有了这些比喻,设计分布式系统时可以采取“角色扮演”,团队里的几个人各自扮演一个进程,人的角色由进程的代码决定(管登录的、管消息分发的、管买卖的等等)。每个人有自己的记忆,但不知道别人的记忆,要想知道别人的看法,只能通过交谈(暂不考虑共享内存这种IPC)。然后就可以思考:
·容错:万一有人突然死了
·扩容:新人中途加进来
·负载均衡:把甲的活儿挪给乙做
·退休:甲要修复bug,先别派新任务,等他做完手上的事情就把他重启
等等各种场景,十分便利。
线程的特点是共享地址空间,从而可以高效地共享数据。一台机器上的多个进程能高效地共享代码段(操作系统可以映射为同样的物理内存),但不能共享数据。如果多个进程大量共享内存,等于是把多进程程序当成多线程来写,掩耳盗铃。
“多线程”的价值,我认为是为了更好地发挥多核处理器(multi-cores)的效能。在单核时代,多线程没有多大价值(个人想法:如果要完成的任务是CPU密集型的,那多线程没有优势,甚至因为线程切换的开销,多线程反而更慢;如果要完成的任务既有CPU计算,又有磁盘或网络IO,则使用多线程的好处是,当某个线程因为IO而阻塞时,OS可以调度其他线程执行,虽然效率确实要比任务的顺序执行效率要高,然而,这种类型的任务,可以通过单线程的”non-blocking IO+IO multiplexing”的模型(事件驱动)来提高效率,采用多线程的方式,带来的可能仅仅是编程上的简单而已)。Alan Cox说过:”A computer is a state machine.Threads are for people who can’t program state machines.”(计算机是一台状态机。线程是给那些不能编写状态机程序的人准备的)如果只有一块CPU、一个执行单元,那么确实如Alan Cox所说,按状态机的思路去写程序是最高效的。
二:单线程服务器的常用编程模型
据我了解,在高性能的网络程序中,使用得最为广泛的恐怕要数”non-blocking IO+ IO multiplexing”这种模型,即Reactor模式。
在”non-blocking IO+ IO multiplexing”这种模型中,程序的基本结构是一个事件循环(event loop),以事件驱动(event-driven)和事件回调的方式实现业务逻辑:
[cpp] view plain copy
//代码仅为示意,没有完整考虑各种情况
while(!done)
{
int timeout_ms= max(1000, getNextTimedCallback());
int retval= poll(fds, nfds, timeout_ms);
if(retval<0){
处理错误,回调用户的error handler
}else{
处理到期的timers,回调用户的timer handler
if(retval>0){
处理IO事件,回调用户的IO event handler
}
}
}
这里select(2)/poll(2)有伸缩性方面的不足(描述符过多时,效率较低),Linux下可替换为epoll(4),其他操作系统也有对应的高性能替代品。
Reactor模型的优点很明显,编程不难,效率也不错。不仅可以用于读写socket,连接的建立(connect(2)/accept(2)),甚至DNS解析都可以用非阻塞方式进行,以提高并发度和吞吐量(throughput),对于IO密集的应用是个不错的选择。lighttpd就是这样,它内部的fdevent结构十分精妙,值得学习。
基于事件驱动的编程模型也有其本质的缺点,它要求事件回调函数必须是非阻塞的。对于涉及网络IO的请求响应式协议,它容易割裂业务逻辑,使其散布于多个回调函数之中,相对不容易理解和维护。
三:多线程服务器的常用编程模型
大概有这么几种:
a:每个请求创建一个线程,使用阻塞式IO操作。在Java 1.4引人NIO之前,这是Java网络编程的推荐做法。可惜伸缩性不佳(请求太多时,操作系统创建不了这许多线程)。
b:使用线程池,同样使用阻塞式IO操作。与第1种相比,这是提高性能的措施。
c:使用non-blocking IO+ IO multiplexing。即Java NIO的方式。
d:Leader/Follower等高级模式。
在默认情况下,我会使用第3种,即non-blocking IO+ one loop per thread模式来编写多线程C++网络服务程序。
1:one loop per thread
此种模型下,程序里的每个IO线程有一个event loop,用于处理读写和定时事件(无论周期性的还是单次的)。代码框架跟“单线程服务器的常用编程模型”一节中的一样。
libev的作者说:
One loop per thread is usually a good model. Doing this is almost never wrong, some times a better-performance model exists, but it is always a good start.
这种方式的好处是:
a:线程数目基本固定,可以在程序启动的时候设置,不会频繁创建与销毁。
b:可以很方便地在线程间调配负载。
c:IO事件发生的线程是固定的,同一个TCP连接不必考虑事件并发。
Event loop代表了线程的主循环,需要让哪个线程干活,就把timer或IO channel(如TCP连接)注册到哪个线程的loop里即可:对实时性有要求的connection可以单独用一个线程;数据量大的connection可以独占一个线程,并把数据处理任务分摊到另几个计算线程中(用线程池);其他次要的辅助性connections可以共享一个线程。
比如,在dbproxy中,一个线程用于专门处理客户端发来的管理命令;一个线程用于处理客户端发来的MySQL命令,而与后端数据库通信执行该命令时,是将该任务分配给所有事件线程处理的。
对于non-trivial(有一定规模)的服务端程序,一般会采用non-blocking IO+ IO multiplexing,每个connection/acceptor都会注册到某个event loop上,程序里有多个event loop,每个线程至多有一个event loop。
多线程程序对event loop提出了更高的要求,那就是“线程安全”。要允许一个线程往别的线程的loop里塞东西,这个loop必须得是线程安全的。
在dbproxy中,线程向其他线程分发任务,是通过管道和队列实现的。比如主线程accept到连接后,将表示该连接的结构放入队列,并向管道中写入一个字节。计算线程在自己的event loop中注册管道的读事件,一旦有数据可读,就尝试从队列中取任务。
2:线程池
不过,对于没有IO而光有计算任务的线程,使用event loop有点浪费。可以使用一种补充方案,即用blocking queue实现的任务队列:
[cpp] view plain copy
typedef boost::function<void()>Functor;
BlockingQueue<Functor> taskQueue;//线程安全的全局阻塞队列
//计算线程
void workerThread()
{
while(running)//running变量是个全局标志
{
Functor task= taskQueue.take();//this blocks
task();//在产品代码中需要考虑异常处理
}
}
//创建容量(并发数)为N的线程池
int N= num_of_computing_threads;
for(int i= 0; i< N;++i)
{
create_thread(&workerThread);//启动线程
}
//向任务队列中追加任务
Foo foo;//Foo有calc()成员函数
boost::function<void()> task= boost::bind(&Foo::calc,&foo);
taskQueue.post(task);
除了任务队列,还可以用BlockingQueue<T>实现数据的生产者消费者队列,即T是数据类型而非函数对象,queue的消费者从中拿到数据进行处理。其实本质上是一样的。
3:总结
总结而言,我推荐的C++多线程服务端编程模式为:one(event) loop per thread+ thread pool:
event loop用作IO multiplexing,配合non-blockingIO和定时器;
thread pool用来做计算,具体可以是任务队列或生产者消费者队列。
以这种方式写服务器程序,需要一个优质的基于Reactor模式的网络库来支撑,muduo正是这样的网络库。比如dbproxy使用的是libevent。
程序里具体用几个loop、线程池的大小等参数需要根据应用来设定,基本的原则是“阻抗匹配”(解释见下),使得CPU和IO都能高效地运作。所谓阻抗匹配原则:
如果池中线程在执行任务时,密集计算所占的时间比重为 P(0< P<= 1),而系统一共有 C个 CPU,为了让这 C个 CPU跑满而又不过载,线程池大小的经验公式 T= C/P。(T是个 hint,考虑到 P值的估计不是很准确,T的最佳值可以上下浮动 50%)
以后我再讲这个经验公式是怎么来的,先验证边界条件的正确性。
假设 C= 8,P= 1.0,线程池的任务完全是密集计算,那么T= 8。只要 8个活动线程就能让 8个 CPU饱和,再多也没用,因为 CPU资源已经耗光了。
假设 C= 8,P= 0.5,线程池的任务有一半是计算,有一半等在 IO上,那么T= 16。考虑操作系统能灵活合理地调度 sleeping/writing/running线程,那么大概 16个“50%繁忙的线程”能让 8个 CPU忙个不停。启动更多的线程并不能提高吞吐量,反而因为增加上下文切换的开销而降低性能。
如果 P< 0.2,这个公式就不适用了,T可以取一个固定值,比如 5*C。
另外,公式里的 C不一定是 CPU总数,可以是“分配给这项任务的 CPU数目”,比如在 8核机器上分出 4个核来做一项任务,那么 C=4。
四:进程间通信只用TCP
Linux下进程间通信的方式有:匿名管道(pipe)、具名管道(FIFO)、POSIX消息队列、共享内存、信号(signals),以及Socket。同步原语有互斥器(mutex)、条件变量(condition variable)、读写锁(reader-writer lock)、文件锁(record locking)、信号量(semaphore)等等。
进程间通信我首选Sockets(主要指TCP,我没有用过UDP,也不考虑Unix domain协议)。其好处在于:
可以跨主机,具有伸缩性。反正都是多进程了,如果一台机器的处理能力不够,很自然地就能用多台机器来处理。把进程分散到同一局域网的多台机器上,程序改改host:port配置就能继续用;
TCP sockets和pipe都是操作文件描述符,用来收发字节流,都可以read/write/fcntl/select/poll等。不同的是,TCP是双向的,Linux的pipe是单向的,进程间双向通信还得开两个文件描述符,不方便;而且进程要有父子关系才能用pipe,这些都限制了pipe的使用;
TCP port由一个进程独占,且进程退出时操作系统会自动回收文件描述符。因此即使程序意外退出,也不会给系统留下垃圾,程序重启之后能比较容易地恢复,而不需要重启操作系统(用跨进程的mutex就有这个风险);而且,port是独占的,可以防止程序重复启动,后面那个进程抢不到port,自然就没法初始化了,避免造成意料之外的结果;
与其他IPC相比,TCP协议的一个天生的好处是“可记录、可重现”。tcpdump和Wireshark是解决两个进程间协议和状态争端的好帮手,也是性能(吞吐量、延迟)分析的利器。我们可以借此编写分布式程序的自动化回归测试。也可以用tcpcopy之类的工具进行压力测试。TCP还能跨语言,服务端和客户端不必使用同一种语言。
分布式系统的软件设计和功能划分一般应该以“进程”为单位。从宏观上看,一个分布式系统是由运行在多台机器上的多个进程组成的,进程之间采用TCP长连接通信。
使用TCP长连接的好处有两点:一是容易定位分布式系统中的服务之间的依赖关系。只要在机器上运行netstat-tpna|grep<port>就能立刻列出用到某服务的客户端地址(Foreign Address列),然后在客户端的机器上用netstat或lsof命令找出是哪个进程发起的连接。TCP短连接和UDP则不具备这一特性。二是通过接收和发送队列的长度也较容易定位网络或程序故障。在正常运行的时候,netstat打印的Recv-Q和Send-Q都应该接近0,或者在0附近摆动。如果Recv-Q保持不变或持续增加,则通常意味着服务进程的处理速度变慢,可能发生了死锁或阻塞。如果Send-Q保持不变或持续增加,有可能是对方服务器太忙、来不及处理,也有可能是网络中间某个路由器或交换机故障造成丢包,甚至对方服务器掉线,这些因素都可能表现为数据发送不出去。通过持续监控Recv-Q和Send-Q就能及早预警性能或可用性故障。以下是服务端线程阻塞造成Recv-Q和客户端Send-Q激增的例子:
[cpp] view plain copy
$netstat-tn
Proto Recv-Q Send-Q Local Address Foreign
tcp 78393 0 10.0.0.10:2000 10.0.0.10:39748#服务端连接
tcp 0 132608 10.0.0.10:39748 10.0.0.10:2000#客户端连接
tcp 0 52 10.0.0.10:22 10.0.0.4:55572
五:多线程服务器的适用场合
如果要在一台多核机器上提供一种服务或执行一个任务,可用的模式有:
a:运行一个单线程的进程;
b:运行一个多线程的进程;
c:运行多个单线程的进程;
d:运行多个多线程的进程;
考虑这样的场景:如果使用速率为50MB/s的数据压缩库,进程创建销毁的开销是800微秒,线程创建销毁的开销是50微秒。如何执行压缩任务?
如果要偶尔压缩1GB的文本文件,预计运行时间是20s,那么起一个进程去做是合理的,因为进程启动和销毁的开销远远小于实际任务的耗时。
如果要经常压缩500kB的文本数据,预计运行时间是10ms,那么每次都起进程似乎有点浪费了,可以每次单独起一个线程去做。
如果要频繁压缩10kB的文本数据,预计运行时间是200微秒,那么每次起线程似乎也很浪费,不如直接在当前线程搞定。也可以用一个线程池,每次把压缩任务交给线程池,避免阻塞当前线程(特别要避免阻塞IO线程)。
由此可见,多线程并不是万灵丹(silver bullet)。
1:必须使用单线程的场合
据我所知,有两种场合必须使用单线程:
a:程序可能会fork(2);
实际编程中,应该保证只有单线程程序能进行fork(2)。多线程程序不是不能调用fork(2),而是这么做会遇到很多麻烦:
fork一般不能在多线程程序中调用,因为Linux的fork只克隆当前线程的thread of control,不可隆其他线程。fork之后,除了当前线程之外,其他线程都消失了。
这就造成一种危险的局面。其他线程可能正好处于临界区之内,持有了某个锁,而它突然死亡,再也没有机会去解锁了。此时如果子进程试图再对同一个mutex加锁,就会立即死锁。因此,fork之后,子进程就相当于处于signal handler之中(因为不知道调用fork时,父进程中的线程此时正在调用什么函数,这和信号发生时的场景一样),你不能调用线程安全的函数(除非它是可重入的),而只能调用异步信号安全的函数。比如,fork之后,子进程不能调用:
malloc,因为malloc在访问全局状态时几乎肯定会加锁;
任何可能分配或释放内存的函数,比如snprintf;
任何Pthreads函数;
printf系列函数,因为其他线程可能恰好持有stdout/stderr的锁;
除了man 7 signal中明确列出的信号安全函数之外的任何函数。
因此,多线程中调用fork,唯一安全的做法是fork之后,立即调用exec执行另一个程序,彻底隔断子进程与父进程的联系。
在多线程环境中调用fork,产生子进程后。子进程内部只存在一个线程,也就是父进程中调用fork的线程的副本。
使用fork创建子进程时,子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁和条件变量的状态。如果父进程中的某个线程占有锁,则子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁,并且需要释放哪些锁。
尽管Pthread提供了pthread_atfork函数试图绕过这样的问题,但是这回使得代码变得混乱。因此《Programming With Posix Threads》一书的作者说:”Avoid using fork in threaded code except where the child process will immediately exec a new program.”。
b:限制程序的CPU占用率;
这个很容易理解,比如在一个8核的服务器上,一个单线程程序即便发生busy-wait,占满1个core,其CPU使用率也只有12.5%,在这种最坏的情况下,系统还是有87.5%的计算资源可供其他服务进程使用。
因此对于一些辅助性的程序,如果它必须和主要服务进程运行在同一台机器的话,那么做成单线程的能避免过分抢夺系统的计算资源。