- 浏览: 159621 次
- 性别:
- 来自: 北京
文章分类
最新评论
-
西巴拉古呀那:
Linux多线程并发服务器编程(线程池,FTP服务器)网盘地址 ...
多线程服务器的适用场合 -
somefuture:
第四题怎麼用位图法。写了半天没出来,用了下面的,速度很快pri ...
位图法应用 -
寂寞秋江:
系统,全面,条理清晰,由浅入深,简直是集大成之作。 特别是最后 ...
单个进程最大线程数 -
darkjune:
有点意思, 不错
单个进程最大线程数 -
kidding87:
谢谢啦,收藏着
google使用一点敲门分享
转自:
陈硕 (giantchen_AT_gmail) :http://blog.csdn.net/solstice/article/details/5343217
《多线程服务器的适用场合》(以下简称《适用场合》)一文在博客登出之后,有热心读者提出质疑,我自己也觉得原文没有把道理说通说透,这篇文章试图用一些实例来解答读者的疑问。我本来打算修改原文,但是考虑到已经读过的读者不一定会注意到文章的变动,干脆另写一篇。为方便阅读,本文以问答体呈现。这篇文章可能会反复修改扩充,请注意上面的版本号。
本文所说的“多线程服务器”的定义与前文一样,同时参见《多线程服务器的常用编程模型》(以下简称《常用模型》)一文的详细界定,以下“连接、端口”均指 TCP 协议。
1. Linux 能同时启动多少个线程?
对于 32-bit Linux,一个进程的地址空间是 4G,其中用户态能访问 3G 左右,而一个线程的默认栈 (stack) 大小是 10M,心算可知,一个进程大约最多能同时启动 300 个线程。如果不改线程的调用栈大小的话,300 左右是上限,因为程序的其他部分(数据段、代码段、堆、动态库、等等)同样要占用内存(地址空间)。
对于 64-bit 系统,线程数目可大大增加,具体数字我没有测试,因为我实际用不到那么多线程。
以下的关于线程数目的讨论以 32-bit Linux 为例。
2. 多线程能提高并发度吗?
如果指的是“并发连接数”,不能。
由问题 1 可知,假如单纯采用 thread per connection 的模型,那么并发连接数最多 300,这远远低于基于事件的单线程程序所能轻松达到的并发连接数(几千上万,甚至几万)。所谓“基于事件”,指的是用 IO multiplexing event loop 的编程模型,又称 Reactor 模式,在《常用模型》一文中已有介绍。
那么采用《常用模型》一文中推荐的 event loop per thread 呢?至少不逊于单线程程序。
小结:thread per connection 不适合高并发场合,其 scalability 不佳。event loop per thread 的并发度不比单线程程序差。
3. 多线程能提高吞吐量吗?
对于计算密集型服务,不能。
假设有一个耗时的计算服务,用单线程算需要 0.8s。在一台 8 核的机器上,我们可以启动 8 个线程一起对外服务(如果内存够用,启动 8 个进程也一样)。这样完成单个计算仍然要 0.8s,但是由于这些进程的计算可以同时进行,理想情况下吞吐量可以从单线程的 1.25cps (calc per second) 上升到 10cps。(实际情况可能要打个八折——如果不是打对折的话。)
假如改用并行算法,用 8 个核一起算,理论上如果完全并行,加速比高达 8,那么计算时间是 0.1s,吞吐量还是 10cps,但是首次请求的响应时间却降低了很多。实际上根据 Amdahl's law,即便算法的并行度高达 95%,8 核的加速比也只有 6,计算时间为 0.133s,这样会造成吞吐量下降为 7.5cps。不过以此为代价,换得响应时间的提升,在有些应用场合也是值得的。
这也回答了问题 4。
如果用 thread per request 的模型,每个客户请求用一个线程去处理,那么当并发请求数大于某个临界值 T’ 时,吞吐量反而会下降,因为线程多了以后上下文切换的开销也随之增加(分析与数据请见《A Design Framework for Highly Concurrent Systems》 by Matt Welsh et al.)。thread per request 是最简单的使用线程的方式,编程最容易,简单地把多线程程序当成一堆串行程序,用同步的方式顺序编程,比如 Java Servlet 中,一次页面请求由一个函数 HttpServlet#service(HttpServletRequest req, HttpServletResponse resp) 同步地完成。
为了在并发请求数很高时也能保持稳定的吞吐量,我们可以用线程池,线程池的大小应该满足“阻抗匹配原则”,见问题 7。
线程池也不是万能的,如果响应一次请求需要做比较多的计算(比如计算的时间占整个 response time 的 1/5 强),那么用线程池是合理的,能简化编程。如果一次请求响应中,thread 主要是在等待 IO,那么为了进一步提高吞吐,往往要用其它编程模型,比如 Proactor,见问题 8。
4. 多线程能降低响应时间吗?
如果设计合理,充分利用多核资源的话,可以。在突发 (burst) 请求时效果尤为明显。
例1: 多线程处理输入。
以 memcached 服务端为例。memcached 一次请求响应大概可以分为 3 步:
- 读取并解析客户端输入
- 操作 hashtable
- 返回客户端
在单线程模式下,这 3 步是串行执行的。在启用多线程模式时,它会启用多个输入线程(默认是 4 个),并在建立连接时按 round-robin 法把新连接分派给其中一个输入线程,这正好是我说的 event loop per thread 模型。这样一来,第 1 步的操作就能多线程并行,在多核机器上提高多用户的响应速度。第 2 步用了全局锁,还是单线程的,这可算是一个值得继续改进的地方。
比如,有两个用户同时发出了请求,这两个用户的连接正好分配在两个 IO 线程上,那么两个请求的第 1 步操作可以在两个线程上并行执行,然后汇总到第 2 步串行执行,这样总的响应时间比完全串行执行要短一些(在“读取并解析”所占的比重较大的时候,效果更为明显)。请继续看下面这个例子。
例2: 多线程分担负载。
假设我们要做一个求解 Sudoku 的服务(见《谈谈数独》),这个服务程序在 9981 端口接受请求,输入为一行 81 个数字(待填数字用 0 表示),输出为填好之后的 81 个数字 (1 ~ 9),如果无解,输出 “NO/r/n”。
由于输入格式很简单,用单个线程做 IO 就行了。先假设每次求解的计算用时 10ms,用前面的方法计算,单线程程序能达到的吞吐量上限为 100req/s,在 8 核机器上,如果用线程池来做计算,能达到的吞吐量上限为 800req/s。下面我们看看多线程如何降低响应时间。
假设 1 个用户在极短的时间内发出了 10 个请求,如果用单线程“来一个处理一个”的模型,这些 reqs 会排在队列里依次处理(这个队列是操作系统的 TCP 缓冲区,不是程序里自己的任务队列)。在不考虑网络延迟的情况下,第 1 个请求的响应时间是 10ms;第 2 个请求要等第 1 个算完了才能获得 CPU 资源,它等了 10ms,算了 10ms,响应时间是 20ms;依次类推,第 10 个请求的响应时间为 100ms;10个请求的平均响应时间为 55ms。
如果 Sudoku 服务在每个请求到达时开始计时,会发现每个请求都是 10ms 响应时间,而从用户的观点,10 个请求的平均响应时间为 55ms,请读者想想为什么会有这个差异。
下面改用多线程:1 个 IO 线程,8 个计算线程(线程池)。二者之间用 BlockingQueue 沟通。同样是 10 个并发请求,第 1 个请求被分配到计算线程1,第 2 个请求被分配到计算线程 2,以此类推,直到第 8 个请求被第 8 个计算线程承担。第 9 和第 10 号请求会等在 BlockingQueue 里,直到有计算线程回到空闲状态才能被处理。(请注意,这里的分配实际上是由操作系统来做,操作系统会从处于 Waiting 状态的线程里挑一个,不一定是 round-robin 的。)
这样一来,前 8 个请求的响应时间差不多都是 10ms,后 2 个请求属于第二批,其响应时间大约会是 20ms,总的平均响应时间是 12ms。可以看出比单线程快了不少。
由于每道 Sudoku 题目的难度不一,对于简单的题目,可能 1ms 就能算出来,复杂的题目最多用 10ms。那么线程池方案的优势就更明显,它能有效地降低“简单任务被复杂任务压住”的出现概率。
以上举的都是计算密集的例子,即线程在响应一次请求时不会等待 IO,下面谈谈更复杂的情况。
5. 多线程程序如何让 IO 和“计算”相互重叠,降低 latency?
基本思路是,把 IO 操作(通常是写操作)通过 BlockingQueue 交给别的线程去做,自己不必等待。
例1: logging
在多线程服务器程序中,日志 (logging) 至关重要,本例仅考虑写 log file 的情况,不考虑 log server。
在一次请求响应中,可能要写多条日志消息,而如果用同步的方式写文件(fprintf 或 fwrite),多半会降低性能,因为:
- 文件操作一般比较慢,服务线程会等在 IO 上,让 CPU 闲置,增加响应时间。
- 就算有 buffer,还是不灵。多个线程一起写,为了不至于把 buffer 写错乱,往往要加锁。这会让服务线程互相等待,降低并发度。(同时用多个 log 文件不是办法,除非你有多个磁盘,且保证 log files 分散在不同的磁盘上,否则还是受到磁盘 IO 瓶颈制约。)
解决办法是单独用一个 logging 线程,负责写磁盘文件,通过一个或多个 BlockingQueue 对外提供接口。别的线程要写日志的时候,先把消息(字符串)准备好,然后往 queue 里一塞就行,基本不用等待。这样服务线程的计算就和 logging 线程的磁盘 IO 相互重叠,降低了服务线程的响应时间。
尽管 logging 很重要,但它不是程序的主要逻辑,因此对程序的结构影响越小越好,最好能简单到如同一条 printf 语句,且不用担心其他性能开销,而一个好的多线程异步 logging 库能帮我们做到这一点。(Apache 的 log4cxx 和 log4j 都支持 AsyncAppender 这种异步 logging 方式。)
例2: memcached 客户端
假设我们用 memcached 来保存用户最后发帖的时间,那么每次响应用户发帖的请求时,程序里要去设置一下 memcached 里的值。这一步如果用同步 IO,会增加延迟。
对于“设置一个值”这样的 write-only idempotent 操作,我们其实不用等 memcached 返回操作结果,这里也不用在乎 set 操作失败,那么可以借助多线程来降低响应延迟。比方说我们可以写一个多线程版的 memcached 的客户端,对于 set 操作,调用方只要把 key 和 value 准备好,调用一下 asyncSet() 函数,把数据往 BlockingQueue 上一放就能立即返回,延迟很小。剩下的时就留给 memcached 客户端的线程去操心,而服务线程不受阻碍。
其实所有的网络写操作都可以这么异步地做,不过这也有一个缺点,那就是每次 asyncWrite 都要在线程间传递数据,其实如果 TCP 缓冲区是空的,我们可以在本线程写完,不用劳烦专门的 IO 线程。Jboss 的 Netty 就使用了这个办法来进一步降低延迟。
以上都仅讨论了“打一枪就跑”的情况,如果是一问一答,比如从 memcached 取一个值,那么“重叠 IO”并不能降低响应时间,因为你无论如何要等 memcached 的回复。这时我们可以用别的方式来提高并发度,见问题8。(虽然不能降低响应时间,但也不要浪费线程在空等上,对吧)
另外以上的例子也说明,BlockingQueue 是构建多线程程序的利器。
6. 为什么第三方库往往要用自己的线程?
往往因为 event loop 模型没有标准实现。如果自己写代码,尽可以按所用 Reactor 的推荐方式来编程,但是第三方库不一定能很好地适应并融入这个 event loop framework。有时需要用线程来做一些串并转换。
对于 Java,这个问题还好办一些,因为 thread pool 在 Java 里有标准实现,叫 ExecutorService。如果第三方库支持线程池,那么它可以和主程序共享一个 ExecutorService ,而不是自己创建一堆线程。(比如在初始化时传入主程序的 obj。)对于 C++,情况麻烦得多,Reactor 和 Thread pool 都没有标准库。
例1:libmemcached 只支持同步操作
libmemcached 支持所谓的“非阻塞操作”,但没有暴露一个能被 select/poll/epoll 的 file describer,它的 memcached_fetch 始终会阻塞。它号称 memcached_set 可以是非阻塞的,实际意思是不必等待结果返回,但实际上这个函数会同步地调用 write(),仍可能阻塞在网络 IO 上。
如果在我们的 reactor event handler 里调用了 libmemcached 的函数,那么 latency 就堪忧了。如果想继续用 libmemcached,我们可以为它做一次线程封装,按问题 5 例 2 的办法,同额外的线程专门做 memcached 的 IO,而程序主体还是 reactor。甚至可以把 memcached “数据就绪”作为一个 event,注入到我们的 event loop 中,以进一步提高并发度。(例子留待问题 8 讲)
万幸的是,memcached 的协议非常简单,大不了可以自己写一个基于 reactor 的客户端,但是数据库客户端就没那么幸运了。
例2:MySQL 的官方 C API 不支持异步操作
MySQL 的客户端只支持同步操作,对于 UPDATE/INSERT/DELETE 之类只要行为不管结果的操作(如果代码需要得知其执行结果则另当别论),我们可以用一个单独的线程来做,以降低服务线程的延迟。可仿照前面 memcached_set 的例子,不再赘言。麻烦的是 SELECT,如果要把它也异步化,就得动用更复杂的模式了,见问题 8。
相比之下,PostgreSQL 的 C 客户端 libpq 的设计要好得多,我们可以用 PQsendQuery() 来发起一次查询,然后用标准的 select/poll/epoll 来等待 PQsocket,如果有数据可读,那么用 PQconsumeInput 处理之,并用 PQisBusy 判断查询结果是否已就绪,最后用 PQgetResult 来获取结果。借助这套异步 API,我们可以很容易地为 libpq 写一套 wrapper,使之融入到程序所用的 reactor 模型中。
7. 什么是线程池大小的阻抗匹配原则?
我在《常用模型》中提到“阻抗匹配原则”,这里大致讲一讲。
如果池中线程在执行任务时,密集计算所占的时间比重为 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。
8. 除了你推荐的 reactor + thread poll,还有别的 non-trivial 多线程编程模型吗?
有,Proactor。
如果一次请求响应中要和别的进程打多次交道,那么 proactor 模型往往能做到更高的并发度。当然,代价是代码变得支离破碎,难以理解。
这里举 http proxy 为例,一次 http proxy 的请求如果没有命中本地 cache,那么它多半会:
- 解析域名 (不要小看这一步,对于一个陌生的域名,解析可能要花半秒钟)
- 建立连接
- 发送 HTTP 请求
- 等待对方回应
- 把结果返回客户
这 5 步里边跟 2 个 server 发生了 3 次 round-trip:
- 向 DNS 问域名,等待回复;
- 向对方 http 服务器发起连接,等待 TCP 三路握手完成;
- 向对方发送 http request,等待对方 response。
而实际上 http proxy 本身的运算量不大,如果用线程池,池中线程的数目会很庞大,不利于操作系统管理调度。
这时我们有两个解决思路:
- 把“域名已解析”,“连接已建立”,“对方已完成响应”做成 event,继续按照 Reactor 的方式来编程。这样一来,每次客户请求就不能用一个函数从头到尾执行完成,而要分成多个阶段,并且要管理好请求的状态(“目前到了第几步?”)。
- 用回调函数,让系统来把任务串起来。比如收到用户请求,如果没有命中本地 cache,立刻发起异步的 DNS 解析 startDNSResolve(),告诉系统在解析完之后调用 DNSResolved() 函数;在 DNSResolved() 中,发起连接,告诉系统在连接建立之后调用 connectionEstablished();在 connectionEstablished() 中发送 http request,告诉系统在收到响应之后调用 httpResponsed();最后,在 httpResponsed() 里把结果返回给客户。.NET 大量采用的 Begin/End 操作也是这个编程模式。当然,对于不熟悉这种编程方式的人,代码会显得很难看。Proactor 模式的例子可看 boost::asio 的文档,这里不再多说。
Proactor 模式依赖操作系统或库来高效地调度这些子任务,每个子任务都不会阻塞,因此能用比较少的线程达到很高的 IO 并发度。
Proactor 能提高吞吐,但不能降低延迟,所以我没有深入研究。
9. 模式 2 和模式 3a 该如何取舍?
这里的“模式”不是 pattern,而是 model,不巧它们的中译是一样的。《适用场合》中提到,模式 2 是一个多线程的进程,模式 3a 是多个相同的单线程进程。
我认为,在其他条件相同的情况下,可以根据工作集 (work set) 的大小来取舍。工作集是指服务程序响应一次请求所访问的内存大小。
如果工作集较大,那么就用多线程,避免 CPU cache 换入换出,影响性能;否则,就用单线程多进程,享受单线程编程的便利。
例如,memcached 这个内存消耗大户用多线程服务端就比在同一台机器上运行多个 memcached instance 要好。(除非你在 16G 内存的机器上运行 32-bit memcached,那么多 instance 是必须的。)
又例如,求解 Sudoku 用不了多大内存,如果单线程编程更方便的话,可以用单线程多进程来做。再在前面加一个单线程的 load balancer,仿 lighttpd + fastcgi 的成例。
线程不能减少工作量,即不能减少 CPU 时间。如果解决一个问题需要执行一亿条指令(这个数字不大,不要被吓到),那么用多线程只会让这个数字增加。但是通过合理调配这一亿条指令在多个核上的执行情况,我们能让工期提早结束。这听上去像统筹方法,确实也正是统筹方法。
请注意,我一般不在 CSDN 博客的评论跟帖中回复匿名用户的提问,如果希望我解答疑惑,请:1. 给我写信,2. 在 twitter 上 follow @bnu_chenshuo,3. 登陆以后评论。 如有不便,还请见谅。
如果一个memcached仅用于某个单线程程序作本地缓存(无需网络访问和进程间共享),它有什么别的优势么?是否还不如直接用内存缓存更高效呢,例如在get数据方面?(假设内存没有限制,例如在64位系统上)。
对于一个高并发比如同时可能 15000个连接数 甚至更多(周末更严重),采用 reactor 模式,每个连接进入events loop 之后,业务逻辑处理需要多次和客户端交互(read and write socket ),且逻辑处理(很难再拆分成多个逻辑处理线程,串行处理)需要很长时间,比如每3个event 需要 1s钟完成 ,这样算下来系统延迟也很糟糕,这种情况有什么好方法处理呢? 是不是不适合在线处理了
“立刻发起异步的 DNS 解析 startDNSResolve(),告诉系统在解析完之后调用 DNSResolved() 函数;”
我一般用gethostbyname做域名解析
发表评论
-
jetty_dm
2014-03-12 09:35 6721.概述 1. jetty的deploymentManag ... -
jetty_start.jar
2014-03-12 09:35 6341.概述 本文主要分析下jetty的start.jar中的 ... -
jetty_handler
2014-03-12 09:36 9561.handler类图和时序 先上一个handler的继承 ... -
jetty_webappcontext
2014-03-05 15:40 8881.概述 jetty的web工程主要完成servlet中c ... -
jetty_connector
2014-03-14 10:11 7591.Connector的继承体系 jetty的conne ... -
jetty_classloader
2014-03-05 15:41 17021.现象 在从jboss迁移到jetty后,有一个应用页面 ... -
长连接 短连接
2012-03-09 08:54 0一、长连接与短连接: ... -
http 请求流程
2014-03-11 11:15 749我们来看当我们在浏览器输入http://www.myc ... -
关于长连接
2012-03-09 08:43 0在使用短连接方式时, ... -
多线程服务器的常用编程模型 .
2012-01-10 14:48 1821转自: 陈硕 (giantchen_AT_gmail) : ... -
多线程服务器的适用场合
2012-01-10 14:45 4283本博文转自 陈硕 (giantchen_AT_gmail) ...
相关推荐
这篇讨论主要围绕《多线程服务器的适用场合》一文的例释与答疑展开,作者陈硕通过问答形式深入剖析了多线程服务器在不同场景下的表现。 首先,针对“Linux能同时启动多少个线程”的问题,32位Linux系统中,一个进程...
在IT行业中,多线程服务器的适用场合是一个重要的议题,特别是在设计高性能、高并发的网络应用时。本文主要探讨了四种不同的进程与线程模式,并分析了它们在多核服务器环境下的优劣。 1. 单线程进程:这种模式不...
java 实现绘制指针时钟 和多线程服务器java 实现绘制指针时钟 和多线程服务器java 实现绘制指针时钟 和多线程服务器java 实现绘制指针时钟 和多线程服务器java 实现绘制指针时钟 和多线程服务器java 实现绘制指针...
"基于Qt的多线程并发服务器"是一个典型的解决方案,它利用了Qt库的强大功能,特别是其对多线程的支持,来处理来自多个客户端的并发请求。下面我们将深入探讨这个主题。 首先,Qt是一个跨平台的应用程序开发框架,...
"Linux下的多线程服务器程序设计" Linux操作系统是当前最流行的开源操作系统之一,它广泛应用于服务器领域。设计一个高性能的服务器程序是非常重要的,然而传统的服务器工作方式存在三个显著的缺点:fork开销大、...
QT框架中的多线程TCP服务器与客户端编程是一个关键的领域,尤其在开发高效、响应迅速的网络应用时。本文将深入探讨如何利用QT库,特别是QT5.11.1版本,来构建多线程的TCP服务器和客户端,以及QThread在其中的作用。 ...
本文将详细讲解如何初识通信,并以多线程服务器的构建为例,来阐述相关技术点。 首先,理解多线程的概念至关重要。多线程是指在一个进程中可以同时执行多个线程,这样可以充分利用CPU资源,提高程序的执行效率。在...
4. **工作线程与通信**:`Worker`类是我们的工作线程,它继承自`QObject`,并实现了`readyRead`和`writeData`信号。`readyRead`信号在有可读数据时触发,`writeData`信号用于向客户端发送数据。 ```cpp class ...
简单的多线程服务器实现,东北大学网络实验之一
这是在公司同事那里看到的一本书,写的是linux多线程服务器编程
"多线程" 标签表明了服务器采用了多线程编程模型,通过线程池或并发线程处理客户端请求,以提高资源利用率和系统响应速度。 "HTTP post服务器" 标签揭示了服务器的主要功能,即接收和处理HTTP POST请求,这种请求...
本示例中的“TCP多线程服务器demo”是针对初学者设计的一个实践项目,旨在帮助理解如何利用多线程技术来提高服务器的并发处理能力。 首先,TCP协议的基本概念是需要了解的。TCP是一种点对点的通信协议,它通过三次...
服务器端多线程,各客户端向服务器发送服务请求
在IT领域,多线程服务器是一种能够同时处理多个客户端连接请求的服务器实现方式。这种设计模式充分利用了现代计算机的多核或多处理器能力,提高了服务的并发性和响应速度。本篇文章将深入探讨多线程服务器的工作原理...
当涉及多线程Web服务器时,这个概念更进一步,它利用多线程技术来并发处理多个客户端请求,从而提高服务器的性能和响应速度。在Java中实现多线程Web服务器是一项常见的实践,因为Java提供了强大的多线程支持。 首先...
实现多线程的服务器与客户机通信,对应自己的博客https://blog.csdn.net/weixin_43732570/article/details/88633454
- **共享内存**:允许多个进程访问同一段内存区域,适用于需要大量数据交换的场合。 - **消息队列**:类似于管道,但可以在多个无亲缘关系的进程间传递消息。 - **套接字**:支持网络进程间的通信,即使这些进程运行...
基于事件的 NIO 多线程服务器
C++(Socket)实现的TCP协议的多线程例子,允许多个客户端连接