一:进程和线程
每个进程有自己独立的地址空间。“在同一个进程”还是“不在同一个进程”是系统功能划分的重要决策点。《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
//代码仅为示意,没有完整考虑各种情况
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
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模式的网络库来支撑,muo正是这样的网络库。比如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协议的一个天生的好处是“可记录、可重现”。tcpmp和Wireshark是解决两个进程间协议和状态争端的好帮手,也是性能(吞吐量、延迟)分析的利器。我们可以借此编写分布式程序的自动化回归测试。也可以用tcp之类的工具进行压力测试。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
$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%的计算资源可供其他服务进程使用。
因此对于一些辅助性的程序,如果它必须和主要服务进程运行在同一台机器的话,那么做成单线程的能避免过分抢夺系统的计算资源。
B. 《Linux高性能服务器编程》pdf下载在线阅读全文,求百度网盘云资源
《Linux高性能服务器编程》(游双)电子书网盘下载免费在线阅读
链接:
书名:Linux高性能服务器编程
作者:游双
豆瓣评分:7.9
出版社:机械工业出版社
出版年份:2013-5-1
页数:360
内容简介:
本书是Linux服务器编程领域的经典着作,由资深Linux软件开发工程师撰写,从网络协议、服务器编程核心要素、原理机制、工具框架等多角度全面阐释了编写高性能Linux服务器应用的方法、技巧和思想。不仅理论全面、深入,抓住了重点和难点,还包含两个综合性案例,极具实战意义。
全书共17章,分为3个部分:第一部分对Linux服务器编程的核心基础——TCP/IP协议进行了深入的解读和阐述,包括TCP/IP协议族、TCP/IP协议,以及一个经典的TCP/IP通信案例;第二部分对高性能服务器编程的核心要素进行了全面深入的剖析,包含Linux网络编程API、高级I/O函数、Linux服务器程序规范、高性能服务器程序框架、I/O复用、信号、定时器、高性能I/O框架库Libevent、多进程编程、多线程编程、进程池和线程池等内容,原理、技术与方法并重;第三部分从侧重实战的角度讲解了高性能服务器的优化与监测,包含服务器的调制、调试和测试,以及各种实用系统监测工具的使用等内容。
作者简介:
游双,资深Linux软件开发工程师,对Linux网络编程,尤其是服务器端的编程,有非常深入的研究,实战经验也十分丰富。曾就职于摩托罗拉,担任高级Linux软件工程师。此外,他还精通C++、Android、QT等相关的技术。活跃于Chinaunix等专业技术社区,发表了大量关于Linux网络编程的文章,深受社区欢迎。
C. 有学linux的书籍推荐吗
D. 本人面试的javaweb,这是在做linux运维吗
可能公司的javaweb项目今后是要放在liunx系统服务器中的,也就是在此之前你需要学会liunx的基本应用,而且现在liunx系统在服务器一块应用很广泛,你看看你们公司是否有开发javaee的,如岗位果有的话你今后应该会被调到开发javaee中,如果没有或者人很少且不缺人的话,你可能较长一段时间都要在做liunx维护。
E. linux下服务器程序的几种基本模型
我总结下来有这么几种:
单进程提供服务
多进程提供服务
多进程池服务(prefork)
io复用提供服务(select,poll)
epoll(其实也是一种IO复用)
多线程提供服务
多线程池提供服务
信号驱动提供服务
F. linux多线程服务端编程 看什么书
这本书主要分享了作者在实现公司内部的分布式服务系统中积累的多线程和网络编程方面的经验,并介绍了C++ 在编写这种分布式系统的服务端程序时的功能取舍与注意事项,书中的很多决策(design decision)是在这一应用场景下做出的。
这本书没有细谈分布式系统的设计,只在第9章列举了分布式系统的挑战及其对程序设计(服务端编程)的影响,例如可靠性、可维护性等。
G. 想接触C++多线程编程,需要从哪方面入手,有没有
多线程编程的难点不在于锁,正常人看一下操作系统再写几个线程demo就可以基本理解了。对于C++而言,甚至连编写线程安全的类也不是难事。只需要用同步原语来保持对共享资源的访问即可。
我个人觉得最需要的就是实战,写Demo谁都会写。同步原语就那么几个,信号量,互斥量,条件变量等。但是怎么用呢?当你从点击星际争霸到和玩家匹配进行游戏,这当中程序是怎么运行的?
事件驱动是怎么驱动的?
就目前来说,我遇到的困难不是线程的死锁,而是对并发模型的理解。Actor,Reactor模式等。这些东西不实战,个人空想理解起来会吃力。
推荐《Linux多线程服务端编程》,这本书给我的观点是实战性很强,而且涉及面也比较广。后几章提到了分布式系统和作者对C++的思考以及STL algotrithm的运用。如作者所说:“对于面向对象,封装式必须的;但继承和多态耦合性太强,很不划算”我就很赞同
同时展示了一个用C++开发的网络库,不过虽然看了这本书,我还是没找到为什么要用C++的理由。我认为C的确可以很好地解决问题。C++的话就RAII算是真的有益处。
但读之前你需要有一定的C++和操作系统基础。当时买这本书的时候还觉得有点心疼,现在看看物超所值。(我那本CSAPP就翻了一章=-=)
总结:看现代操作系统第二章,同时结合C++11的thread库写经典Demo(生产者消费者问题等)
花两周左右。剩下的就是实战。如果不实战,你还是不知道这些东西在生产环境中是怎么使用的。
可以结合muo skynet等开源网络框架学习并发模型。
H. 陈硕 linux 多线程服务端编程 怎样生成可执行文件
进程和线程 每个进程有自己独立的地址空间。“在同一个进程”还是“不在同一个进程”是系统功能划分的重要决策点。《Erlang程序设计》[ERL]把进程比喻为人: 每个人有自己的记忆(内存),人与人通过谈话(消息传递)来交流
I. linux 服务器程序接受请求,是单线程好,还是多线程好
看处理逻辑。如果一个处理逻辑申请大量资源,建议多进程。
如果是轻量级别,建议多线程。
J. 如何看懂《Linux多线程服务端编程
:进程线程 每进程自独立址空间同进程同进程系统功能划重要决策点《Erlang程序设计》[ERL]进程比喻: 每自记忆(内存)与通谈(消息传递)交流谈既面谈(同台服务器)电谈(同服务器中国络通信)面谈电谈区别于面谈立即知道否死(crash,SIGCHLD)电谈能通周期性跳判断否着 些比喻设计布式系统采取角色扮演团队几各自扮演进程角色由进程代码决定(管登录、管消息发、管买卖等等)每自记忆知道别记忆要想知道别看能通交谈(暂考虑共享内存种IPC)思考: ·容错:万突死 ·扩容:新途加进 ·负载均衡:甲挪给乙做 ·退休:甲要修复bug先别派新任务等做完手事情重启 等等各种场景十便利 线程特点共享址空间高效共享数据台机器进程能高效共享代码段(操作系统映射同物理内存)能共享数据进程量共享内存等于进程程序线程写掩耳盗铃 线程价值我认更发挥核处理器(multi-cores)效能单核代线程没价值(想:要完任务CPU密集型线程没优势甚至线程切换销线程反更慢;要完任务既CPU计算磁盘或中国络IO则使用线程处某线程IO阻塞OS调度其线程执行虽效率确实要比任务顺序执行效率要高种类型任务通单线程non-blocking IO+IO multiplexing模型(事件驱)提高效率采用线程式带能仅仅编程简单已)Alan Cox说:A 中国puter 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 //代码仅示意没完整考虑各种情况 while(!done) { int timeout_ms = max(1000, getNextTimedCallback()); int retval = poll(fds, nfds, timeout_ms); if (retval0){ 处理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 typedef boost::functionFunctor; BlockingQueue taskQueue; //线程安全全局阻塞队列 //计算线程 void workerThread() { while (running) //running变量全局标志 { Functor task = taskQueue.take(); //this blocks task(); //产品代码需要考虑异处理 } } // 创建容量(并发数)N线程池 int N = num_of_中国puting_threads; for (int i = 0; i < N; ++i) { create_thread(&workerThread); //启线程 } //向任务队列追加任务 Foo foo; //Foocalc()员函数 boost::function task = boost::bind(&Foo::calc&foo); taskQueue.post(task); 除任务队列用BlockingQueue实现数据产者消费者队列即T数据类型非函数象queue消费者拿数据进行处理其实本质 3:总结 总结言我推荐C++线程服务端编程模式:one (event) loop per thread + thread pool: event loop用作IO multiplexing配合non-blockingIO定器; thread pool用做计算具体任务队列或产者消费者队列 种式写服务器程序需要优质基于Reactor模式中国络库支撑muo中国络库比dbproxy使用libevent 程序具体用几loop、线程池等参数需要根据应用设定基本原则阻抗匹配(解释见)使CPUIO都能高效运作所谓阻抗匹配原则: 池线程执行任务密集计算所占间比重 P (0 < P <= 1)系统共 C CPU让 C CPU 跑满载线程池经验公式 T = C/P(T hint考虑 P 值估计准确T 佳值浮 50%) 我再讲经验公式先验证边界条件确性 假设 C = 8P = 1.0线程池任务完全密集计算T = 8要 8 线程能让 8 CPU 饱再没用 CPU 资源已经耗光 假设 C = 8P = 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 socketspipe都操作文件描述符用收发字节流都read/write/fcntl/select/poll等同TCP双向Linuxpipe单向进程间双向通信两文件描述符便;且进程要父关系才能用pipe些都限制pipe使用; TCP port由进程独占且进程退操作系统自收文件描述符即使程序意外退给系统留垃圾程序重启能比较容易恢复需要重启操作系统(用跨进程mutex风险);且port独占防止程序重复启面进程抢port自没初始化避免造意料外结; 与其IPC相比TCP协议处记录、重现tcpmpWireshark解决两进程间协议状态争端帮手性能(吞吐量、延迟)析利器我借编写布式程序自化归测试用tcp类工具进行压力测试TCP能跨语言服务端客户端必使用同种语言 布式系统软件设计功能划般应该进程单位宏观看布式系统由运行台机器进程组进程间采用TCP连接通信 使用TCP连接处两点:容易定位布式系统服务间依赖关系要机器运行netstat -tpna|grep 能立刻列用某服务客户端址(Foreign Address列)客户端机器用netstat或lsof命令找哪进程发起连接TCP短连接UDP则具备特性二通接收发送队列度较容易定位中国络或程序故障运行候netstat打印Recv-QSend-Q都应该接近0或者0附近摆Recv-Q保持变或持续增加则通意味着服务进程处理速度变慢能发死锁或阻塞Send-Q保持变或持续增加能服务器太忙、及处理能中国络间某路由器或交换机故障造丢包甚至服务器掉线些素都能表现数据发送通持续监控Recv-QSend-Q能及早预警性能或用性故障服务端线程阻塞造Recv-Q客户端Send-Q激增例: [cpp] view plain $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般能线程程序调用Linuxfork克隆前线程thread of control隆其线程fork除前线程外其线程都消失 造种危险局面其线程能处于临界区内持某锁突死亡再没机解锁进程试图再同mutex加锁立即死锁fork进程相于处于signal handler(知道调用fork父进程线程调用函数信号发场景)能调用线程安全函数(除非重入)能调用异步信号安全函数比fork进程能调用: mallocmalloc访问全局状态几乎肯定加锁; 任何能配或释放内存函数比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占满1core其CPU使用率12.5%种坏情况系统87.5%计算资源供其服务进程使用 于些辅助性程序必须主要服务进程运行同台机器做单线程能避免抢夺系统计算资