<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>中文 &#187; 陈硕</title>
	<atom:link href="http://software.intel.com/zh-cn/blogs/author/470930/feed/" rel="self" type="application/rss+xml" />
	<link>http://software.intel.com/zh-cn/blogs</link>
	<description></description>
	<lastBuildDate>Mon, 28 May 2012 13:40:23 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.1.3</generator>
		<item>
		<title>《多线程服务器的适用场合》-- 例释与答疑</title>
		<link>http://software.intel.com/zh-cn/blogs/2010/04/28/400003687/</link>
		<comments>http://software.intel.com/zh-cn/blogs/2010/04/28/400003687/#comments</comments>
		<pubDate>Wed, 28 Apr 2010 13:31:34 +0000</pubDate>
		<dc:creator>陈硕</dc:creator>
				<category><![CDATA[博客征文专栏]]></category>
		<category><![CDATA[并行计算]]></category>
		<category><![CDATA[服务器]]></category>
		<category><![CDATA[多线程]]></category>

		<guid isPermaLink="false">http://software.intel.com/zh-cn/blogs/2010/04/28/400003687/</guid>
		<description><![CDATA[《多线程服务器的适用场合》（以下简称《适用场合》）一文在博客登出之后，有热心读者提出质疑，我自己也觉得原文没有把道理说通说透，这篇文章试图用一些实例来解答读者的疑问。我本来打算修改原文，但是考虑到已经读过的读者不一定会注意到文章的变动，干脆另写一篇。为方便阅读，本文以问答体呈现。这篇文章可能会反复修改扩充，请注意上面的版本号。 本文所说的“多线程服务器”的定义与前文一样，同时参见《多线程服务器的常用编程模型》（以下简称《常用模型》）一文的详细界定，以下“连接、端口”均指 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 [...]]]></description>
			<content:encoded><![CDATA[<p>《多线程服务器的适用场合》（以下简称《适用场合》）一文在博客登出之后，有热心读者提出质疑，我自己也觉得原文没有把道理说通说透，这篇文章试图用一些实例来解答读者的疑问。我本来打算修改原文，但是考虑到已经读过的读者不一定会注意到文章的变动，干脆另写一篇。为方便阅读，本文以问答体呈现。这篇文章可能会反复修改扩充，请注意上面的版本号。</p>
<p>本文所说的“多线程服务器”的定义与前文一样，同时参见《多线程服务器的常用编程模型》（以下简称《常用模型》）一文的详细界定，以下“连接、端口”均指 TCP 协议。</p>
<p>1. Linux 能同时启动多少个线程？<br />
对于 32-bit Linux，一个进程的地址空间是 4G，其中用户态能访问 3G 左右，而一个线程的默认栈 (stack) 大小是 10M，心算可知，一个进程大约最多能同时启动 300 个线程。如果不改线程的调用栈大小的话，300 左右是上限，因为程序的其他部分（数据段、代码段、堆、动态库、等等）同样要占用内存（地址空间）。</p>
<p>对于 64-bit 系统，线程数目可大大增加，具体数字我没有测试，因为我实际用不到那么多线程。</p>
<p>以下的关于线程数目的讨论以 32-bit Linux 为例。</p>
<p>2. 多线程能提高并发度吗？<br />
如果指的是“并发连接数”，不能。</p>
<p>由问题 1 可知，假如单纯采用 thread per connection 的模型，那么并发连接数最多 300，这远远低于基于事件的单线程程序所能轻松达到的并发连接数（几千上万，甚至几万）。所谓“基于事件”，指的是用 IO multiplexing event loop 的编程模型，又称 Reactor 模式，在《常用模型》一文中已有介绍。</p>
<p>那么采用《常用模型》一文中推荐的 event loop per thread 呢？至少不逊于单线程程序。</p>
<p>小结：thread per connection 不适合高并发场合，其 scalability 不佳。event loop per thread 的并发度不比单线程程序差。</p>
<p>3. 多线程能提高吞吐量吗？<br />
对于计算密集型服务，不能。</p>
<p>假设有一个耗时的计算服务，用单线程算需要 0.8s。在一台 8 核的机器上，我们可以启动 8 个线程一起对外服务（如果内存够用，启动 8 个进程也一样）。这样完成单个计算仍然要 0.8s，但是由于这些进程的计算可以同时进行，理想情况下吞吐量可以从单线程的 1.25cps （calc per second） 上升到 10cps。（实际情况可能要打个八折——如果不是打对折的话。）</p>
<p>假如改用并行算法，用 8 个核一起算，理论上如果完全并行，加速比高达 8，那么计算时间是 0.1s，吞吐量还是 10cps，但是首次请求的响应时间却降低了很多。实际上根据 Amdahl's law，即便算法的并行度高达 95%，8 核的加速比也只有 6，计算时间为 0.133s，这样会造成吞吐量下降为 7.5cps。不过以此为代价，换得响应时间的提升，在有些应用场合也是值得的。</p>
<p>这也回答了问题 4。</p>
<p>如果用 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) 同步地完成。</p>
<p>为了在并发请求数很高时也能保持稳定的吞吐量，我们可以用线程池，线程池的大小应该满足“阻抗匹配原则”，见问题 7。</p>
<p>线程池也不是万能的，如果响应一次请求需要做比较多的计算（比如计算的时间占整个 response time 的 1/5 强），那么用线程池是合理的，能简化编程。如果一次请求响应中，thread 主要是在等待 IO，那么为了进一步提高吞吐，往往要用其它编程模型，比如 Proactor，见问题 8。</p>
<p>4. 多线程能降低响应时间吗？<br />
如果设计合理，充分利用多核资源的话，可以。在突发 (burst) 请求时效果尤为明显。</p>
<p>例1: 多线程处理输入。</p>
<p>以 memcached 服务端为例。memcached 一次请求响应大概可以分为 3 步：</p>
<p>1.读取并解析客户端输入<br />
2.操作 hashtable<br />
3.返回客户端<br />
在单线程模式下，这 3 步是串行执行的。在启用多线程模式时，它会启用多个输入线程（默认是 4 个），并在建立连接时按 round-robin 法把新连接分派给其中一个输入线程，这正好是我说的 event loop per thread 模型。这样一来，第 1 步的操作就能多线程并行，在多核机器上提高多用户的响应速度。第 2 步用了全局锁，还是单线程的，这可算是一个值得继续改进的地方。</p>
<p>比如，有两个用户同时发出了请求，这两个用户的连接正好分配在两个 IO 线程上，那么两个请求的第 1 步操作可以在两个线程上并行执行，然后汇总到第 2 步串行执行，这样总的响应时间比完全串行执行要短一些（在“读取并解析”所占的比重较大的时候，效果更为明显）。请继续看下面这个例子。</p>
<p>例2: 多线程分担负载。</p>
<p>假设我们要做一个求解 Sudoku 的服务（见《谈谈数独》），这个服务程序在 9981 端口接受请求，输入为一行 81 个数字（待填数字用 0 表示），输出为填好之后的 81 个数字 (1 ~ 9)，如果无解，输出 “NO\r\n”。</p>
<p>由于输入格式很简单，用单个线程做 IO 就行了。先假设每次求解的计算用时 10ms，用前面的方法计算，单线程程序能达到的吞吐量上限为 100req/s，在 8 核机器上，如果用线程池来做计算，能达到的吞吐量上限为 800req/s。下面我们看看多线程如何降低响应时间。</p>
<p>假设 1 个用户在极短的时间内发出了 10 个请求，如果用单线程“来一个处理一个”的模型，这些 reqs 会排在队列里依次处理（这个队列是操作系统的 TCP 缓冲区，不是程序里自己的任务队列）。在不考虑网络延迟的情况下，第 1 个请求的响应时间是 10ms；第 2 个请求要等第 1 个算完了才能获得 CPU 资源，它等了 10ms，算了 10ms，响应时间是 20ms；依次类推，第 10 个请求的响应时间为 100ms；10个请求的平均响应时间为 55ms。</p>
<p>如果 Sudoku 服务在每个请求到达时开始计时，会发现每个请求都是 10ms 响应时间，而从用户的观点，10 个请求的平均响应时间为 55ms，请读者想想为什么会有这个差异。</p>
<p>下面改用多线程：1 个 IO 线程，8 个计算线程（线程池）。二者之间用 BlockingQueue 沟通。同样是 10 个并发请求，第 1 个请求被分配到计算线程1，第 2 个请求被分配到计算线程 2，以此类推，直到第 8 个请求被第 8 个计算线程承担。第 9 和第 10 号请求会等在 BlockingQueue 里，直到有计算线程回到空闲状态才能被处理。（请注意，这里的分配实际上是由操作系统来做，操作系统会从处于 Waiting 状态的线程里挑一个，不一定是 round-robin 的。）</p>
<p>这样一来，前 8 个请求的响应时间差不多都是 10ms，后 2 个请求属于第二批，其响应时间大约会是 20ms，总的平均响应时间是 12ms。可以看出比单线程快了不少。</p>
<p>由于每道 Sudoku 题目的难度不一，对于简单的题目，可能 1ms 就能算出来，复杂的题目最多用 10ms。那么线程池方案的优势就更明显，它能有效地降低“简单任务被复杂任务压住”的出现概率。</p>
<p>以上举的都是计算密集的例子，即线程在响应一次请求时不会等待 IO，下面谈谈更复杂的情况。</p>
<p>5. 多线程程序如何让 IO 和“计算”相互重叠，降低 latency？<br />
基本思路是，把 IO 操作（通常是写操作）通过 BlockingQueue 交给别的线程去做，自己不必等待。</p>
<p>例1: logging</p>
<p>在多线程服务器程序中，日志 (logging) 至关重要，本例仅考虑写 log file 的情况，不考虑 log server。</p>
<p>在一次请求响应中，可能要写多条日志消息，而如果用同步的方式写文件（fprintf 或 fwrite），多半会降低性能，因为：</p>
<p>文件操作一般比较慢，服务线程会等在 IO 上，让 CPU 闲置，增加响应时间。<br />
就算有 buffer，还是不灵。多个线程一起写，为了不至于把 buffer 写错乱，往往要加锁。这会让服务线程互相等待，降低并发度。（同时用多个 log 文件不是办法，除非你有多个磁盘，且保证 log files 分散在不同的磁盘上，否则还是受到磁盘 IO 瓶颈制约。）<br />
解决办法是单独用一个 logging 线程，负责写磁盘文件，通过一个或多个 BlockingQueue 对外提供接口。别的线程要写日志的时候，先把消息（字符串）准备好，然后往 queue 里一塞就行，基本不用等待。这样服务线程的计算就和 logging 线程的磁盘 IO 相互重叠，降低了服务线程的响应时间。</p>
<p>尽管 logging 很重要，但它不是程序的主要逻辑，因此对程序的结构影响越小越好，最好能简单到如同一条 printf 语句，且不用担心其他性能开销，而一个好的多线程异步 logging 库能帮我们做到这一点。（Apache 的 log4cxx 和 log4j 都支持 AsyncAppender 这种异步 logging 方式。）</p>
<p>例2: memcached 客户端</p>
<p>假设我们用 memcached 来保存用户最后发帖的时间，那么每次响应用户发帖的请求时，程序里要去设置一下 memcached 里的值。这一步如果用同步 IO，会增加延迟。</p>
<p>对于“设置一个值”这样的 write-only idempotent 操作，我们其实不用等 memcached 返回操作结果，这里也不用在乎 set 操作失败，那么可以借助多线程来降低响应延迟。比方说我们可以写一个多线程版的 memcached 的客户端，对于 set 操作，调用方只要把 key 和 value 准备好，调用一下 asyncSet() 函数，把数据往 BlockingQueue 上一放就能立即返回，延迟很小。剩下的时就留给 memcached 客户端的线程去操心，而服务线程不受阻碍。</p>
<p>其实所有的网络写操作都可以这么异步地做，不过这也有一个缺点，那就是每次 asyncWrite 都要在线程间传递数据，其实如果 TCP 缓冲区是空的，我们可以在本线程写完，不用劳烦专门的 IO 线程。Jboss 的 Netty 就使用了这个办法来进一步降低延迟。</p>
<p>以上都仅讨论了“打一枪就跑”的情况，如果是一问一答，比如从 memcached 取一个值，那么“重叠 IO”并不能降低响应时间，因为你无论如何要等 memcached 的回复。这时我们可以用别的方式来提高并发度，见问题8。（虽然不能降低响应时间，但也不要浪费线程在空等上，对吧）</p>
<p>另外以上的例子也说明，BlockingQueue 是构建多线程程序的利器。</p>
<p>6. 为什么第三方库往往要用自己的线程？<br />
往往因为 event loop 模型没有标准实现。如果自己写代码，尽可以按所用 Reactor 的推荐方式来编程，但是第三方库不一定能很好地适应并融入这个 event loop framework。有时需要用线程来做一些串并转换。</p>
<p>对于 Java，这个问题还好办一些，因为 thread pool 在 Java 里有标准实现，叫 ExecutorService。如果第三方库支持线程池，那么它可以和主程序共享一个 ExecutorService ，而不是自己创建一堆线程。（比如在初始化时传入主程序的 obj。）对于 C++，情况麻烦得多，Reactor 和 Thread pool 都没有标准库。</p>
<p>例1：libmemcached 只支持同步操作</p>
<p>libmemcached 支持所谓的“非阻塞操作”，但没有暴露一个能被 select/poll/epoll 的 file describer，它的 memcached_fetch 始终会阻塞。它号称 memcached_set 可以是非阻塞的，实际意思是不必等待结果返回，但实际上这个函数会同步地调用 write()，仍可能阻塞在网络 IO 上。</p>
<p>如果在我们的 reactor event handler 里调用了 libmemcached 的函数，那么 latency 就堪忧了。如果想继续用 libmemcached，我们可以为它做一次线程封装，按问题 5 例 2 的办法，同额外的线程专门做 memcached 的 IO，而程序主体还是 reactor。甚至可以把 memcached “数据就绪”作为一个 event，注入到我们的 event loop 中，以进一步提高并发度。（例子留待问题 8 讲）</p>
<p>万幸的是，memcached 的协议非常简单，大不了可以自己写一个基于 reactor 的客户端，但是数据库客户端就没那么幸运了。</p>
<p>例2：MySQL 的官方 C API 不支持异步操作</p>
<p>MySQL 的客户端只支持同步操作，对于 UPDATE/INSERT/DELETE 之类只要行为不管结果的操作（如果代码需要得知其执行结果则另当别论），我们可以用一个单独的线程来做，以降低服务线程的延迟。可仿照前面 memcached_set 的例子，不再赘言。麻烦的是 SELECT，如果要把它也异步化，就得动用更复杂的模式了，见问题 8。</p>
<p>相比之下，PostgreSQL 的 C 客户端 libpq 的设计要好得多，我们可以用 PQsendQuery() 来发起一次查询，然后用标准的 select/poll/epoll 来等待 PQsocket，如果有数据可读，那么用 PQconsumeInput 处理之，并用 PQisBusy 判断查询结果是否已就绪，最后用 PQgetResult 来获取结果。借助这套异步 API，我们可以很容易地为 libpq 写一套 wrapper，使之融入到程序所用的 reactor 模型中。</p>
<p>7. 什么是线程池大小的阻抗匹配原则？<br />
我在《常用模型》中提到“阻抗匹配原则”，这里大致讲一讲。</p>
<p>如果池中线程在执行任务时，密集计算所占的时间比重为 P （0 &lt; P &lt;= 1），而系统一共有 C 个 CPU，为了让这 C 个 CPU 跑满而又不过载，线程池大小的经验公式 T = C/P。（T 是个 hint，考虑到 P 值的估计不是很准确，T 的最佳值可以上下浮动 50%。）</p>
<p>以后我再讲这个经验公式是怎么来的，先验证边界条件的正确性。</p>
<p>假设 C = 8, P = 1.0，线程池的任务完全是密集计算，那么 T = 8。只要 8 个活动线程就能让 8 个 CPU 饱和，再多也没用，因为 CPU 资源已经耗光了。</p>
<p>假设 C = 8, P = 0.5，线程池的任务有一半是计算，有一半等在 IO 上，那么 T = 16。考虑操作系统能灵活合理地调度 sleeping/writing/running 线程，那么大概 16 个“50% 繁忙的线程”能让 8 个 CPU 忙个不停。启动更多的线程并不能提高吞吐量，反而因为增加上下文切换的开销而降低性能。</p>
<p>如果 P &lt; 0.2，这个公式就不适用了，T 可以取一个固定值，比如 5*C。</p>
<p>另外，公式里的 C 不一定是 CPU 总数，可以是“分配给这项任务的 CPU 数目”，比如在 8 核机器上分出 4 个核来做一项任务，那么 C=4。</p>
<p>8. 除了你推荐的 reactor + thread poll，还有别的 non-trivial 多线程编程模型吗？<br />
有，Proactor。</p>
<p>如果一次请求响应中要和别的进程打多次交道，那么 proactor 模型往往能做到更高的并发度。当然，代价是代码变得支离破碎，难以理解。</p>
<p>这里举 http proxy 为例，一次 http proxy 的请求如果没有命中本地 cache，那么它多半会：</p>
<p>1.解析域名 （不要小看这一步，对于一个陌生的域名，解析可能要花半秒钟）<br />
2.建立连接<br />
3.发送 HTTP 请求<br />
4.等待对方回应<br />
5.把结果返回客户<br />
这 5 步里边跟 2 个 server 发生了 3 次 round-trip：</p>
<p>1.向 DNS 问域名，等待回复；<br />
2.向对方 http 服务器发起连接，等待 TCP 三路握手完成；<br />
3.向对方发送 http request，等待对方 response。<br />
而实际上 http proxy 本身的运算量不大，如果用线程池，池中线程的数目会很庞大，不利于操作系统管理调度。</p>
<p>这时我们有两个解决思路：</p>
<p>1.把“域名已解析”，“连接已建立”，“对方已完成响应”做成 event，继续按照 Reactor 的方式来编程。这样一来，每次客户请求就不能用一个函数从头到尾执行完成，而要分成多个阶段，并且要管理好请求的状态（“目前到了第几步？”）。<br />
2.用回调函数，让系统来把任务串起来。比如收到用户请求，如果没有命中本地 cache，立刻发起异步的 DNS 解析 startDNSResolve()，告诉系统在解析完之后调用 DNSResolved() 函数；在 DNSResolved() 中，发起连接，告诉系统在连接建立之后调用 connectionEstablished()；在 connectionEstablished() 中发送 http request，告诉系统在收到响应之后调用 httpResponsed()；最后，在 httpResponsed() 里把结果返回给客户。.NET 大量采用的 Begin/End 操作也是这个编程模式。当然，对于不熟悉这种编程方式的人，代码会显得很难看。Proactor 模式的例子可看 boost::asio 的文档，这里不再多说。<br />
Proactor 模式依赖操作系统或库来高效地调度这些子任务，每个子任务都不会阻塞，因此能用比较少的线程达到很高的 IO 并发度。</p>
<p>Proactor 能提高吞吐，但不能降低延迟，所以我没有深入研究。</p>
<p>9. 模式 2 和模式 3a 该如何取舍？<br />
这里的“模式”不是 pattern，而是 model，不巧它们的中译是一样的。《适用场合》中提到，模式 2 是一个多线程的进程，模式 3a 是多个相同的单线程进程。</p>
<p>我认为，在其他条件相同的情况下，可以根据工作集 (work set) 的大小来取舍。工作集是指服务程序响应一次请求所访问的内存大小。</p>
<p>如果工作集较大，那么就用多线程，避免 CPU cache 换入换出，影响性能；否则，就用单线程多进程，享受单线程编程的便利。</p>
<p>例如，memcached 这个内存消耗大户用多线程服务端就比在同一台机器上运行多个 memcached instance 要好。（除非你在 16G 内存的机器上运行 32-bit memcached，那么多 instance 是必须的。）</p>
<p>又例如，求解 Sudoku 用不了多大内存，如果单线程编程更方便的话，可以用单线程多进程来做。再在前面加一个单线程的 load balancer，仿 lighttpd + fastcgi 的成例。</p>
<p>线程不能减少工作量，即不能减少 CPU 时间。如果解决一个问题需要执行一亿条指令（这个数字不大，不要被吓到），那么用多线程只会让这个数字增加。但是通过合理调配这一亿条指令在多个核上的执行情况，我们能让工期提早结束。这听上去像统筹方法，确实也正是统筹方法。</p>
]]></content:encoded>
			<wfw:commentRss>http://software.intel.com/zh-cn/blogs/2010/04/28/400003687/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>多线程服务器的适用场合</title>
		<link>http://software.intel.com/zh-cn/blogs/2010/03/24/400003436/</link>
		<comments>http://software.intel.com/zh-cn/blogs/2010/03/24/400003436/#comments</comments>
		<pubDate>Wed, 24 Mar 2010 10:24:44 +0000</pubDate>
		<dc:creator>陈硕</dc:creator>
				<category><![CDATA[博客征文专栏]]></category>
		<category><![CDATA[并行计算]]></category>
		<category><![CDATA[多线程]]></category>

		<guid isPermaLink="false">http://software.intel.com/zh-cn/blogs/2010/03/24/400003436/</guid>
		<description><![CDATA[这篇文章原本是前一篇博客《多线程服务器的常用编程模型》（以下简称《常用模型》）计划中的一节，今天终于写完了。 “服务器开发”包罗万象，本文所指的“服务器开发”的含义请见《常用模型》一文，一句话形容是：跑在多核机器上的 Linux 用户态的没有用户界面的长期运行的网络应用程序。“长期运行”的意思不是指程序 7x24 不重启，而是程序不会因为无事可做而退出，它会等着下一个请求的到来。例如 wget 不是长期运行的，httpd 是长期运行的。 正名 与前文相同，本文的“进程”指的是 fork() 系统调用的产物。“线程”指的是 pthread_create() 的产物，而且我指的 pthreads 是 NPTL 的，每个线程由 clone() 产生，对应一个内核的 task_struct。本文所用的开发语言是 C++，运行环境为 Linux。 首先，一个由多台机器组成的分布式系统必然是多进程的（字面意义上），因为进程不能跨 OS 边界。在这个前提下，我们把目光集中到一台机器，一台拥有至少 4 个核的普通服务器。如果要在一台多核机器上提供一种服务或执行一个任务，可用的模式有： 运行一个单线程的进程 运行一个多线程的进程 运行多个单线程的进程 运行多个多线程的进程 这些模式之间的比较已经是老生常谈，简单地总结： 模式 1 是不可伸缩的 (scalable)，不能发挥多核机器的计算能力； 模式 3 是目前公认的主流模式。它有两种子模式： 3a 简单地把模式 1 中的进程运行多份，如果能用多个 tcp port 对外提供服务的话； 3b 主进程+woker进程，如果必须绑定到一个 tcp port，比如 httpd+fastcgi。 模式 2 [...]]]></description>
			<content:encoded><![CDATA[<p>这篇文章原本是前一篇博客《多线程服务器的常用编程模型》（以下简称《常用模型》）计划中的一节，今天终于写完了。</p>
<p>“服务器开发”包罗万象，本文所指的“服务器开发”的含义请见《常用模型》一文，一句话形容是：跑在多核机器上的 Linux 用户态的没有用户界面的长期运行的网络应用程序。“长期运行”的意思不是指程序 7x24 不重启，而是程序不会因为无事可做而退出，它会等着下一个请求的到来。例如 wget 不是长期运行的，httpd 是长期运行的。</p>
<p>正名<br />
与前文相同，本文的“进程”指的是 fork() 系统调用的产物。“线程”指的是 pthread_create() 的产物，而且我指的 pthreads 是 NPTL 的，每个线程由 clone() 产生，对应一个内核的 task_struct。本文所用的开发语言是 C++，运行环境为 Linux。</p>
<p>首先，一个由多台机器组成的分布式系统必然是多进程的（字面意义上），因为进程不能跨 OS 边界。在这个前提下，我们把目光集中到一台机器，一台拥有至少 4 个核的普通服务器。如果要在一台多核机器上提供一种服务或执行一个任务，可用的模式有：</p>
<p>运行一个单线程的进程<br />
运行一个多线程的进程<br />
运行多个单线程的进程<br />
运行多个多线程的进程<br />
这些模式之间的比较已经是老生常谈，简单地总结：</p>
<p>模式 1 是不可伸缩的 (scalable)，不能发挥多核机器的计算能力；<br />
模式 3 是目前公认的主流模式。它有两种子模式：<br />
3a 简单地把模式 1 中的进程运行多份，如果能用多个 tcp port 对外提供服务的话；<br />
3b 主进程+woker进程，如果必须绑定到一个 tcp port，比如 httpd+fastcgi。<br />
模式 2 是很多人鄙视的，认为多线程程序难写，而且不比模式 3 有什么优势；<br />
模式 4 更是千夫所指，它不但没有结合 2 和 3 的优点，反而汇聚了二者的缺点。<br />
本文主要想讨论的是模式 2 和模式 3b 的优劣，即：什么时候一个服务器程序应该是多线程的。</p>
<p>从功能上讲，没有什么是多线程能做到而单线程做不到的，反之亦然，都是状态机嘛（我很高兴看到反例）。从性能上讲，无论是 IO bound 还是 CPU bound 的服务，多线程都没有什么优势。那么究竟为什么要用多线程？</p>
<p>在回答这个问题之前，我先谈谈必须用必须用单线程的场合。</p>
<p>必须用单线程的场合<br />
据我所知，有两种场合必须使用单线程：</p>
<p>程序可能会 fork()<br />
限制程序的 CPU 占用率<br />
先说 fork()，我在《Linux 新增系统调用的启示》中提到：</p>
<p>fork() 一般不能在多线程程序中调用，因为 Linux 的 fork() 只克隆当前线程的 thread of control，不克隆其他线程。也就是说不能一下子 fork() 出一个和父进程一样的多线程子进程，Linux 也没有 forkall() 这样的系统调用。forkall() 其实也是很难办的（从语意上），因为其他线程可能等在 condition variable 上，可能阻塞在系统调用上，可能等着 mutex 以跨入临界区，还可能在密集的计算中，这些都不好全盘搬到子进程里。</p>
<p>更为糟糕的是，如果在 fork() 的一瞬间某个别的线程 a 已经获取了 mutex，由于 fork() 出的新进程里没有这个“线程a”，那么这个 mutex 永远也不会释放，新的进程就不能再获取那个 mutex，否则会死锁。（这一点仅为推测，还没有做实验，不排除 fork() 会释放所有 mutex 的可能。）</p>
<p>综上，一个设计为可能调用 fork() 的程序必须是单线程的，比如我在《启示》一文中提到的“看门狗进程”。多线程程序不是不能调用 fork()，而是这么做会遇到很多麻烦，我想不出做的理由。</p>
<p>一个程序 fork() 之后一般有两种行为：</p>
<p>立刻执行 exec()，变身为另一个程序。例如 shell 和 inetd；又比如 lighttpd fork() 出子进程，然后运行 fastcgi 程序。或者集群中运行在计算节点上的负责启动 job 的守护进程（即我所谓的“看门狗进程”）。<br />
不调用 exec()，继续运行当前程序。要么通过共享的文件描述符与父进程通信，协同完成任务；要么接过父进程传来的文件描述符，独立完成工作，例如 80 年代的 web 服务器 NCSA httpd。<br />
这些行为中，我认为只有“看门狗进程”必须坚持单线程，其他的均可替换为多线程程序（从功能上讲）。</p>
<p>单线程程序能限制程序的 CPU 占用率。</p>
<p>这个很容易理解，比如在一个 8-core 的主机上，一个单线程程序即便发生 busy-wait（无论是因为 bug 还是因为 overload），其 CPU 使用率也只有 12.5%，即占满 1 个 core。在这种最坏的情况下，系统还是有 87.5% 的计算资源可供其他服务进程使用。</p>
<p>因此对于一些辅助性的程序，如果它必须和主要功能进程运行在同一台机器的话（比如它要监控其他服务进程的状态），那么做成单线程的能避免过分抢夺系统的计算资源。</p>
<p>基于进程的分布式系统设计<br />
《常用模型》一文提到，分布式系统的软件设计和功能划分一般应该以“进程”为单位。我提倡用多线程，并不是说把整个系统放到一个进程里实现，而是指功能划分之后，在实现每一类服务进程时，在必要时可以借助多线程来提高性能。对于整个分布式系统，要做到能 scale out，即享受增加机器带来的好处。</p>
<p>对于上层的应用而言，每个进程的代码量控制在 10 万行 C++ 以下，这不包括现成的 library 的代码量。这样每个进程都能被一个脑子完全理解，不会出现混乱。（其实我更想说 5 万行。）</p>
<p>这里推荐一篇 Google 的好文《Introduction to Distributed System Design》。其中点睛之笔是：分布式系统设计，是 design for failure。</p>
<p>本文继续讨论一个服务进程什么时候应该用多线程，先说说单线程的优势。</p>
<p>单线程程序的优势<br />
从编程的角度，单线程程序的优势无需赘言：简单。程序的结构一般如《常用模型》所言，是一个基于 IO multiplexing 的 event loop。或者如云风所言，直接用阻塞 IO。</p>
<p>event loop 的典型代码框架是：</p>
<p>while (!done) {<br />
int retval = ::poll(fds, nfds, timeout_ms);<br />
if (retval 0) {<br />
处理 IO 事件<br />
}<br />
}<br />
}</p>
<p>event loop 有一个明显的缺点，它是非抢占的(non-preemptive)。假设事件 a 的优先级高于事件 b，处理事件 a 需要 1ms，处理事件 b 需要 10ms。如果事件 b 稍早于 a 发生，那么当事件 a 到来时，程序已经离开了 poll() 调用开始处理事件 b。事件 a 要等上 10ms 才有机会被处理，总的响应时间为 11ms。这等于发生了优先级反转。</p>
<p>这可缺点可以用多线程来克服，这也是多线程的主要优势。</p>
<p>多线程程序有性能优势吗？<br />
前面我说，无论是 IO bound 还是 CPU bound 的服务，多线程都没有什么绝对意义上的性能优势。这里详细阐述一下这句话的意思。</p>
<p>这句话是说，如果用很少的 CPU 负载就能让的 IO 跑满，或者用很少的 IO 流量就能让 CPU 跑满，那么多线程没啥用处。举例来说：</p>
<p>对于静态 web 服务器，或者 ftp 服务器，CPU 的负载较轻，主要瓶颈在磁盘 IO 和网络 IO。这时候往往一个单线程的程序（模式 1）就能撑满 IO。用多线程并不能提高吞吐量，因为 IO 硬件容量已经饱和了。同理，这时增加 CPU 数目也不能提高吞吐量。<br />
CPU 跑满的情况比较少见，这里我只好虚构一个例子。假设有一个服务，它的输入是 n 个整数，问能否从中选出 m 个整数，使其和为 0 （这里 n 0）。这是著名的 subset sum 问题，是 NP-Complete 的。对于这样一个“服务”，哪怕很小的 n 值也会让 CPU 算死，比如 n = 30，一次的输入不过 120 字节（32-bit 整数），CPU 的运算时间可能长达几分钟。对于这种应用，模式 3a 是最适合的，能发挥多核的优势，程序也简单。<br />
也就是说，无论任何一方早早地先到达瓶颈，多线程程序都没啥优势。</p>
<p>说到这里，可能已经有读者不耐烦了：你讲了这么多，都在说单线程的好处，那么多线程究竟有什么用？</p>
<p>适用多线程程序的场景<br />
我认为多线程的适用场景是：提高响应速度，让 IO 和“计算”相互重叠，降低 latency。</p>
<p>虽然多线程不能提高绝对性能，但能提高平均响应性能。</p>
<p>一个程序要做成多线程的，大致要满足：</p>
<p>有多个 CPU 可用。单核机器上多线程的优势不明显。<br />
线程间有共享数据。如果没有共享数据，用模型 3b 就行。虽然我们应该把线程间的共享数据降到最低，但不代表没有；<br />
共享的数据是可以修改的，而不是静态的常量表。如果数据不能修改，那么可以在进程间用 shared memory，模式 3 就能胜任；<br />
提供非均质的服务。即，事件的响应有优先级差异，我们可以用专门的线程来处理优先级高的事件。防止优先级反转；<br />
latency 和 throughput 同样重要，不是逻辑简单的 IO bound 或 CPU bound 程序；<br />
利用异步操作。比如 logging。无论往磁盘写 log file，还是往 log server 发送消息都不应该阻塞 critical path；<br />
能 scale up。一个好的多线程程序应该能享受增加 CPU 数目带来的好处，目前主流是 8 核，很快就会用到 16 核的机器了。<br />
具有可预测的性能。随着负载增加，性能缓慢下降，超过某个临界点之后急速下降。线程数目一般不随负载变化。<br />
多线程能有效地划分责任与功能，让每个线程的逻辑比较简单，任务单一，便于编码。而不是把所有逻辑都塞到一个 event loop 里，就像 Win32 SDK 程序那样。<br />
这些条件比较抽象，这里举一个具体的（虽然是虚构的）例子。</p>
<p>假设要管理一个 Linux 服务器机群，这个机群里有 8 个计算节点，1 个控制节点。机器的配置都是一样的，双路四核 CPU，千兆网互联。现在需要编写一个简单的机群管理软件（参考 LLNL 的 SLURM），这个软件由三个程序组成：</p>
<p>运行在控制节点上的 master，这个程序监视并控制整个机群的状态。<br />
运在每个计算节点上的 slave，负责启动和终止 job，并监控本机的资源。<br />
给最终用户的 client 命令行工具，用于提交 job。<br />
根据前面的分析，slave 是个“看门狗进程”，它会启动别的 job 进程，因此必须是个单线程程序。另外它不应该占用太多的 CPU 资源，这也适合单线程模型。</p>
<p>master 应该是个模式 2 的多线程程序：</p>
<p>它独占一台 8 核的机器，如果用模型 1，等于浪费了 87.5% 的 CPU 资源。<br />
整个机群的状态应该能完全放在内存中，这些状态是共享且可变的。如果用模式 3，那么进程之间的状态同步会成大问题。而如果大量使用共享内存，等于是掩耳盗铃，披着多进程外衣的多线程程序。<br />
master 的主要性能指标不是 throughput，而是 latency，即尽快地响应各种事件。它几乎不会出现把 IO 或 CPU 跑满的情况。<br />
master 监控的事件有优先级区别，一个程序正常运行结束和异常崩溃的处理优先级不同，计算节点的磁盘满了和机箱温度过高这两种报警条件的优先级也不同。如果用单线程，可能会出现优先级反转。<br />
假设 master 和每个 slave 之间用一个 TCP 连接，那么 master 采用 2 个或 4 个 IO 线程来处理 8 个 TCP connections 能有效地降低延迟。<br />
master 要异步的往本地硬盘写 log，这要求 logging library 有自己的 IO 线程。<br />
master 有可能要读写数据库，那么数据库连接这个第三方 library 可能有自己的线程，并回调 master 的代码。<br />
master 要服务于多个 clients，用多线程也能降低客户响应时间。也就是说它可以再用 2 个 IO 线程专门处理和 clients 的通信。<br />
master 还可以提供一个 monitor 接口，用来广播 (pushing) 机群的状态，这样用户不用主动轮询 (polling)。这个功能如果用单独的线程来做，会比较容易实现，不会搞乱其他主要功能。<br />
master 一共开了 10 个线程：<br />
4 个用于和 slaves 通信的 IO 线程<br />
1 个 logging 线程<br />
1 个数据库 IO 线程<br />
2 个和 clients 通信的 IO 线程<br />
1 个主线程，用于做些背景工作，比如 job 调度<br />
1 个 pushing 线程，用于主动广播机群的状态<br />
虽然线程数目略多于 core 数目，但是这些线程很多时候都是空闲的，可以依赖 OS 的进程调度来保证可控的延迟。<br />
综上所述，master 用多线程方式编写是自然且高效的。</p>
<p>线程的分类<br />
据我的经验，一个多线程服务程序中的线程大致可分为 3 类：</p>
<p>IO 线程，这类线程的的主循环是 io multiplexing，等在 select/poll/epoll 系统调用上。这类线程也处理定时事件。当然它的功能不止 IO，有些计算也可以放入其中。<br />
计算线程，这类线程的主循环是 blocking queue，等在 condition variable 上。这类线程一般位于 thread pool 中。<br />
第三方库所用的线程，比如 logging，又比如 database connection。<br />
服务器程序一般不会频繁地启动和终止线程。甚至，在我写过的程序里，create thread 只在程序启动的时候调用，在服务运行期间是不调用的。</p>
<p>在多核时代，多线程编程是不可避免的，“鸵鸟算法”不是办法。</p>
]]></content:encoded>
			<wfw:commentRss>http://software.intel.com/zh-cn/blogs/2010/03/24/400003436/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>多线程服务器的常用编程模型</title>
		<link>http://software.intel.com/zh-cn/blogs/2010/03/24/400003435/</link>
		<comments>http://software.intel.com/zh-cn/blogs/2010/03/24/400003435/#comments</comments>
		<pubDate>Wed, 24 Mar 2010 10:23:56 +0000</pubDate>
		<dc:creator>陈硕</dc:creator>
				<category><![CDATA[博客征文专栏]]></category>
		<category><![CDATA[并行计算]]></category>
		<category><![CDATA[服务器]]></category>
		<category><![CDATA[多线程]]></category>

		<guid isPermaLink="false">http://software.intel.com/zh-cn/blogs/2010/03/24/400003435/</guid>
		<description><![CDATA[本文主要讲我个人在多线程开发方面的一些粗浅经验。总结了一两种常用的线程模型，归纳了进程间通讯与线程同步的最佳实践，以期用简单规范的方式开发多线程程序。 文中的“多线程服务器”是指运行在 Linux 操作系统上的独占式网络应用程序。硬件平台为 Intel x64 系列的多核 CPU，单路或双路 SMP 服务器（每台机器一共拥有四个核或八个核，十几 GB 内存），机器之间用百兆或千兆以太网连接。这大概是目前民用 PC 服务器的主流配置。 本文不涉及 Windows 系统，不涉及人机交互界面（无论命令行或图形）；不考虑文件读写（往磁盘写 log 除外），不考虑数据库操作，不考虑 Web 应用；不考虑低端的单核主机或嵌入式系统，不考虑手持式设备，不考虑专门的网络设备，不考虑高端的 &#62;=32 核 Unix 主机；只考虑 TCP，不考虑 UDP，也不考虑除了局域网络之外的其他数据收发方式（例如串并口、USB口、数据采集板卡、实时控制等）。 有了以上这么多限制，那么我将要谈的“网络应用程序”的基本功能可以归纳为“收到数据，算一算，再发出去”。在这个简化了的模型里，似乎看不出用多线程的必要，单线程应该也能做得很好。“为什么需要写多线程程序”这个问题容易引发口水战，我放到另一篇博客里讨论。请允许我先假定“多线程编程”这一背景。 “服务器”这个词有时指程序，有时指进程，有时指硬件（无论虚拟的或真实的），请注意按上下文区分。另外，本文不考虑虚拟化的场景，当我说“两个进程不在同一台机器上”，指的是逻辑上不在同一个操作系统里运行，虽然物理上可能位于同一机器虚拟出来的两台“虚拟机”上。 本文假定读者已经有多线程编程的知识与经验，这不是一篇入门教程。 本文承蒙 Milo Yip 先生审读，在此深表谢意。当然，文中任何错误责任均在我。 1 进程与线程 “进程/process”是操作里最重要的两个概念之一（另一个是文件），粗略地讲，一个进程是“内存中正在运行的程序”。本文的进程指的是 Linux 操作系统通过 fork() 系统调用产生的那个东西，或者 Windows 下 CreateProcess() 的产物，不是 Erlang 里的那种轻量级进程。 每个进程有自己独立的地址空间 (address space)，“在同一个进程”还是“不在同一个进程”是系统功能划分的重要决策点。Erlang 书把“进程”比喻为“人”，我觉得十分精当，为我们提供了一个思考的框架。 每个人有自己的记忆 (memory)，人与人通过谈话（消息传递）来交流，谈话既可以是面谈（同一台服务器），也可以在电话里谈（不同的服务器，有网络通信）。面谈和电话谈的区别在于，面谈可以立即知道对方死否死了（crash, SIGCHLD），而电话谈只能通过周期性的心跳来判断对方是否还活着。 有了这些比喻，设计分布式系统时可以采取“角色扮演”，团队里的几个人各自扮演一个进程，人的角色由进程的代码决定（管登陆的、管消息分发的、管买卖的等等）。每个人有自己的记忆，但不知道别人的记忆，要想知道别人的看法，只能通过交谈。（暂不考虑共享内存这种 [...]]]></description>
			<content:encoded><![CDATA[<p>本文主要讲我个人在多线程开发方面的一些粗浅经验。总结了一两种常用的线程模型，归纳了进程间通讯与线程同步的最佳实践，以期用简单规范的方式开发多线程程序。</p>
<p>文中的“多线程服务器”是指运行在 Linux 操作系统上的独占式网络应用程序。硬件平台为 Intel x64 系列的多核 CPU，单路或双路 SMP 服务器（每台机器一共拥有四个核或八个核，十几 GB 内存），机器之间用百兆或千兆以太网连接。这大概是目前民用 PC 服务器的主流配置。</p>
<p>本文不涉及 Windows 系统，不涉及人机交互界面（无论命令行或图形）；不考虑文件读写（往磁盘写 log 除外），不考虑数据库操作，不考虑 Web 应用；不考虑低端的单核主机或嵌入式系统，不考虑手持式设备，不考虑专门的网络设备，不考虑高端的 &gt;=32 核 Unix 主机；只考虑 TCP，不考虑 UDP，也不考虑除了局域网络之外的其他数据收发方式（例如串并口、USB口、数据采集板卡、实时控制等）。</p>
<p>有了以上这么多限制，那么我将要谈的“网络应用程序”的基本功能可以归纳为“收到数据，算一算，再发出去”。在这个简化了的模型里，似乎看不出用多线程的必要，单线程应该也能做得很好。“为什么需要写多线程程序”这个问题容易引发口水战，我放到另一篇博客里讨论。请允许我先假定“多线程编程”这一背景。</p>
<p>“服务器”这个词有时指程序，有时指进程，有时指硬件（无论虚拟的或真实的），请注意按上下文区分。另外，本文不考虑虚拟化的场景，当我说“两个进程不在同一台机器上”，指的是逻辑上不在同一个操作系统里运行，虽然物理上可能位于同一机器虚拟出来的两台“虚拟机”上。</p>
<p>本文假定读者已经有多线程编程的知识与经验，这不是一篇入门教程。</p>
<p>本文承蒙 Milo Yip 先生审读，在此深表谢意。当然，文中任何错误责任均在我。</p>
<p>1 进程与线程<br />
“进程/process”是操作里最重要的两个概念之一（另一个是文件），粗略地讲，一个进程是“内存中正在运行的程序”。本文的进程指的是 Linux 操作系统通过 fork() 系统调用产生的那个东西，或者 Windows 下 CreateProcess() 的产物，不是 Erlang 里的那种轻量级进程。</p>
<p>每个进程有自己独立的地址空间 (address space)，“在同一个进程”还是“不在同一个进程”是系统功能划分的重要决策点。Erlang 书把“进程”比喻为“人”，我觉得十分精当，为我们提供了一个思考的框架。</p>
<p>每个人有自己的记忆 (memory)，人与人通过谈话（消息传递）来交流，谈话既可以是面谈（同一台服务器），也可以在电话里谈（不同的服务器，有网络通信）。面谈和电话谈的区别在于，面谈可以立即知道对方死否死了（crash, SIGCHLD），而电话谈只能通过周期性的心跳来判断对方是否还活着。</p>
<p>有了这些比喻，设计分布式系统时可以采取“角色扮演”，团队里的几个人各自扮演一个进程，人的角色由进程的代码决定（管登陆的、管消息分发的、管买卖的等等）。每个人有自己的记忆，但不知道别人的记忆，要想知道别人的看法，只能通过交谈。（暂不考虑共享内存这种 IPC。）然后就可以思考容错（万一有人突然死了）、扩容（新人中途加进来）、负载均衡（把 a 的活儿挪給 b 做）、退休（a 要修复 bug，先别给他派新活儿，等他做完手上的事情就把他重启）等等各种场景，十分便利。</p>
<p>“线程”这个概念大概是在 1993 年以后才慢慢流行起来的，距今不过十余年，比不得有 40 年光辉历史的 Unix 操作系统。线程的出现给 Unix 添了不少乱，很多 C 库函数（strtok(), ctime()）不是线程安全的，需要重新定义；signal 的语意也大为复杂化。据我所知，最早支持多线程编程的（民用）操作系统是 Solaris 2.2 和 Windows NT 3.1，它们均发布于 1993 年。随后在 1995 年，POSIX threads 标准确立。</p>
<p>线程的特点是共享地址空间，从而可以高效地共享数据。一台机器上的多个进程能高效地共享代码段（操作系统可以映射为同样的物理内存），但不能共享数据。如果多个进程大量共享内存，等于是把多进程程序当成多线程来写，掩耳盗铃。</p>
<p>“多线程”的价值，我认为是为了更好地发挥对称多路处理 (SMP) 的效能。在 SMP 之前，多线程没有多大价值。Alan Cox 说过 A computer is a state machine. Threads are for people who can't program state machines. （计算机是一台状态机。线程是给那些不能编写状态机程序的人准备的。）如果只有一个执行单元，一个 CPU，那么确实如 Alan Cox 所说，按状态机的思路去写程序是最高效的，这正好也是下一节展示的编程模型。</p>
<p>2 典型的单线程服务器编程模型<br />
UNP3e 对此有很好的总结（第 6 章：IO 模型，第 30 章：客户端/服务器设计范式），这里不再赘述。据我了解，在高性能的网络程序中，使用得最为广泛的恐怕要数“non-blocking IO + IO multiplexing”这种模型，即 Reactor 模式，我知道的有：</p>
<p>l lighttpd，单线程服务器。（nginx 估计与之类似，待查）</p>
<p>l libevent/libev</p>
<p>l ACE，Poco C++ libraries（QT 待查）</p>
<p>l Java NIO (Selector/SelectableChannel), Apache Mina, Netty (Java)</p>
<p>l POE (Perl)</p>
<p>l Twisted (Python)</p>
<p>相反，boost::asio 和 Windows I/O Completion Ports 实现了 Proactor 模式，应用面似乎要窄一些。当然，ACE 也实现了 Proactor 模式，不表。</p>
<p>在“non-blocking IO + IO multiplexing”这种模型下，程序的基本结构是一个事件循环 (event loop)：（代码仅为示意，没有完整考虑各种情况）</p>
<p>while (!done)</p>
<p>{</p>
<p>int timeout_ms = max(1000, getNextTimedCallback());</p>
<p>int retval = ::poll(fds, nfds, timeout_ms);</p>
<p>if (retval 0) {</p>
<p>处理 IO 事件</p>
<p>}</p>
<p>}</p>
<p>}</p>
<p>当然，select(2)/poll(2) 有很多不足，Linux 下可替换为 epoll，其他操作系统也有对应的高性能替代品（搜 c10k problem）。</p>
<p>Reactor 模型的优点很明显，编程简单，效率也不错。不仅网络读写可以用，连接的建立（connect/accept）甚至 DNS 解析都可以用非阻塞方式进行，以提高并发度和吞吐量 (throughput)。对于 IO 密集的应用是个不错的选择，Lighttpd 即是这样，它内部的 fdevent 结构十分精妙，值得学习。（这里且不考虑用阻塞 IO 这种次优的方案。）</p>
<p>当然，实现一个优质的 Reactor 不是那么容易，我也没有用过坊间开源的库，这里就不推荐了。</p>
<p>3 典型的多线程服务器的线程模型<br />
这方面我能找到的文献不多，大概有这么几种：</p>
<p>1. 每个请求创建一个线程，使用阻塞式 IO 操作。在 Java 1.4 引入 NIO 之前，这是 Java 网络编程的推荐做法。可惜伸缩性不佳。</p>
<p>2. 使用线程池，同样使用阻塞式 IO 操作。与 1 相比，这是提高性能的措施。</p>
<p>3. 使用 non-blocking IO + IO multiplexing。即 Java NIO 的方式。</p>
<p>4. Leader/Follower 等高级模式</p>
<p>在默认情况下，我会使用第 3 种，即 non-blocking IO + one loop per thread 模式。</p>
<p>One loop per thread<br />
此种模型下，程序里的每个 IO 线程有一个 event loop （或者叫 Reactor），用于处理读写和定时事件（无论周期性的还是单次的），代码框架跟第 2 节一样。</p>
<p>这种方式的好处是：</p>
<p>l 线程数目基本固定，可以在程序启动的时候设置，不会频繁创建与销毁。</p>
<p>l 可以很方便地在线程间调配负载。</p>
<p>event loop 代表了线程的主循环，需要让哪个线程干活，就把 timer 或 IO channel (TCP connection) 注册到那个线程的 loop 里即可。对实时性有要求的 connection 可以单独用一个线程；数据量大的 connection 可以独占一个线程，并把数据处理任务分摊到另几个线程中；其他次要的辅助性 connections 可以共享一个线程。</p>
<p>对于 non-trivial 的服务端程序，一般会采用 non-blocking IO + IO multiplexing，每个 connection/acceptor 都会注册到某个 Reactor 上，程序里有多个 Reactor，每个线程至多有一个 Reactor。</p>
<p>多线程程序对 Reactor 提出了更高的要求，那就是“线程安全”。要允许一个线程往别的线程的 loop 里塞东西，这个 loop 必须得是线程安全的。</p>
<p>线程池<br />
不过，对于没有 IO 光有计算任务的线程，使用 event loop 有点浪费，我会用有一种补充方案，即用 blocking queue 实现的任务队列(TaskQueue)：</p>
<p>blocking_queue&lt;boost::function &gt; taskQueue; // 线程安全的阻塞队列</p>
<p>void worker_thread()</p>
<p>{</p>
<p>while (!quit) {</p>
<p>boost::function task = taskQueue.take(); // this blocks</p>
<p>task(); // 在产品代码中需要考虑异常处理</p>
<p>}</p>
<p>}</p>
<p>用这种方式实现线程池特别容易：</p>
<p>启动容量为 N 的线程池：</p>
<p>int N = num_of_computing_threads;</p>
<p>for (int i = 0; i &lt; N; ++i) {</p>
<p>create_thread(&amp;worker_thread); // 伪代码：启动线程</p>
<p>}</p>
<p>使用起来也很简单：</p>
<p>boost::function task = boost::bind(&amp;Foo::calc, this);</p>
<p>taskQueue.post(task);</p>
<p>上面十几行代码就实现了一个简单的固定数目的线程池，功能大概相当于 Java 5 的 ThreadPoolExecutor 的某种“配置”。当然，在真实的项目中，这些代码都应该封装到一个 class 中，而不是使用全局对象。另外需要注意一点：Foo 对象的生命期，我的另一篇博客《当析构函数遇到多线程——C++ 中线程安全的对象回调》详细讨论了这个问题</p>
<p>除了任务队列，还可以用 blocking_queue 实现数据的消费者-生产者队列，即 T 的是数据类型而非函数对象，queue 的消费者(s)从中拿到数据进行处理。这样做比 task queue 更加 specific 一些。</p>
<p>blocking_queue 是多线程编程的利器，它的实现可参照 Java 5 util.concurrent 里的 (Array|Linked)BlockingQueue，通常 C++ 可以用 deque 来做底层的容器。Java 5 里的代码可读性很高，代码的基本结构和教科书一致（1 个 mutex，2 个 condition variables），健壮性要高得多。如果不想自己实现，用现成的库更好。（我没有用过免费的库，这里就不乱推荐了，有兴趣的同学可以试试 Intel Threading Building Blocks 里的 concurrent_queue。）</p>
<p>归纳<br />
总结起来，我推荐的多线程服务端编程模式为：event loop per thread + thread pool。</p>
<p>l event loop 用作 non-blocking IO 和定时器。</p>
<p>l thread pool 用来做计算，具体可以是任务队列或消费者-生产者队列。</p>
<p>以这种方式写服务器程序，需要一个优质的基于 Reactor 模式的网络库来支撑，我只用过 in-house 的产品，无从比较并推荐市面上常见的 C++ 网络库，抱歉。</p>
<p>程序里具体用几个 loop、线程池的大小等参数需要根据应用来设定，基本的原则是“阻抗匹配”，使得 CPU 和 IO 都能高效地运作，具体的考虑点容我以后再谈。</p>
<p>这里没有谈线程的退出，留待下一篇 blog“多线程编程反模式”探讨。</p>
<p>此外，程序里或许还有个别执行特殊任务的线程，比如 logging，这对应用程序来说基本是不可见的，但是在分配资源（CPU 和 IO）的时候要算进去，以免高估了系统的容量。</p>
<p>4 进程间通信与线程间通信<br />
Linux 下进程间通信 (IPC) 的方式数不胜数，光 UNPv2 列出的就有：pipe、FIFO、POSIX 消息队列、共享内存、信号 (signals) 等等，更不必说 Sockets 了。同步原语 (synchronization primitives) 也很多，互斥器 (mutex)、条件变量 (condition variable)、读写锁 (reader-writer lock)、文件锁 (Record locking)、信号量 (Semaphore) 等等。</p>
<p>如何选择呢？根据我的个人经验，贵精不贵多，认真挑选三四样东西就能完全满足我的工作需要，而且每样我都能用得很熟,，不容易犯错。</p>
<p>5 进程间通信<br />
进程间通信我首选 Sockets（主要指 TCP，我没有用过 UDP，也不考虑 Unix domain 协议），其最大的好处在于：可以跨主机，具有伸缩性。反正都是多进程了，如果一台机器处理能力不够，很自然地就能用多台机器来处理。把进程分散到同一局域网的多台机器上，程序改改 host:port 配置就能继续用。相反，前面列出的其他 IPC 都不能跨机器（比如共享内存效率最高，但再怎么着也不能高效地共享两台机器的内存），限制了 scalability。</p>
<p>在编程上，TCP sockets 和 pipe 都是一个文件描述符，用来收发字节流，都可以 read/write/fcntl/select/poll 等。不同的是，TCP 是双向的，pipe 是单向的 (Linux)，进程间双向通讯还得开两个文件描述符，不方便；而且进程要有父子关系才能用 pipe，这些都限制了 pipe 的使用。在收发字节流这一通讯模型下，没有比 sockets/TCP 更自然的 IPC 了。当然，pipe 也有一个经典应用场景，那就是写 Reactor/Selector 时用来异步唤醒 select (或等价的 poll/epoll) 调用（Sun JVM 在 Linux 就是这么做的）。</p>
<p>TCP port 是由一个进程独占，且操作系统会自动回收（listening port 和已建立连接的 TCP socket 都是文件描述符，在进程结束时操作系统会关闭所有文件描述符）。这说明，即使程序意外退出，也不会给系统留下垃圾，程序重启之后能比较容易地恢复，而不需要重启操作系统（用跨进程的 mutex 就有这个风险）。还有一个好处，既然 port 是独占的，那么可以防止程序重复启动（后面那个进程抢不到 port，自然就没法工作了），造成意料之外的结果。</p>
<p>两个进程通过 TCP 通信，如果一个崩溃了，操作系统会关闭连接，这样另一个进程几乎立刻就能感知，可以快速 failover。当然，应用层的心跳也是必不可少的，我以后在讲服务端的日期与时间处理的时候还会谈到心跳协议的设计。</p>
<p>与其他 IPC 相比，TCP 协议的一个自然好处是“可记录可重现”，tcpdump/Wireshark 是解决两个进程间协议/状态争端的好帮手。</p>
<p>另外，如果网络库带“连接重试”功能的话，我们可以不要求系统里的进程以特定的顺序启动，任何一个进程都能单独重启，这对开发牢靠的分布式系统意义重大。</p>
<p>使用 TCP 这种字节流 (byte stream) 方式通信，会有 marshal/unmarshal 的开销，这要求我们选用合适的消息格式，准确地说是 wire format。这将是我下一篇 blog 的主题，目前我推荐 Google Protocol Buffers。</p>
<p>有人或许会说，具体问题具体分析，如果两个进程在同一台机器，就用共享内存，否则就用 TCP，比如 MS SQL Server 就同时支持这两种通信方式。我问，是否值得为那么一点性能提升而让代码的复杂度大大增加呢？TCP 是字节流协议，只能顺序读取，有写缓冲；共享内存是消息协议，a 进程填好一块内存让 b 进程来读，基本是“停等”方式。要把这两种方式揉到一个程序里，需要建一个抽象层，封装两种 IPC。这会带来不透明性，并且增加测试的复杂度，而且万一通信的某一方崩溃，状态 reconcile 也会比 sockets 麻烦。为我所不取。再说了，你舍得让几万块买来的 SQL Server 和你的程序分享机器资源吗？产品里的数据库服务器往往是独立的高配置服务器，一般不会同时运行其他占资源的程序。</p>
<p>TCP 本身是个数据流协议，除了直接使用它来通信，还可以在此之上构建 RPC/REST/SOAP 之类的上层通信协议，这超过了本文的范围。另外，除了点对点的通信之外，应用级的广播协议也是非常有用的，可以方便地构建可观可控的分布式系统。</p>
<p>本文不具体讲 Reactor 方式下的网络编程，其实这里边有很多值得注意的地方，比如带 back off 的 retry connecting，用优先队列来组织 timer 等等，留作以后分析吧。</p>
<p>6 线程间同步<br />
线程同步的四项原则，按重要性排列：</p>
<p>1. 首要原则是尽量最低限度地共享对象，减少需要同步的场合。一个对象能不暴露给别的线程就不要暴露；如果要暴露，优先考虑 immutable 对象；实在不行才暴露可修改的对象，并用同步措施来充分保护它。</p>
<p>2. 其次是使用高级的并发编程构件，如 TaskQueue、Producer-Consumer Queue、CountDownLatch 等等；</p>
<p>3. 最后不得已必须使用底层同步原语 (primitives) 时，只用非递归的互斥器和条件变量，偶尔用一用读写锁；</p>
<p>4. 不自己编写 lock-free 代码，不去凭空猜测“哪种做法性能会更好”，比如 spin lock vs. mutex。</p>
<p>前面两条很容易理解，这里着重讲一下第 3 条：底层同步原语的使用。</p>
<p>互斥器 (mutex)<br />
互斥器 (mutex) 恐怕是使用得最多的同步原语，粗略地说，它保护了临界区，一个时刻最多只能有一个线程在临界区内活动。（请注意，我谈的是 pthreads 里的 mutex，不是 Windows 里的重量级跨进程 Mutex。）单独使用 mutex 时，我们主要为了保护共享数据。我个人的原则是：</p>
<p>l 用 RAII 手法封装 mutex 的创建、销毁、加锁、解锁这四个操作。</p>
<p>l 只用非递归的 mutex（即不可重入的 mutex）。</p>
<p>l 不手工调用 lock() 和 unlock() 函数，一切交给栈上的 Guard 对象的构造和析构函数负责，Guard 对象的生命期正好等于临界区（分析对象在什么时候析构是 C++ 程序员的基本功）。这样我们保证在同一个函数里加锁和解锁，避免在 foo() 里加锁，然后跑到 bar() 里解锁。</p>
<p>l 在每次构造 Guard 对象的时候，思考一路上（调用栈上）已经持有的锁，防止因加锁顺序不同而导致死锁 (deadlock)。由于 Guard 对象是栈上对象，看函数调用栈就能分析用锁的情况，非常便利。</p>
<p>次要原则有：</p>
<p>l 不使用跨进程的 mutex，进程间通信只用 TCP sockets。</p>
<p>l 加锁解锁在同一个线程，线程 a 不能去 unlock 线程 b 已经锁住的 mutex。（RAII 自动保证）</p>
<p>l 别忘了解锁。（RAII 自动保证）</p>
<p>l 不重复解锁。（RAII 自动保证）</p>
<p>l 必要的时候可以考虑用 PTHREAD_MUTEX_ERRORCHECK 来排错</p>
<p>用 RAII 封装这几个操作是通行的做法，这几乎是 C++ 的标准实践，后面我会给出具体的代码示例，相信大家都已经写过或用过类似的代码了。Java 里的 synchronized 语句和 C# 的 using 语句也有类似的效果，即保证锁的生效期间等于一个作用域，不会因异常而忘记解锁。</p>
<p>Mutex 恐怕是最简单的同步原语，安照上面的几条原则，几乎不可能用错。我自己从来没有违背过这些原则，编码时出现问题都很快能招到并修复。</p>
<p>跑题：非递归的 mutex<br />
谈谈我坚持使用非递归的互斥器的个人想法。</p>
<p>Mutex 分为递归 (recursive) 和非递归(non-recursive)两种，这是 POSIX 的叫法，另外的名字是可重入 (Reentrant) 与非可重入。这两种 mutex 作为线程间 (inter-thread) 的同步工具时没有区别，它们的惟一区别在于：同一个线程可以重复对 recursive mutex 加锁，但是不能重复对 non-recursive mutex 加锁。</p>
<p>首选非递归 mutex，绝对不是为了性能，而是为了体现设计意图。non-recursive 和 recursive 的性能差别其实不大，因为少用一个计数器，前者略快一点点而已。在同一个线程里多次对 non-recursive mutex 加锁会立刻导致死锁，我认为这是它的优点，能帮助我们思考代码对锁的期求，并且及早（在编码阶段）发现问题。</p>
<p>毫无疑问 recursive mutex 使用起来要方便一些，因为不用考虑一个线程会自己把自己给锁死了，我猜这也是 Java 和 Windows 默认提供 recursive mutex 的原因。（Java 语言自带的 intrinsic lock 是可重入的，它的 concurrent 库里提供 ReentrantLock，Windows 的 CRITICAL_SECTION 也是可重入的。似乎它们都不提供轻量级的 non-recursive mutex。）</p>
<p>正因为它方便，recursive mutex 可能会隐藏代码里的一些问题。典型情况是你以为拿到一个锁就能修改对象了，没想到外层代码已经拿到了锁，正在修改（或读取）同一个对象呢。具体的例子：</p>
<p>std::vector foos;</p>
<p>MutexLock mutex;</p>
<p>void post(const Foo&amp; f)</p>
<p>{</p>
<p>MutexLockGuard lock(mutex);</p>
<p>foos.push_back(f);</p>
<p>}</p>
<p>void traverse()</p>
<p>{</p>
<p>MutexLockGuard lock(mutex);</p>
<p>for (auto it = foos.begin(); it != foos.end(); ++it) { // 用了 0x 新写法</p>
<p>it-&gt;doit();</p>
<p>}</p>
<p>}</p>
<p>post() 加锁，然后修改 foos 对象； traverse() 加锁，然后遍历 foos 数组。将来有一天，Foo::doit() 间接调用了 post() （这在逻辑上是错误的），那么会很有戏剧性的：</p>
<p>1. Mutex 是非递归的，于是死锁了。</p>
<p>2. Mutex 是递归的，由于 push_back 可能（但不总是）导致 vector 迭代器失效，程序偶尔会 crash。</p>
<p>这时候就能体现 non-recursive 的优越性：把程序的逻辑错误暴露出来。死锁比较容易 debug，把各个线程的调用栈打出来（(gdb) thread apply all bt），只要每个函数不是特别长，很容易看出来是怎么死的。（另一方面支持了函数不要写过长。）或者可以用 PTHREAD_MUTEX_ERRORCHECK 一下子就能找到错误（前提是 MutexLock 带 debug 选项。）</p>
<p>程序反正要死，不如死得有意义一点，让验尸官的日子好过些。</p>
<p>如果一个函数既可能在已加锁的情况下调用，又可能在未加锁的情况下调用，那么就拆成两个函数：</p>
<p>1. 跟原来的函数同名，函数加锁，转而调用第 2 个函数。</p>
<p>2. 给函数名加上后缀 WithLockHold，不加锁，把原来的函数体搬过来。</p>
<p>就像这样：</p>
<p>void post(const Foo&amp; f)</p>
<p>{</p>
<p>MutexLockGuard lock(mutex);</p>
<p>postWithLockHold(f); // 不用担心开销，编译器会自动内联的</p>
<p>}</p>
<p>// 引入这个函数是为了体现代码作者的意图，尽管 push_back 通常可以手动内联</p>
<p>void postWithLockHold(const Foo&amp; f)</p>
<p>{</p>
<p>foos.push_back(f);</p>
<p>}</p>
<p>这有可能出现两个问题（感谢水木网友 ilovecpp 提出）：a) 误用了加锁版本，死锁了。b) 误用了不加锁版本，数据损坏了。</p>
<p>对于 a)，仿造前面的办法能比较容易地排错。对于 b)，如果 pthreads 提供 isLocked() 就好办，可以写成：</p>
<p>void postWithLockHold(const Foo&amp; f)</p>
<p>{</p>
<p>assert(mutex.isLocked()); // 目前只是一个愿望</p>
<p>// ...</p>
<p>}</p>
<p>另外，WithLockHold 这个显眼的后缀也让程序中的误用容易暴露出来。</p>
<p>C++ 没有 annotation，不能像 Java 那样给 method 或 field 标上 @GuardedBy 注解，需要程序员自己小心在意。虽然这里的办法不能一劳永逸地解决全部多线程错误，但能帮上一点是一点了。</p>
<p>我还没有遇到过需要使用 recursive mutex 的情况，我想将来遇到了都可以借助 wrapper 改用 non-recursive mutex，代码只会更清晰。</p>
<p>=== 回到正题 ===</p>
<p>本文这里只谈了 mutex 本身的正确使用，在 C++ 里多线程编程还会遇到其他很多 race condition，请参考拙作《当析构函数遇到多线程——C++ 中线程安全的对象回调》。请注意这里的 class 命名与那篇文章有所不同。我现在认为 MutexLock 和 MutexLockGuard 是更好的名称。</p>
<p>性能注脚：Linux 的 pthreads mutex 采用 futex 实现，不必每次加锁解锁都陷入系统调用，效率不错。Windows 的 CRITICAL_SECTION 也是类似。</p>
<p>条件变量<br />
条件变量 (condition variable) 顾名思义是一个或多个线程等待某个布尔表达式为真，即等待别的线程“唤醒”它。条件变量的学名叫管程 (monitor)。Java Object 内置的 wait(), notify(), notifyAll() 即是条件变量（它们以容易用错著称）。条件变量只有一种正确使用的方式，对于 wait() 端：</p>
<p>1. 必须与 mutex 一起使用，该布尔表达式的读写需受此 mutex 保护</p>
<p>2. 在 mutex 已上锁的时候才能调用 wait()</p>
<p>3. 把判断布尔条件和 wait() 放到 while 循环中</p>
<p>写成代码是：</p>
<p>MutexLock mutex;</p>
<p>Condition cond(mutex);</p>
<p>std::deque queue;</p>
<p>int dequeue()</p>
<p>{</p>
<p>MutexLockGuard lock(mutex);</p>
<p>while (queue.empty()) { // 必须用循环；必须在判断之后再 wait()</p>
<p>cond.wait(); // 这一步会原子地 unlock mutex 并进入 blocking，不会与 enqueue 死锁</p>
<p>}</p>
<p>assert(!queue.empty());</p>
<p>int top = queue.front();</p>
<p>queue.pop_front();</p>
<p>return top;</p>
<p>}</p>
<p>对于 signal/broadcast 端：</p>
<p>1. 不一定要在 mutex 已上锁的情况下调用 signal （理论上）</p>
<p>2. 在 signal 之前一般要修改布尔表达式</p>
<p>3. 修改布尔表达式通常要用 mutex 保护（至少用作 full memory barrier）</p>
<p>写成代码是：</p>
<p>void enqueue(int x)</p>
<p>{</p>
<p>MutexLockGuard lock(mutex);</p>
<p>queue.push_back(x);</p>
<p>cond.notify();</p>
<p>}</p>
<p>上面的 dequeue/enqueue 实际上实现了一个简单的 unbounded BlockingQueue。</p>
<p>条件变量是非常底层的同步原语，很少直接使用，一般都是用它来实现高层的同步措施，如 BlockingQueue 或 CountDownLatch。</p>
<p>读写锁与其他<br />
读写锁 (Reader-Writer lock)，读写锁是个优秀的抽象，它明确区分了 read 和 write 两种行为。需要注意的是，reader lock 是可重入的，writer lock 是不可重入（包括不可提升 reader lock）的。这正是我说它“优秀”的主要原因。</p>
<p>遇到并发读写，如果条件合适，我会用《借 shared_ptr 实现线程安全的 copy-on-write》介绍的办法，而不用读写锁。当然这不是绝对的。</p>
<p>信号量 (Semaphore)，我没有遇到过需要使用信号量的情况，无从谈及个人经验。</p>
<p>说一句大逆不道的话，如果程序里需要解决如“哲学家就餐”之类的复杂 IPC 问题，我认为应该首先考察几个设计，为什么线程之间会有如此复杂的资源争抢（一个线程要同时抢到两个资源，一个资源可以被两个线程争夺）？能不能把“想吃饭”这个事情专门交给一个为各位哲学家分派餐具的线程来做，然后每个哲学家等在一个简单的 condition variable 上，到时间了有人通知他去吃饭？从哲学上说，教科书上的解决方案是平权，每个哲学家有自己的线程，自己去拿筷子；我宁愿用集权的方式，用一个线程专门管餐具的分配，让其他哲学家线程拿个号等在食堂门口好了。这样不损失多少效率，却让程序简单很多。虽然 Windows 的 WaitForMultipleObjects 让这个问题 trivial 化，在 Linux 下正确模拟 WaitForMultipleObjects 不是普通程序员该干的。</p>
<p>封装 MutexLock、MutexLockGuard 和 Condition<br />
本节把前面用到的 MutexLock、MutexLockGuard、Condition classes 的代码列出来，前面两个 classes 没多大难度，后面那个有点意思。</p>
<p>MutexLock 封装临界区（Critical secion），这是一个简单的资源类，用 RAII 手法 [CCS:13]封装互斥器的创建与销毁。临界区在 Windows 上是 CRITICAL_SECTION，是可重入的；在 Linux 下是 pthread_mutex_t，默认是不可重入的。MutexLock 一般是别的 class 的数据成员。</p>
<p>MutexLockGuard 封装临界区的进入和退出，即加锁和解锁。MutexLockGuard 一般是个栈上对象，它的作用域刚好等于临界区域。</p>
<p>这两个 classes 应该能在纸上默写出来，没有太多需要解释的：</p>
<p>#include</p>
<p>#include</p>
<p>class MutexLock : boost::noncopyable</p>
<p>{</p>
<p>public:</p>
<p>MutexLock() // 为了节省版面，单行函数都没有正确缩进</p>
<p>{ pthread_mutex_init(&amp;mutex_, NULL); }</p>
<p>~MutexLock()</p>
<p>{ pthread_mutex_destroy(&amp;mutex_); }</p>
<p>void lock() // 程序一般不主动调用</p>
<p>{ pthread_mutex_lock(&amp;mutex_); }</p>
<p>void unlock() // 程序一般不主动调用</p>
<p>{ pthread_mutex_unlock(&amp;mutex_); }</p>
<p>pthread_mutex_t* getPthreadMutex() // 仅供 Condition 调用，严禁自己调用</p>
<p>{ return &amp;mutex_; }</p>
<p>private:</p>
<p>pthread_mutex_t mutex_;</p>
<p>};</p>
<p>class MutexLockGuard : boost::noncopyable</p>
<p>{</p>
<p>public:</p>
<p>explicit MutexLockGuard(MutexLock&amp; mutex) : mutex_(mutex)</p>
<p>{ mutex_.lock(); }</p>
<p>~MutexLockGuard()</p>
<p>{ mutex_.unlock(); }</p>
<p>private:</p>
<p>MutexLock&amp; mutex_;</p>
<p>};</p>
<p>#define MutexLockGuard(x) static_assert(false, "missing mutex guard var name")</p>
<p>注意代码的最后一行定义了一个宏，这个宏的作用是防止程序里出现如下错误：</p>
<p>void doit()</p>
<p>{</p>
<p>MutexLockGuard(mutex); // 没有变量名，产生一个临时对象又马上销毁了，没有锁住临界区</p>
<p>// 正确写法是 MutexLockGuard lock(mutex);</p>
<p>// 临界区</p>
<p>}</p>
<p>这里 MutexLock 没有提供 trylock() 函数，因为我没有用过它，我想不出什么时候程序需要“试着去锁一锁”，或许我写过的代码太简单了。</p>
<p>我见过有人把 MutexLockGuard 写成 template，我没有这么做是因为它的模板类型参数只有 MutexLock 一种可能，没有必要随意增加灵活性，于是我人肉把模板具现化 (instantiate) 了。此外一种更激进的写法是，把 lock/unlock 放到 private 区，然后把 Guard 设为 MutexLock 的 friend，我认为在注释里告知程序员即可，另外 check-in 之前的 code review 也很容易发现误用的情况 (grep getPthreadMutex)。</p>
<p>这段代码没有达到工业强度：a) Mutex 创建为 PTHREAD_MUTEX_DEFAULT 类型，而不是我们预想的 PTHREAD_MUTEX_NORMAL 类型（实际上这二者很可能是等同的），严格的做法是用 mutexattr 来显示指定 mutex 的类型。b) 没有检查返回值。这里不能用 assert 检查返回值，因为 assert 在 release build 里是空语句。我们检查返回值的意义在于防止 ENOMEM 之类的资源不足情况，这一般只可能在负载很重的产品程序中出现。一旦出现这种错误，程序必须立刻清理现场并主动退出，否则会莫名其妙地崩溃，给事后调查造成困难。这里我们需要 non-debug 的 assert，或许 google-glog 的 CHECK() 是个不错的思路。</p>
<p>以上两点改进留作练习。</p>
<p>Condition class 的实现有点意思。</p>
<p>Pthreads condition variable 允许在 wait() 的时候指定 mutex，但是我想不出什么理由一个 condition variable 会和不同的 mutex 配合使用。Java 的 intrinsic condition 和 Conditon class 都不支持这么做，因此我觉得可以放弃这一灵活性，老老实实一对一好了。相反 boost::thread 的 condition_varianle 是在 wait 的时候指定 mutex，请参观其同步原语的庞杂设计：</p>
<p>l Concept 有四种 Lockable, TimedLockable, SharedLockable, UpgradeLockable.</p>
<p>l Lock 有五六种： lock_guard, unique_lock, shared_lock, upgrade_lock, upgrade_to_unique_lock, scoped_try_lock.</p>
<p>l Mutex 有七种：mutex, try_mutex, timed_mutex, recursive_mutex, recursive_try_mutex, recursive_timed_mutex, shared_mutex.</p>
<p>恕我愚钝，见到 boost::thread 这样如 Rube Goldberg Machine 一样“灵活”的库我只得三揖绕道而行。这些 class 名字也很无厘头，为什么不老老实实用 reader_writer_lock 这样的通俗名字呢？非得增加精神负担，自己发明新名字。我不愿为这样的灵活性付出代价，宁愿自己做几个简简单单的一看就明白的 classes 来用，这种简单的几行代码的轮子造造也无妨。提供灵活性固然是本事，然而在不需要灵活性的地方把代码写死，更需要大智慧。</p>
<p>下面这个 Condition 简单地封装了 pthread cond var，用起来也容易，见本节前面的例子。这里我用 notify/notifyAll 作为函数名，因为 signal 有别的含义，C++ 里的 signal/slot，C 里的 signal handler 等等。就别 overload 这个术语了。</p>
<p>class Condition : boost::noncopyable</p>
<p>{</p>
<p>public:</p>
<p>Condition(MutexLock&amp; mutex) : mutex_(mutex)</p>
<p>{ pthread_cond_init(&amp;pcond_, NULL); }</p>
<p>~Condition()</p>
<p>{ pthread_cond_destroy(&amp;pcond_); }</p>
<p>void wait()</p>
<p>{ pthread_cond_wait(&amp;pcond_, mutex_.getPthreadMutex()); }</p>
<p>void notify()</p>
<p>{ pthread_cond_signal(&amp;pcond_); }</p>
<p>void notifyAll()</p>
<p>{ pthread_cond_broadcast(&amp;pcond_); }</p>
<p>private:</p>
<p>MutexLock&amp; mutex_;</p>
<p>pthread_cond_t pcond_;</p>
<p>};</p>
<p>如果一个 class 要包含 MutexLock 和 Condition，请注意它们的声明顺序和初始化顺序，mutex_ 应先于 condition_ 构造，并作为后者的构造参数：</p>
<p>class CountDownLatch</p>
<p>{</p>
<p>public:</p>
<p>CountDownLatch(int count)</p>
<p>: count_(count),</p>
<p>mutex_(),</p>
<p>condition_(mutex_)</p>
<p>{ }</p>
<p>private:</p>
<p>int count_;</p>
<p>MutexLock mutex_; // 顺序很重要</p>
<p>Condition condition_;</p>
<p>};</p>
<p>请允许我再次强调，虽然本节花了大量篇幅介绍如何正确使用 mutex 和 condition variable，但并不代表我鼓励到处使用它们。这两者都是非常底层的同步原语，主要用来实现更高级的并发编程工具，一个多线程程序里如果大量使用 mutex 和 condition variable 来同步，基本跟用铅笔刀锯大树（孟岩语）没啥区别。</p>
<p>线程安全的 Singleton 实现<br />
研究 Signleton 的线程安全实现的历史你会发现很多有意思的事情，一度人们认为 Double checked locking 是王道，兼顾了效率与正确性。后来有神牛指出由于乱序执行的影响，DCL 是靠不住的。（这个又让我想起了 SQL 注入，十年前用字符串拼接出 SQL 语句是 Web 开发的通行做法，直到有一天有人利用这个漏洞越权获得并修改网站数据，人们才幡然醒悟，赶紧修补。）Java 开发者还算幸运，可以借助内部静态类的装载来实现。C++ 就比较惨，要么次次锁，要么 eager initialize、或者动用 memory barrier 这样的大杀器。接下来 Java 5 修订了内存模型，并增强了 volatile 的语义，这下 DCL (with volatile) 又是安全的了。然而 C++ 的内存模型还在修订中，C++ 的 volatile 目前还不能（将来也难说）保证 DCL 的正确性（只在 VS2005+ 上有效）。</p>
<p>其实没那么麻烦，在实践中用 pthread once 就行：</p>
<p>#include</p>
<p>template</p>
<p>class Singleton : boost::noncopyable</p>
<p>{</p>
<p>public:</p>
<p>static T&amp; instance()</p>
<p>{</p>
<p>pthread_once(&amp;ponce_, &amp;Singleton::init);</p>
<p>return *value_;</p>
<p>}</p>
<p>static void init()</p>
<p>{</p>
<p>value_ = new T();</p>
<p>}</p>
<p>private:</p>
<p>static pthread_once_t ponce_;</p>
<p>static T* value_;</p>
<p>};</p>
<p>template</p>
<p>pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT;</p>
<p>template</p>
<p>T* Singleton::value_ = NULL;</p>
<p>上面这个 Singleton 没有任何花哨的技巧，用 pthread_once_t 来保证 lazy-initialization 的线程安全。使用方法也很简单：</p>
<p>Foo&amp; foo = Singleton::instance();</p>
<p>当然，这个 Singleton 没有考虑对象的销毁，在服务器程序里，这不是一个问题，因为当程序退出的时候自然就释放所有资源了（前提是程序里不使用不能由操作系统自动关闭的资源，比如跨进程的 Mutex）。另外，这个 Singleton 只能调用默认构造函数，如果用户想要指定 T 的构造方式，我们可以用模板特化 (template specialization) 技术来提供一个定制点，这需要引入另一层间接。</p>
<p>归纳<br />
l 进程间通信首选 TCP sockets</p>
<p>l 线程同步的四项原则</p>
<p>l 使用互斥器的条件变量的惯用手法 (idiom)，关键是 RAII</p>
<p>用好这几样东西，基本上能应付多线程服务端开发的各种场合，只是或许有人会觉得性能没有发挥到极致。我认为，先把程序写正确了，再考虑性能优化，这在多线程下任然成立。让一个正确的程序变快，远比“让一个快的程序变正确”容易得多。</p>
<p>7 总结<br />
在现代的多核计算背景下，线程是不可避免的。多线程编程是一项重要的个人技能，不能因为它难就本能地排斥，现在的软件开发比起 10 年 20 年前已经难了不知道多少倍。掌握多线程编程，才能更理智地选择用还是不用多线程，因为你能预估多线程实现的难度与收益，在一开始做出正确的选择。要知道把一个单线程程序改成多线程的，往往比重头实现一个多线程的程序更难。</p>
<p>掌握同步原语和它们的适用场合时多线程编程的基本功。以我的经验，熟练使用文中提到的同步原语，就能比较容易地编写线程安全的程序。本文没有考虑 signal 对多线程编程的影响，Unix 的 signal 在多线程下的行为比较复杂，一般要靠底层的网络库 (如 Reactor) 加以屏蔽，避免干扰上层应用程序的开发。</p>
<p>通篇来看，“效率”并不是我的主要考虑点，a) TCP 不是效率最高的 IPC，b) 我提倡正确加锁而不是自己编写 lock-free 算法（使用原子操作除外）。在程序的复杂度和性能之前取得平衡，并经考虑未来两三年扩容的可能（无论是 CPU 变快、核数变多，还是机器数量增加，网络升级）。下一篇“多线程编程的反模式”会考察伸缩性方面的常见错误，我认为在分布式系统中，伸缩性 (scalability) 比单机的性能优化更值得投入精力。</p>
<p>这篇文章记录了我目前对多线程编程的理解，用文中介绍的手法，我能解决自己面临的全部多线程编程任务。如果文章的观点与您不合，比如您使用了我没有推荐使用的技术或手法（共享内存、信号量等等），只要您理由充分，但行无妨。</p>
<p>这篇文章本来还有两节“多线程编程的反模式”与“多线程的应用场景”，考虑到字数已经超过一万了，且听下回分解吧 <img src='http://software.intel.com/zh-cn/blogs/wordpress/wp-includes/images/smilies/icon_smile.gif' alt=':-)' class='wp-smiley' /> </p>
<p>后文预览：Sleep 反模式<br />
我认为 sleep 只能出现在测试代码中，比如写单元测试的时候。（涉及时间的单元测试不那么好写，短的如一两秒钟可以用 sleep，长的如一小时一天得想其他办法，比如把算法提出来并把时间注入进去。）产品代码中线程的等待可分为两种：一种是无所事事的时候（要么等在 select/poll/epoll 上。要么等在 condition variable 上，等待 BlockingQueue /CountDownLatch 亦可归入此类），一种是等着进入临界区（等在 mutex 上）以便继续处理。在程序的正常执行中，如果需要等待一段时间，应该往 event loop 里注册一个 timer，然后在 timer 的回调函数里接着干活，因为线程是个珍贵的共享资源，不能轻易浪费。如果多线程的安全性和效率要靠代码主动调用 sleep 来保证，这是设计出了问题。等待一个事件发生，正确的做法是用 select 或 condition variable 或（更理想地）高层同步工具。当然，在 GUI 编程中会有主动让出 CPU 的做法，比如调用 sleep(0) 来实现 yield。</p>
]]></content:encoded>
			<wfw:commentRss>http://software.intel.com/zh-cn/blogs/2010/03/24/400003435/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

