西西軟件園多重安全檢測(cè)下載網(wǎng)站、值得信賴的軟件下載站!
軟件
軟件
文章
搜索

首頁(yè)西西教程網(wǎng)站推廣 → 多線程服務(wù)器的常用編程模型

多線程服務(wù)器的常用編程模型

相關(guān)軟件相關(guān)文章發(fā)表評(píng)論 來(lái)源:本站原創(chuàng)時(shí)間:2010/2/13 12:26:23字體大小:A-A+

作者:陳碩點(diǎn)擊:736次評(píng)論:0次標(biāo)簽: 服務(wù)器

飛信2017V5.6.8860.0 官方正式版
  • 類(lèi)型:聊天其它大。69.1M語(yǔ)言:中文 評(píng)分:9.6
  • 標(biāo)簽:
立即下載

本文主要講我個(gè)人在多線程開(kāi)發(fā)方面的一些粗淺經(jīng)驗(yàn)?偨Y(jié)了一兩種常用的線程模型,歸納了進(jìn)程間通訊與線程同步的最佳實(shí)踐,以期用簡(jiǎn)單規(guī)范的方式開(kāi)發(fā)多線程程序。

文中的“多線程服務(wù)器”是指運(yùn)行在 Linux 操作系統(tǒng)上的獨(dú)占式網(wǎng)絡(luò)應(yīng)用程序。硬件平臺(tái)為 Intel x64 系列的多核 CPU,單路或雙路 SMP 服務(wù)器(每臺(tái)機(jī)器一共擁有四個(gè)核或八個(gè)核,十幾 GB 內(nèi)存),機(jī)器之間用百兆或千兆以太網(wǎng)連接。這大概是目前民用 PC 服務(wù)器的主流配置。

本文不涉及 Windows 系統(tǒng),不涉及人機(jī)交互界面(無(wú)論命令行或圖形);不考慮文件讀寫(xiě)(往磁盤(pán)寫(xiě) log 除外),不考慮數(shù)據(jù)庫(kù)操作,不考慮 Web 應(yīng)用;不考慮低端的單核主機(jī)或嵌入式系統(tǒng),不考慮手持式設(shè)備,不考慮專門(mén)的網(wǎng)絡(luò)設(shè)備,不考慮高端的 >=32 核 Unix 主機(jī);只考慮 TCP,不考慮 UDP,也不考慮除了局域網(wǎng)絡(luò)之外的其他數(shù)據(jù)收發(fā)方式(例如串并口、USB口、數(shù)據(jù)采集板卡、實(shí)時(shí)控制等)。

有了以上這么多限制,那么我將要談的“網(wǎng)絡(luò)應(yīng)用程序”的基本功能可以歸納為“收到數(shù)據(jù),算一算,再發(fā)出去”。在這個(gè)簡(jiǎn)化了的模型里,似乎看不出用多線程的必要,單線程應(yīng)該也能做得很好。“為什么需要寫(xiě)多線程程序”這個(gè)問(wèn)題容易引發(fā)口水戰(zhàn),我放到另一篇博客里討論。請(qǐng)?jiān)试S我先假定“多線程編程”這一背景。

“服務(wù)器”這個(gè)詞有時(shí)指程序,有時(shí)指進(jìn)程,有時(shí)指硬件(無(wú)論虛擬的或真實(shí)的),請(qǐng)注意按上下文區(qū)分。另外,本文不考慮虛擬化的場(chǎng)景,當(dāng)我說(shuō)“兩個(gè)進(jìn)程不在同一臺(tái)機(jī)器上”,指的是邏輯上不在同一個(gè)操作系統(tǒng)里運(yùn)行,雖然物理上可能位于同一機(jī)器虛擬出來(lái)的兩臺(tái)“虛擬機(jī)”上。

本文假定讀者已經(jīng)有多線程編程的知識(shí)與經(jīng)驗(yàn),這不是一篇入門(mén)教程。

本文承蒙 Milo Yip 先生審讀,在此深表謝意。當(dāng)然,文中任何錯(cuò)誤責(zé)任均在我。

1 進(jìn)程與線程
“進(jìn)程/process”是操作里最重要的兩個(gè)概念之一(另一個(gè)是文件),粗略地講,一個(gè)進(jìn)程是“內(nèi)存中正在運(yùn)行的程序”。本文的進(jìn)程指的是 Linux 操作系統(tǒng)通過(guò) fork() 系統(tǒng)調(diào)用產(chǎn)生的那個(gè)東西,或者 Windows 下 CreateProcess() 的產(chǎn)物,不是 Erlang 里的那種輕量級(jí)進(jìn)程。

每個(gè)進(jìn)程有自己獨(dú)立的地址空間 (address space),“在同一個(gè)進(jìn)程”還是“不在同一個(gè)進(jìn)程”是系統(tǒng)功能劃分的重要決策點(diǎn)。Erlang 書(shū)把“進(jìn)程”比喻為“人”,我覺(jué)得十分精當(dāng),為我們提供了一個(gè)思考的框架。

每個(gè)人有自己的記憶 (memory),人與人通過(guò)談話(消息傳遞)來(lái)交流,談話既可以是面談(同一臺(tái)服務(wù)器),也可以在電話里談(不同的服務(wù)器,有網(wǎng)絡(luò)通信)。面談和電話談的區(qū)別在于,面談可以立即知道對(duì)方死否死了(crash, SIGCHLD),而電話談只能通過(guò)周期性的心跳來(lái)判斷對(duì)方是否還活著。

有了這些比喻,設(shè)計(jì)分布式系統(tǒng)時(shí)可以采取“角色扮演”,團(tuán)隊(duì)里的幾個(gè)人各自扮演一個(gè)進(jìn)程,人的角色由進(jìn)程的代碼決定(管登陸的、管消息分發(fā)的、管買(mǎi)賣(mài)的等等)。每個(gè)人有自己的記憶,但不知道別人的記憶,要想知道別人的看法,只能通過(guò)交談。(暫不考慮共享內(nèi)存這種 IPC。)然后就可以思考容錯(cuò)(萬(wàn)一有人突然死了)、擴(kuò)容(新人中途加進(jìn)來(lái))、負(fù)載均衡(把 a 的活兒挪給 b 做)、退休(a 要修復(fù) bug,先別給他派新活兒,等他做完手上的事情就把他重啟)等等各種場(chǎng)景,十分便利。

“線程”這個(gè)概念大概是在 1993 年以后才慢慢流行起來(lái)的,距今不過(guò)十余年,比不得有 40 年光輝歷史的 Unix 操作系統(tǒng)。線程的出現(xiàn)給 Unix 添了不少亂,很多 C 庫(kù)函數(shù)(strtok(), ctime())不是線程安全的,需要重新定義;signal 的語(yǔ)意也大為復(fù)雜化。據(jù)我所知,最早支持多線程編程的(民用)操作系統(tǒng)是 Solaris 2.2 和 Windows NT 3.1,它們均發(fā)布于 1993 年。隨后在 1995 年,POSIX threads 標(biāo)準(zhǔn)確立。

線程的特點(diǎn)是共享地址空間,從而可以高效地共享數(shù)據(jù)。一臺(tái)機(jī)器上的多個(gè)進(jìn)程能高效地共享代碼段(操作系統(tǒng)可以映射為同樣的物理內(nèi)存),但不能共享數(shù)據(jù)。如果多個(gè)進(jìn)程大量共享內(nèi)存,等于是把多進(jìn)程程序當(dāng)成多線程來(lái)寫(xiě),掩耳盜鈴。

“多線程”的價(jià)值,我認(rèn)為是為了更好地發(fā)揮對(duì)稱多路處理 (SMP) 的效能。在 SMP 之前,多線程沒(méi)有多大價(jià)值。Alan Cox 說(shuō)過(guò) A computer is a state machine. Threads are for people who can't program state machines. (計(jì)算機(jī)是一臺(tái)狀態(tài)機(jī)。線程是給那些不能編寫(xiě)狀態(tài)機(jī)程序的人準(zhǔn)備的。)如果只有一個(gè)執(zhí)行單元,一個(gè) CPU,那么確實(shí)如 Alan Cox 所說(shuō),按狀態(tài)機(jī)的思路去寫(xiě)程序是最高效的,這正好也是下一節(jié)展示的編程模型。

2 典型的單線程服務(wù)器編程模型
UNP3e 對(duì)此有很好的總結(jié)(第 6 章:IO 模型,第 30 章:客戶端/服務(wù)器設(shè)計(jì)范式),這里不再贅述。據(jù)我了解,在高性能的網(wǎng)絡(luò)程序中,使用得最為廣泛的恐怕要數(shù)“non-blocking IO + IO multiplexing”這種模型,即 Reactor 模式,我知道的有:

lighttpd,單線程服務(wù)器。(nginx 估計(jì)與之類(lèi)似,待查)
libevent/libev
ACE,Poco C++ libraries(QT 待查)
Java NIO (Selector/SelectableChannel), Apache Mina, Netty (Java)
POE (Perl)
Twisted (Python)
相反,boost::asio 和 Windows I/O Completion Ports 實(shí)現(xiàn)了 Proactor 模式,應(yīng)用面似乎要窄一些。當(dāng)然,ACE 也實(shí)現(xiàn)了 Proactor 模式,不表。

在“non-blocking IO + IO multiplexing”這種模型下,程序的基本結(jié)構(gòu)是一個(gè)事件循環(huán) (event loop):(代碼僅為示意,沒(méi)有完整考慮各種情況)

01 while (!done)

02 {

03 int timeout_ms = max(1000, getNextTimedCallback());

04 int retval = ::poll(fds, nfds, timeout_ms);

05 if (retval < 0) {

06 處理錯(cuò)誤

07 } else {

08 處理到期的 timers

09 if (retval > 0) {

10 處理 IO 事件

11 }

12 }

13 }

當(dāng)然,select(2)/poll(2) 有很多不足,Linux 下可替換為 epoll,其他操作系統(tǒng)也有對(duì)應(yīng)的高性能替代品(搜 c10k problem)。

Reactor 模型的優(yōu)點(diǎn)很明顯,編程簡(jiǎn)單,效率也不錯(cuò)。不僅網(wǎng)絡(luò)讀寫(xiě)可以用,連接的建立(connect/accept)甚至 DNS 解析都可以用非阻塞方式進(jìn)行,以提高并發(fā)度和吞吐量 (throughput)。對(duì)于 IO 密集的應(yīng)用是個(gè)不錯(cuò)的選擇,Lighttpd 即是這樣,它內(nèi)部的 fdevent 結(jié)構(gòu)十分精妙,值得學(xué)習(xí)。(這里且不考慮用阻塞 IO 這種次優(yōu)的方案。)

當(dāng)然,實(shí)現(xiàn)一個(gè)優(yōu)質(zhì)的 Reactor 不是那么容易,我也沒(méi)有用過(guò)坊間開(kāi)源的庫(kù),這里就不推薦了。

3 典型的多線程服務(wù)器的線程模型
這方面我能找到的文獻(xiàn)不多,大概有這么幾種:

1. 每個(gè)請(qǐng)求創(chuàng)建一個(gè)線程,使用阻塞式 IO 操作。在 Java 1.4 引入 NIO 之前,這是 Java 網(wǎng)絡(luò)編程的推薦做法。可惜伸縮性不佳。

2. 使用線程池,同樣使用阻塞式 IO 操作。與 1 相比,這是提高性能的措施。

3. 使用 non-blocking IO + IO multiplexing。即 Java NIO 的方式。

4. Leader/Follower 等高級(jí)模式

在默認(rèn)情況下,我會(huì)使用第 3 種,即 non-blocking IO + one loop per thread 模式。
http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#THREADS_AND_COROUTINES

One loop per thread
此種模型下,程序里的每個(gè) IO 線程有一個(gè) event loop (或者叫 Reactor),用于處理讀寫(xiě)和定時(shí)事件(無(wú)論周期性的還是單次的),代碼框架跟第 2 節(jié)一樣。

這種方式的好處是:

線程數(shù)目基本固定,可以在程序啟動(dòng)的時(shí)候設(shè)置,不會(huì)頻繁創(chuàng)建與銷(xiāo)毀。
可以很方便地在線程間調(diào)配負(fù)載。
event loop 代表了線程的主循環(huán),需要讓哪個(gè)線程干活,就把 timer 或 IO channel (TCP connection) 注冊(cè)到那個(gè)線程的 loop 里即可。對(duì)實(shí)時(shí)性有要求的 connection 可以單獨(dú)用一個(gè)線程;數(shù)據(jù)量大的 connection 可以獨(dú)占一個(gè)線程,并把數(shù)據(jù)處理任務(wù)分?jǐn)偟搅韼讉(gè)線程中;其他次要的輔助性 connections 可以共享一個(gè)線程。

對(duì)于 non-trivial 的服務(wù)端程序,一般會(huì)采用 non-blocking IO + IO multiplexing,每個(gè) connection/acceptor 都會(huì)注冊(cè)到某個(gè) Reactor 上,程序里有多個(gè) Reactor,每個(gè)線程至多有一個(gè) Reactor。

多線程程序?qū)?Reactor 提出了更高的要求,那就是“線程安全”。要允許一個(gè)線程往別的線程的 loop 里塞東西,這個(gè) loop 必須得是線程安全的。

線程池
不過(guò),對(duì)于沒(méi)有 IO 光有計(jì)算任務(wù)的線程,使用 event loop 有點(diǎn)浪費(fèi),我會(huì)用有一種補(bǔ)充方案,即用 blocking queue 實(shí)現(xiàn)的任務(wù)隊(duì)列(TaskQueue):

1 blocking_queue<boost::function<void()> > taskQueue; // 線程安全的阻塞隊(duì)列

2

3 void worker_thread()

4 {

5 while (!quit) {

6 boost::function<void()> task = taskQueue.take(); // this blocks

7 task(); // 在產(chǎn)品代碼中需要考慮異常處理

8 }

9 }

用這種方式實(shí)現(xiàn)線程池特別容易:

1 // 啟動(dòng)容量為 N 的線程池:

2 int N = num_of_computing_threads;

3 for (int i = 0; i < N; ++i) {

4 create_thread(&worker_thread); // 偽代碼:?jiǎn)?dòng)線程

5 }

使用起來(lái)也很簡(jiǎn)單:

1 boost::function<void()> task = boost::bind(&Foo::calc, this);

2 taskQueue.post(task);

上面十幾行代碼就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的固定數(shù)目的線程池,功能大概相當(dāng)于 Java 5 的 ThreadPoolExecutor 的某種“配置”。當(dāng)然,在真實(shí)的項(xiàng)目中,這些代碼都應(yīng)該封裝到一個(gè) class 中,而不是使用全局對(duì)象。另外需要注意一點(diǎn):Foo 對(duì)象的生命期,我的另一篇博客《當(dāng)析構(gòu)函數(shù)遇到多線程——C++ 中線程安全的對(duì)象回調(diào)》詳細(xì)討論了這個(gè)問(wèn)題
http://blog.csdn.net/Solstice/archive/2010/01/22/5238671.aspx

除了任務(wù)隊(duì)列,還可以用 blocking_queue<T> 實(shí)現(xiàn)數(shù)據(jù)的消費(fèi)者-生產(chǎn)者隊(duì)列,即 T 的是數(shù)據(jù)類(lèi)型而非函數(shù)對(duì)象,queue 的消費(fèi)者(s)從中拿到數(shù)據(jù)進(jìn)行處理。這樣做比 task queue 更加 specific 一些。

blocking_queue<T> 是多線程編程的利器,它的實(shí)現(xiàn)可參照 Java 5 util.concurrent 里的 (Array|Linked)BlockingQueue,通常 C++ 可以用 deque 來(lái)做底層的容器。Java 5 里的代碼可讀性很高,代碼的基本結(jié)構(gòu)和教科書(shū)一致(1 個(gè) mutex,2 個(gè) condition variables),健壯性要高得多。如果不想自己實(shí)現(xiàn),用現(xiàn)成的庫(kù)更好。(我沒(méi)有用過(guò)免費(fèi)的庫(kù),這里就不亂推薦了,有興趣的同學(xué)可以試試 Intel Threading Building Blocks 里的 concurrent_queue<T>。)

歸納
總結(jié)起來(lái),我推薦的多線程服務(wù)端編程模式為:event loop per thread + thread pool。

event loop 用作 non-blocking IO 和定時(shí)器。
thread pool 用來(lái)做計(jì)算,具體可以是任務(wù)隊(duì)列或消費(fèi)者-生產(chǎn)者隊(duì)列。
以這種方式寫(xiě)服務(wù)器程序,需要一個(gè)優(yōu)質(zhì)的基于 Reactor 模式的網(wǎng)絡(luò)庫(kù)來(lái)支撐,我只用過(guò) in-house 的產(chǎn)品,無(wú)從比較并推薦市面上常見(jiàn)的 C++ 網(wǎng)絡(luò)庫(kù),抱歉。

程序里具體用幾個(gè) loop、線程池的大小等參數(shù)需要根據(jù)應(yīng)用來(lái)設(shè)定,基本的原則是“阻抗匹配”,使得 CPU 和 IO 都能高效地運(yùn)作,具體的考慮點(diǎn)容我以后再談。

這里沒(méi)有談線程的退出,留待下一篇 blog“多線程編程反模式”探討。

此外,程序里或許還有個(gè)別執(zhí)行特殊任務(wù)的線程,比如 logging,這對(duì)應(yīng)用程序來(lái)說(shuō)基本是不可見(jiàn)的,但是在分配資源(CPU 和 IO)的時(shí)候要算進(jìn)去,以免高估了系統(tǒng)的容量。

4 進(jìn)程間通信與線程間通信
Linux 下進(jìn)程間通信 (IPC) 的方式數(shù)不勝數(shù),光 UNPv2 列出的就有:pipe、FIFO、POSIX 消息隊(duì)列、共享內(nèi)存、信號(hào) (signals) 等等,更不必說(shuō) Sockets 了。同步原語(yǔ) (synchronization primitives) 也很多,互斥器 (mutex)、條件變量 (condition variable)、讀寫(xiě)鎖 (reader-writer lock)、文件鎖 (Record locking)、信號(hào)量 (Semaphore) 等等。

如何選擇呢?根據(jù)我的個(gè)人經(jīng)驗(yàn),貴精不貴多,認(rèn)真挑選三四樣?xùn)|西就能完全滿足我的工作需要,而且每樣我都能用得很熟,,不容易犯錯(cuò)。

5 進(jìn)程間通信
進(jìn)程間通信我首選 Sockets(主要指 TCP,我沒(méi)有用過(guò) UDP,也不考慮 Unix domain 協(xié)議),其最大的好處在于:可以跨主機(jī),具有伸縮性。反正都是多進(jìn)程了,如果一臺(tái)機(jī)器處理能力不夠,很自然地就能用多臺(tái)機(jī)器來(lái)處理。把進(jìn)程分散到同一局域網(wǎng)的多臺(tái)機(jī)器上,程序改改 host:port 配置就能繼續(xù)用。相反,前面列出的其他 IPC 都不能跨機(jī)器(比如共享內(nèi)存效率最高,但再怎么著也不能高效地共享兩臺(tái)機(jī)器的內(nèi)存),限制了 scalability。

在編程上,TCP sockets 和 pipe 都是一個(gè)文件描述符,用來(lái)收發(fā)字節(jié)流,都可以 read/write/fcntl/select/poll 等。不同的是,TCP 是雙向的,pipe 是單向的 (Linux),進(jìn)程間雙向通訊還得開(kāi)兩個(gè)文件描述符,不方便;而且進(jìn)程要有父子關(guān)系才能用 pipe,這些都限制了 pipe 的使用。在收發(fā)字節(jié)流這一通訊模型下,沒(méi)有比 sockets/TCP 更自然的 IPC 了。當(dāng)然,pipe 也有一個(gè)經(jīng)典應(yīng)用場(chǎng)景,那就是寫(xiě) Reactor/Selector 時(shí)用來(lái)異步喚醒 select (或等價(jià)的 poll/epoll) 調(diào)用(Sun JVM 在 Linux 就是這么做的)。

TCP port 是由一個(gè)進(jìn)程獨(dú)占,且操作系統(tǒng)會(huì)自動(dòng)回收(listening port 和已建立連接的 TCP socket 都是文件描述符,在進(jìn)程結(jié)束時(shí)操作系統(tǒng)會(huì)關(guān)閉所有文件描述符)。這說(shuō)明,即使程序意外退出,也不會(huì)給系統(tǒng)留下垃圾,程序重啟之后能比較容易地恢復(fù),而不需要重啟操作系統(tǒng)(用跨進(jìn)程的 mutex 就有這個(gè)風(fēng)險(xiǎn))。還有一個(gè)好處,既然 port 是獨(dú)占的,那么可以防止程序重復(fù)啟動(dòng)(后面那個(gè)進(jìn)程搶不到 port,自然就沒(méi)法工作了),造成意料之外的結(jié)果。

兩個(gè)進(jìn)程通過(guò) TCP 通信,如果一個(gè)崩潰了,操作系統(tǒng)會(huì)關(guān)閉連接,這樣另一個(gè)進(jìn)程幾乎立刻就能感知,可以快速 failover。當(dāng)然,應(yīng)用層的心跳也是必不可少的,我以后在講服務(wù)端的日期與時(shí)間處理的時(shí)候還會(huì)談到心跳協(xié)議的設(shè)計(jì)。

與其他 IPC 相比,TCP 協(xié)議的一個(gè)自然好處是“可記錄可重現(xiàn)”,tcpdump/Wireshark 是解決兩個(gè)進(jìn)程間協(xié)議/狀態(tài)爭(zhēng)端的好幫手。

另外,如果網(wǎng)絡(luò)庫(kù)帶“連接重試”功能的話,我們可以不要求系統(tǒng)里的進(jìn)程以特定的順序啟動(dòng),任何一個(gè)進(jìn)程都能單獨(dú)重啟,這對(duì)開(kāi)發(fā)牢靠的分布式系統(tǒng)意義重大。

使用 TCP 這種字節(jié)流 (byte stream) 方式通信,會(huì)有 marshal/unmarshal 的開(kāi)銷(xiāo),這要求我們選用合適的消息格式,準(zhǔn)確地說(shuō)是 wire format。這將是我下一篇 blog 的主題,目前我推薦 Google Protocol Buffers。

有人或許會(huì)說(shuō),具體問(wèn)題具體分析,如果兩個(gè)進(jìn)程在同一臺(tái)機(jī)器,就用共享內(nèi)存,否則就用 TCP,比如 MS SQL Server 就同時(shí)支持這兩種通信方式。我問(wèn),是否值得為那么一點(diǎn)性能提升而讓代碼的復(fù)雜度大大增加呢?TCP 是字節(jié)流協(xié)議,只能順序讀取,有寫(xiě)緩沖;共享內(nèi)存是消息協(xié)議,a 進(jìn)程填好一塊內(nèi)存讓 b 進(jìn)程來(lái)讀,基本是“停等”方式。要把這兩種方式揉到一個(gè)程序里,需要建一個(gè)抽象層,封裝兩種 IPC。這會(huì)帶來(lái)不透明性,并且增加測(cè)試的復(fù)雜度,而且萬(wàn)一通信的某一方崩潰,狀態(tài) reconcile 也會(huì)比 sockets 麻煩。為我所不取。再說(shuō)了,你舍得讓幾萬(wàn)塊買(mǎi)來(lái)的 SQL Server 和你的程序分享機(jī)器資源嗎?產(chǎn)品里的數(shù)據(jù)庫(kù)服務(wù)器往往是獨(dú)立的高配置服務(wù)器,一般不會(huì)同時(shí)運(yùn)行其他占資源的程序。

TCP 本身是個(gè)數(shù)據(jù)流協(xié)議,除了直接使用它來(lái)通信,還可以在此之上構(gòu)建 RPC/REST/SOAP 之類(lèi)的上層通信協(xié)議,這超過(guò)了本文的范圍。另外,除了點(diǎn)對(duì)點(diǎn)的通信之外,應(yīng)用級(jí)的廣播協(xié)議也是非常有用的,可以方便地構(gòu)建可觀可控的分布式系統(tǒng)。

本文不具體講 Reactor 方式下的網(wǎng)絡(luò)編程,其實(shí)這里邊有很多值得注意的地方,比如帶 back off 的 retry connecting,用優(yōu)先隊(duì)列來(lái)組織 timer 等等,留作以后分析吧。

6 線程間同步
線程同步的四項(xiàng)原則,按重要性排列:

1. 首要原則是盡量最低限度地共享對(duì)象,減少需要同步的場(chǎng)合。一個(gè)對(duì)象能不暴露給別的線程就不要暴露;如果要暴露,優(yōu)先考慮 immutable 對(duì)象;實(shí)在不行才暴露可修改的對(duì)象,并用同步措施來(lái)充分保護(hù)它。

2. 其次是使用高級(jí)的并發(fā)編程構(gòu)件,如 TaskQueue、Producer-Consumer Queue、CountDownLatch 等等;

3. 最后不得已必須使用底層同步原語(yǔ) (primitives) 時(shí),只用非遞歸的互斥器和條件變量,偶爾用一用讀寫(xiě)鎖;

4. 不自己編寫(xiě) lock-free 代碼,不去憑空猜測(cè)“哪種做法性能會(huì)更好”,比如 spin lock vs. mutex。

前面兩條很容易理解,這里著重講一下第 3 條:底層同步原語(yǔ)的使用。

互斥器 (mutex)
互斥器 (mutex) 恐怕是使用得最多的同步原語(yǔ),粗略地說(shuō),它保護(hù)了臨界區(qū),一個(gè)時(shí)刻最多只能有一個(gè)線程在臨界區(qū)內(nèi)活動(dòng)。(請(qǐng)注意,我談的是 pthreads 里的 mutex,不是 Windows 里的重量級(jí)跨進(jìn)程 Mutex。)單獨(dú)使用 mutex 時(shí),我們主要為了保護(hù)共享數(shù)據(jù)。我個(gè)人的原則是:

用 RAII 手法封裝 mutex 的創(chuàng)建、銷(xiāo)毀、加鎖、解鎖這四個(gè)操作。
只用非遞歸的 mutex(即不可重入的 mutex)。
不手工調(diào)用 lock() 和 unlock() 函數(shù),一切交給棧上的 Guard 對(duì)象的構(gòu)造和析構(gòu)函數(shù)負(fù)責(zé),Guard 對(duì)象的生命期正好等于臨界區(qū)(分析對(duì)象在什么時(shí)候析構(gòu)是 C++ 程序員的基本功)。這樣我們保證在同一個(gè)函數(shù)里加鎖和解鎖,避免在 foo() 里加鎖,然后跑到 bar() 里解鎖。
在每次構(gòu)造 Guard 對(duì)象的時(shí)候,思考一路上(調(diào)用棧上)已經(jīng)持有的鎖,防止因加鎖順序不同而導(dǎo)致死鎖 (deadlock)。由于 Guard 對(duì)象是棧上對(duì)象,看函數(shù)調(diào)用棧就能分析用鎖的情況,非常便利。
次要原則有:

不使用跨進(jìn)程的 mutex,進(jìn)程間通信只用 TCP sockets。
加鎖解鎖在同一個(gè)線程,線程 a 不能去 unlock 線程 b 已經(jīng)鎖住的 mutex。(RAII 自動(dòng)保證)
別忘了解鎖。(RAII 自動(dòng)保證)
不重復(fù)解鎖。(RAII 自動(dòng)保證)
必要的時(shí)候可以考慮用 PTHREAD_MUTEX_ERRORCHECK 來(lái)排錯(cuò)
用 RAII 封裝這幾個(gè)操作是通行的做法,這幾乎是 C++ 的標(biāo)準(zhǔn)實(shí)踐,后面我會(huì)給出具體的代碼示例,相信大家都已經(jīng)寫(xiě)過(guò)或用過(guò)類(lèi)似的代碼了。Java 里的 synchronized 語(yǔ)句和 C# 的 using 語(yǔ)句也有類(lèi)似的效果,即保證鎖的生效期間等于一個(gè)作用域,不會(huì)因異常而忘記解鎖。

Mutex 恐怕是最簡(jiǎn)單的同步原語(yǔ),安裝上面的幾條原則,幾乎不可能用錯(cuò)。我自己從來(lái)沒(méi)有違背過(guò)這些原則,編碼時(shí)出現(xiàn)問(wèn)題都很快能招到并修復(fù)。

跑題:非遞歸的 mutex
談?wù)勎覉?jiān)持使用非遞歸的互斥器的個(gè)人想法。

Mutex 分為遞歸 (recursive) 和非遞歸(non-recursive)兩種,這是 POSIX 的叫法,另外的名字是可重入 (Reentrant) 與非可重入。這兩種 mutex 作為線程間 (inter-thread) 的同步工具時(shí)沒(méi)有區(qū)別,它們的惟一區(qū)別在于:同一個(gè)線程可以重復(fù)對(duì) recursive mutex 加鎖,但是不能重復(fù)對(duì) non-recursive mutex 加鎖。

首選非遞歸 mutex,絕對(duì)不是為了性能,而是為了體現(xiàn)設(shè)計(jì)意圖。non-recursive 和 recursive 的性能差別其實(shí)不大,因?yàn)樯儆靡粋(gè)計(jì)數(shù)器,前者略快一點(diǎn)點(diǎn)而已。在同一個(gè)線程里多次對(duì) non-recursive mutex 加鎖會(huì)立刻導(dǎo)致死鎖,我認(rèn)為這是它的優(yōu)點(diǎn),能幫助我們思考代碼對(duì)鎖的期求,并且及早(在編碼階段)發(fā)現(xiàn)問(wèn)題。

毫無(wú)疑問(wèn) recursive mutex 使用起來(lái)要方便一些,因?yàn)椴挥每紤]一個(gè)線程會(huì)自己把自己給鎖死了,我猜這也是 Java 和 Windows 默認(rèn)提供 recursive mutex 的原因。(Java 語(yǔ)言自帶的 intrinsic lock 是可重入的,它的 concurrent 庫(kù)里提供 ReentrantLock,Windows 的 CRITICAL_SECTION 也是可重入的。似乎它們都不提供輕量級(jí)的 non-recursive mutex。)

正因?yàn)樗奖,recursive mutex 可能會(huì)隱藏代碼里的一些問(wèn)題。典型情況是你以為拿到一個(gè)鎖就能修改對(duì)象了,沒(méi)想到外層代碼已經(jīng)拿到了鎖,正在修改(或讀。┩粋(gè)對(duì)象呢。具體的例子:

01 std::vector<Foo> foos;

02 MutexLock mutex;

03

04 void post(const Foo& f)

05 {

06 MutexLockGuard lock(mutex);

07 foos.push_back(f);

08 }

09

10 void traverse()

11 {

12 MutexLockGuard lock(mutex);

13 for (auto it = foos.begin(); it != foos.end(); ++it) { // 用了 0x 新寫(xiě)法

14 it->doit();

15 }

16 }

post() 加鎖,然后修改 foos 對(duì)象; traverse() 加鎖,然后遍歷 foos 數(shù)組。將來(lái)有一天,F(xiàn)oo::doit() 間接調(diào)用了 post() (這在邏輯上是錯(cuò)誤的),那么會(huì)很有戲劇性的:

1. Mutex 是非遞歸的,于是死鎖了。

2. Mutex 是遞歸的,由于 push_back 可能(但不總是)導(dǎo)致 vector 迭代器失效,程序偶爾會(huì) crash。

這時(shí)候就能體現(xiàn) non-recursive 的優(yōu)越性:把程序的邏輯錯(cuò)誤暴露出來(lái)。死鎖比較容易 debug,把各個(gè)線程的調(diào)用棧打出來(lái)((gdb) thread apply all bt),只要每個(gè)函數(shù)不是特別長(zhǎng),很容易看出來(lái)是怎么死的。(另一方面支持了函數(shù)不要寫(xiě)過(guò)長(zhǎng)。)或者可以用 PTHREAD_MUTEX_ERRORCHECK 一下子就能找到錯(cuò)誤(前提是 MutexLock 帶 debug 選項(xiàng)。)

程序反正要死,不如死得有意義一點(diǎn),讓驗(yàn)尸官的日子好過(guò)些。

如果一個(gè)函數(shù)既可能在已加鎖的情況下調(diào)用,又可能在未加鎖的情況下調(diào)用,那么就拆成兩個(gè)函數(shù):

1. 跟原來(lái)的函數(shù)同名,函數(shù)加鎖,轉(zhuǎn)而調(diào)用第 2 個(gè)函數(shù)。

2. 給函數(shù)名加上后綴 WithLockHold,不加鎖,把原來(lái)的函數(shù)體搬過(guò)來(lái)。

就像這樣:

01 void post(const Foo& f)

02 {

03 MutexLockGuard lock(mutex);

04 postWithLockHold(f); // 不用擔(dān)心開(kāi)銷(xiāo),編譯器會(huì)自動(dòng)內(nèi)聯(lián)的

05 }

06

07 // 引入這個(gè)函數(shù)是為了體現(xiàn)代碼作者的意圖,盡管 push_back 通?梢允謩(dòng)內(nèi)聯(lián)

08 void postWithLockHold(const Foo& f)

09 {

10 foos.push_back(f);

11 }

這有可能出現(xiàn)兩個(gè)問(wèn)題(感謝水木網(wǎng)友 ilovecpp 提出):a) 誤用了加鎖版本,死鎖了。b) 誤用了不加鎖版本,數(shù)據(jù)損壞了。

對(duì)于 a),仿造前面的辦法能比較容易地排錯(cuò)。對(duì)于 b),如果 pthreads 提供 isLocked() 就好辦,可以寫(xiě)成:

1 void postWithLockHold(const Foo& f)

2 {

3 assert(mutex.isLocked()); // 目前只是一個(gè)愿望

4 // ...

5 }

另外,WithLockHold 這個(gè)顯眼的后綴也讓程序中的誤用容易暴露出來(lái)。

C++ 沒(méi)有 annotation,不能像 Java 那樣給 method 或 field 標(biāo)上 @GuardedBy 注解,需要程序員自己小心在意。雖然這里的辦法不能一勞永逸地解決全部多線程錯(cuò)誤,但能幫上一點(diǎn)是一點(diǎn)了。

我還沒(méi)有遇到過(guò)需要使用 recursive mutex 的情況,我想將來(lái)遇到了都可以借助 wrapper 改用 non-recursive mutex,代碼只會(huì)更清晰。

=== 回到正題 ===

本文這里只談了 mutex 本身的正確使用,在 C++ 里多線程編程還會(huì)遇到其他很多 race condition,請(qǐng)參考拙作《當(dāng)析構(gòu)函數(shù)遇到多線程——C++ 中線程安全的對(duì)象回調(diào)》
http://blog.csdn.net/Solstice/archive/2010/01/22/5238671.aspx 。請(qǐng)注意這里的 class 命名與那篇文章有所不同。我現(xiàn)在認(rèn)為 MutexLock 和 MutexLockGuard 是更好的名稱。

性能注腳:Linux 的 pthreads mutex 采用 futex 實(shí)現(xiàn),不必每次加鎖解鎖都陷入系統(tǒng)調(diào)用,效率不錯(cuò)。Windows 的 CRITICAL_SECTION 也是類(lèi)似。

條件變量
條件變量 (condition variable) 顧名思義是一個(gè)或多個(gè)線程等待某個(gè)布爾表達(dá)式為真,即等待別的線程“喚醒”它。條件變量的學(xué)名叫管程 (monitor)。Java Object 內(nèi)置的 wait(), notify(), notifyAll() 即是條件變量(它們以容易用錯(cuò)著稱)。條件變量只有一種正確使用的方式,對(duì)于 wait() 端:

1. 必須與 mutex 一起使用,該布爾表達(dá)式的讀寫(xiě)需受此 mutex 保護(hù)

2. 在 mutex 已上鎖的時(shí)候才能調(diào)用 wait()

3. 把判斷布爾條件和 wait() 放到 while 循環(huán)中

寫(xiě)成代碼是:

01 MutexLock mutex;

02 Condition cond(mutex);

03 std::deque<int> queue;

04

05 int dequeue()

06 {

07 MutexLockGuard lock(mutex);

08 while (queue.empty()) { // 必須用循環(huán);必須在判斷之后再 wait()

09 cond.wait(); // 這一步會(huì)原子地 unlock mutex 并進(jìn)入 blocking,不會(huì)與 enqueue 死鎖

10 }

11 assert(!queue.empty());

12 int top = queue.front();

13 queue.pop_front();

14 return top;

15 }

對(duì)于 signal/broadcast 端:

1. 不一定要在 mutex 已上鎖的情況下調(diào)用 signal (理論上)

2. 在 signal 之前一般要修改布爾表達(dá)式

3. 修改布爾表達(dá)式通常要用 mutex 保護(hù)(至少用作 full memory barrier)

寫(xiě)成代碼是:

1 void enqueue(int x)

2 {

3 MutexLockGuard lock(mutex);

4 queue.push_back(x);

5 cond.notify();

6 }

上面的 dequeue/enqueue 實(shí)際上實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的 unbounded BlockingQueue。

條件變量是非常底層的同步原語(yǔ),很少直接使用,一般都是用它來(lái)實(shí)現(xiàn)高層的同步措施,如 BlockingQueue 或 CountDownLatch。

讀寫(xiě)鎖與其他
讀寫(xiě)鎖 (Reader-Writer lock),讀寫(xiě)鎖是個(gè)優(yōu)秀的抽象,它明確區(qū)分了 read 和 write 兩種行為。需要注意的是,reader lock 是可重入的,writer lock 是不可重入(包括不可提升 reader lock)的。這正是我說(shuō)它“優(yōu)秀”的主要原因。

遇到并發(fā)讀寫(xiě),如果條件合適,我會(huì)用《借 shared_ptr 實(shí)現(xiàn)線程安全的 copy-on-write》http://blog.csdn.net/Solstice/archive/2008/11/22/3351751.aspx 介紹的辦法,而不用讀寫(xiě)鎖。當(dāng)然這不是絕對(duì)的。

信號(hào)量 (Semaphore),我沒(méi)有遇到過(guò)需要使用信號(hào)量的情況,無(wú)從談及個(gè)人經(jīng)驗(yàn)。

說(shuō)一句大逆不道的話,如果程序里需要解決如“哲學(xué)家就餐”之類(lèi)的復(fù)雜 IPC 問(wèn)題,我認(rèn)為應(yīng)該首先考察幾個(gè)設(shè)計(jì),為什么線程之間會(huì)有如此復(fù)雜的資源爭(zhēng)搶(一個(gè)線程要同時(shí)搶到兩個(gè)資源,一個(gè)資源可以被兩個(gè)線程爭(zhēng)奪)?能不能把“想吃飯”這個(gè)事情專門(mén)交給一個(gè)為各位哲學(xué)家分派餐具的線程來(lái)做,然后每個(gè)哲學(xué)家等在一個(gè)簡(jiǎn)單的 condition variable 上,到時(shí)間了有人通知他去吃飯?從哲學(xué)上說(shuō),教科書(shū)上的解決方案是平權(quán),每個(gè)哲學(xué)家有自己的線程,自己去拿筷子;我寧愿用集權(quán)的方式,用一個(gè)線程專門(mén)管餐具的分配,讓其他哲學(xué)家線程拿個(gè)號(hào)等在食堂門(mén)口好了。這樣不損失多少效率,卻讓程序簡(jiǎn)單很多。雖然 Windows 的 WaitForMultipleObjects 讓這個(gè)問(wèn)題 trivial 化,在 Linux 下正確模擬 WaitForMultipleObjects 不是普通程序員該干的。

封裝 MutexLock、MutexLockGuard 和 Condition
本節(jié)把前面用到的 MutexLock、MutexLockGuard、Condition classes 的代碼列出來(lái),前面兩個(gè) classes 沒(méi)多大難度,后面那個(gè)有點(diǎn)意思。

MutexLock 封裝臨界區(qū)(Critical secion),這是一個(gè)簡(jiǎn)單的資源類(lèi),用 RAII 手法 [CCS:13]封裝互斥器的創(chuàng)建與銷(xiāo)毀。臨界區(qū)在 Windows 上是 CRITICAL_SECTION,是可重入的;在 Linux 下是 pthread_mutex_t,默認(rèn)是不可重入的。MutexLock 一般是別的 class 的數(shù)據(jù)成員。

MutexLockGuard 封裝臨界區(qū)的進(jìn)入和退出,即加鎖和解鎖。MutexLockGuard 一般是個(gè)棧上對(duì)象,它的作用域剛好等于臨界區(qū)域。

這兩個(gè) classes 應(yīng)該能在紙上默寫(xiě)出來(lái),沒(méi)有太多需要解釋的:

01 #include <pthread.h>

02 #include <boost/noncopyable.hpp>

03

04 class MutexLock : boost::noncopyable

05 {

06 public:

07 MutexLock() // 為了節(jié)省版面,單行函數(shù)都沒(méi)有正確縮進(jìn)

08 { pthread_mutex_init(&mutex_, NULL); }

09

10 ~MutexLock()

11 { pthread_mutex_destroy(&mutex_); }

12

13 void lock() // 程序一般不主動(dòng)調(diào)用

14 { pthread_mutex_lock(&mutex_); }

15

16 void unlock() // 程序一般不主動(dòng)調(diào)用

17 { pthread_mutex_unlock(&mutex_); }

18

19 pthread_mutex_t* getPthreadMutex() // 僅供 Condition 調(diào)用,嚴(yán)禁自己調(diào)用

20 { return &mutex_; }

21

22 private:

23 pthread_mutex_t mutex_;

24 };

25

26 class MutexLockGuard : boost::noncopyable

27 {

28 public:

29 explicit MutexLockGuard(MutexLock& mutex) : mutex_(mutex)

30 { mutex_.lock(); }

31

32 ~MutexLockGuard()

33 { mutex_.unlock(); }

34

35 private:

36 MutexLock& mutex_;

37 };

38

39 #define MutexLockGuard(x) static_assert(false, "missing mutex guard var name")

注意代碼的最后一行定義了一個(gè)宏,這個(gè)宏的作用是防止程序里出現(xiàn)如下錯(cuò)誤:

1 void doit()

2 {

3 MutexLockGuard(mutex); // 沒(méi)有變量名,產(chǎn)生一個(gè)臨時(shí)對(duì)象又馬上銷(xiāo)毀了,沒(méi)有鎖住臨界區(qū)

4 // 正確寫(xiě)法是 MutexLockGuard lock(mutex);

5

6 // 臨界區(qū)

7 }

這里 MutexLock 沒(méi)有提供 trylock() 函數(shù),因?yàn)槲覜](méi)有用過(guò)它,我想不出什么時(shí)候程序需要“試著去鎖一鎖”,或許我寫(xiě)過(guò)的代碼太簡(jiǎn)單了。

我見(jiàn)過(guò)有人把 MutexLockGuard 寫(xiě)成 template,我沒(méi)有這么做是因?yàn)樗哪0孱?lèi)型參數(shù)只有 MutexLock 一種可能,沒(méi)有必要隨意增加靈活性,于是我人肉把模板具現(xiàn)化 (instantiate) 了。此外一種更激進(jìn)的寫(xiě)法是,把 lock/unlock 放到 private 區(qū),然后把 Guard 設(shè)為 MutexLock 的 friend,我認(rèn)為在注釋里告知程序員即可,另外 check-in 之前的 code review 也很容易發(fā)現(xiàn)誤用的情況 (grep getPthreadMutex)。

這段代碼沒(méi)有達(dá)到工業(yè)強(qiáng)度:a) Mutex 創(chuàng)建為 PTHREAD_MUTEX_DEFAULT 類(lèi)型,而不是我們預(yù)想的 PTHREAD_MUTEX_NORMAL 類(lèi)型(實(shí)際上這二者很可能是等同的),嚴(yán)格的做法是用 mutexattr 來(lái)顯示指定 mutex 的類(lèi)型。b) 沒(méi)有檢查返回值。這里不能用 assert 檢查返回值,因?yàn)?assert 在 release build 里是空語(yǔ)句。我們檢查返回值的意義在于防止 ENOMEM 之類(lèi)的資源不足情況,這一般只可能在負(fù)載很重的產(chǎn)品程序中出現(xiàn)。一旦出現(xiàn)這種錯(cuò)誤,程序必須立刻清理現(xiàn)場(chǎng)并主動(dòng)退出,否則會(huì)莫名其妙地崩潰,給事后調(diào)查造成困難。這里我們需要 non-debug 的 assert,或許 google-glog 的 CHECK() 是個(gè)不錯(cuò)的思路。

以上兩點(diǎn)改進(jìn)留作練習(xí)。

Condition class 的實(shí)現(xiàn)有點(diǎn)意思。

Pthreads condition variable 允許在 wait() 的時(shí)候指定 mutex,但是我想不出什么理由一個(gè) condition variable 會(huì)和不同的 mutex 配合使用。Java 的 intrinsic condition 和 Conditon class 都不支持這么做,因此我覺(jué)得可以放棄這一靈活性,老老實(shí)實(shí)一對(duì)一好了。相反 boost::thread 的 condition_varianle 是在 wait 的時(shí)候指定 mutex,請(qǐng)參觀其同步原語(yǔ)的龐雜設(shè)計(jì):

Concept 有四種 Lockable, TimedLockable, SharedLockable, UpgradeLockable.
Lock 有五六種: lock_guard, unique_lock, shared_lock, upgrade_lock, upgrade_to_unique_lock, scoped_try_lock.
Mutex 有七種:mutex, try_mutex, timed_mutex, recursive_mutex, recursive_try_mutex, recursive_timed_mutex, shared_mutex.
恕我愚鈍,見(jiàn)到 boost::thread 這樣如 Rube Goldberg Machine 一樣“靈活”的庫(kù)我只得三揖繞道而行。這些 class 名字也很無(wú)厘頭,為什么不老老實(shí)實(shí)用 reader_writer_lock 這樣的通俗名字呢?非得增加精神負(fù)擔(dān),自己發(fā)明新名字。我不愿為這樣的靈活性付出代價(jià),寧愿自己做幾個(gè)簡(jiǎn)簡(jiǎn)單單的一看就明白的 classes 來(lái)用,這種簡(jiǎn)單的幾行代碼的輪子造造也無(wú)妨。提供靈活性固然是本事,然而在不需要靈活性的地方把代碼寫(xiě)死,更需要大智慧

下面這個(gè) Condition 簡(jiǎn)單地封裝了 pthread cond var,用起來(lái)也容易,見(jiàn)本節(jié)前面的例子。這里我用 notify/notifyAll 作為函數(shù)名,因?yàn)?signal 有別的含義,C++ 里的 signal/slot,C 里的 signal handler 等等。就別 overload 這個(gè)術(shù)語(yǔ)了。

01 class Condition : boost::noncopyable

02 {

03 public:

04 Condition(MutexLock& mutex) : mutex_(mutex)

05 { pthread_cond_init(&pcond_, NULL); }

06

07 ~Condition()

08 { pthread_cond_destroy(&pcond_); }

09

10 void wait()

11 { pthread_cond_wait(&pcond_, mutex_.getPthreadMutex()); }

12

13 void notify()

14 { pthread_cond_signal(&pcond_); }

15

16 void notifyAll()

17 { pthread_cond_broadcast(&pcond_); }

18

19 private:

20 MutexLock& mutex_;

21 pthread_cond_t pcond_;

22 };

如果一個(gè) class 要包含 MutexLock 和 Condition,請(qǐng)注意它們的聲明順序和初始化順序,mutex_ 應(yīng)先于 condition_ 構(gòu)造,并作為后者的構(gòu)造參數(shù):

01 class CountDownLatch

02 {

03 public:

04 CountDownLatch(int count)

05 : count_(count),

06 mutex_(),

07 condition_(mutex_)

08 { }

09

10 private:

11 int count_;

12 MutexLock mutex_; // 順序很重要

13 Condition condition_;

14 };

請(qǐng)?jiān)试S我再次強(qiáng)調(diào),雖然本節(jié)花了大量篇幅介紹如何正確使用 mutex 和 condition variable,但并不代表我鼓勵(lì)到處使用它們。這兩者都是非常底層的同步原語(yǔ),主要用來(lái)實(shí)現(xiàn)更高級(jí)的并發(fā)編程工具,一個(gè)多線程程序里如果大量使用 mutex 和 condition variable 來(lái)同步,基本跟用鉛筆刀鋸大樹(shù)(孟巖語(yǔ))沒(méi)啥區(qū)別。

在程序里使用 pthreads 庫(kù)有一個(gè)額外的好處:分析工具認(rèn)得它們,懂得其語(yǔ)意。線程分析工具如 Intel Thread Checker 和 Valgrind-Helgrind 等能識(shí)別 pthreads 調(diào)用,并依據(jù) happens-before 關(guān)系 [Lamport 1978] 分析程序有無(wú) data race。

線程安全的 Singleton 實(shí)現(xiàn)
研究 Signleton 的線程安全實(shí)現(xiàn)的歷史你會(huì)發(fā)現(xiàn)很多有意思的事情,一度人們認(rèn)為 Double checked locking 是王道,兼顧了效率與正確性。后來(lái)有神牛指出由于亂序執(zhí)行的影響,DCL 是靠不住的。(這個(gè)又讓我想起了 SQL 注入,十年前用字符串拼接出 SQL 語(yǔ)句是 Web 開(kāi)發(fā)的通行做法,直到有一天有人利用這個(gè)漏洞越權(quán)獲得并修改網(wǎng)站數(shù)據(jù),人們才幡然醒悟,趕緊修補(bǔ)。)Java 開(kāi)發(fā)者還算幸運(yùn),可以借助內(nèi)部靜態(tài)類(lèi)的裝載來(lái)實(shí)現(xiàn)。C++ 就比較慘,要么次次鎖,要么 eager initialize、或者動(dòng)用 memory barrier 這樣的大殺器( http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf )。接下來(lái) Java 5 修訂了內(nèi)存模型,并增強(qiáng)了 volatile 的語(yǔ)義,這下 DCL (with volatile) 又是安全的了。然而 C++ 的內(nèi)存模型還在修訂中,C++ 的 volatile 目前還不能(將來(lái)也難說(shuō))保證 DCL 的正確性(只在 VS2005+ 上有效)。

其實(shí)沒(méi)那么麻煩,在實(shí)踐中用 pthread once 就行:

01 #include <pthread.h>

02

03 template<typename T>

04 class Singleton : boost::noncopyable

05 {

06 public:

07 static T& instance()

08 {

09 pthread_once(&ponce_, &Singleton::init);

10 return *value_;

11 }

12

13 static void init()

14 {

15 value_ = new T();

16 }

17

18 private:

19 static pthread_once_t ponce_;

20 static T* value_;

21 };

22

23 template<typename T>

24 pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;

25

26 template<typename T>

27 T* Singleton<T>::value_ = NULL;

上面這個(gè) Singleton 沒(méi)有任何花哨的技巧,用 pthread_once_t 來(lái)保證 lazy-initialization 的線程安全。使用方法也很簡(jiǎn)單:

Foo& foo = Singleton<Foo>::instance();

當(dāng)然,這個(gè) Singleton 沒(méi)有考慮對(duì)象的銷(xiāo)毀,在服務(wù)器程序里,這不是一個(gè)問(wèn)題,因?yàn)楫?dāng)程序退出的時(shí)候自然就釋放所有資源了(前提是程序里不使用不能由操作系統(tǒng)自動(dòng)關(guān)閉的資源,比如跨進(jìn)程的 Mutex)。另外,這個(gè) Singleton 只能調(diào)用默認(rèn)構(gòu)造函數(shù),如果用戶想要指定 T 的構(gòu)造方式,我們可以用模板特化 (template specialization) 技術(shù)來(lái)提供一個(gè)定制點(diǎn),這需要引入另一層間接。

歸納
進(jìn)程間通信首選 TCP sockets
線程同步的四項(xiàng)原則
使用互斥器的條件變量的慣用手法 (idiom),關(guān)鍵是 RAII
用好這幾樣?xùn)|西,基本上能應(yīng)付多線程服務(wù)端開(kāi)發(fā)的各種場(chǎng)合,只是或許有人會(huì)覺(jué)得性能沒(méi)有發(fā)揮到極致。我認(rèn)為,先把程序?qū)懻_了,再考慮性能優(yōu)化,這在多線程下任然成立。讓一個(gè)正確的程序變快,遠(yuǎn)比“讓一個(gè)快的程序變正確”容易得多。

7 總結(jié)
在現(xiàn)代的多核計(jì)算背景下,線程是不可避免的。盡管一定程度上可以通過(guò) framework 來(lái)屏蔽,讓你感覺(jué)像是在寫(xiě)單線程程序,比如 Java Servlet。了解 under the hood 發(fā)生了什么對(duì)于編寫(xiě)這種程序也會(huì)有幫助。

多線程編程是一項(xiàng)重要的個(gè)人技能,不能因?yàn)樗y就本能地排斥,現(xiàn)在的軟件開(kāi)發(fā)比起 10 年 20 年前已經(jīng)難了不知道多少倍。掌握多線程編程,才能更理智地選擇用還是不用多線程,因?yàn)槟隳茴A(yù)估多線程實(shí)現(xiàn)的難度與收益,在一開(kāi)始做出正確的選擇。要知道把一個(gè)單線程程序改成多線程的,往往比重頭實(shí)現(xiàn)一個(gè)多線程的程序更難。

掌握同步原語(yǔ)和它們的適用場(chǎng)合時(shí)多線程編程的基本功。以我的經(jīng)驗(yàn),熟練使用文中提到的同步原語(yǔ),就能比較容易地編寫(xiě)線程安全的程序。本文沒(méi)有考慮 signal 對(duì)多線程編程的影響,Unix 的 signal 在多線程下的行為比較復(fù)雜,一般要靠底層的網(wǎng)絡(luò)庫(kù) (如 Reactor) 加以屏蔽,避免干擾上層應(yīng)用程序的開(kāi)發(fā)。

通篇來(lái)看,“效率”并不是我的主要考慮點(diǎn),a) TCP 不是效率最高的 IPC,b) 我提倡正確加鎖而不是自己編寫(xiě) lock-free 算法(使用原子操作除外)。在程序的復(fù)雜度和性能之前取得平衡,并經(jīng)考慮未來(lái)兩三年擴(kuò)容的可能(無(wú)論是 CPU 變快、核數(shù)變多,還是機(jī)器數(shù)量增加,網(wǎng)絡(luò)升級(jí))。下一篇“多線程編程的反模式”會(huì)考察伸縮性方面的常見(jiàn)錯(cuò)誤,我認(rèn)為在分布式系統(tǒng)中,伸縮性 (scalability) 比單機(jī)的性能優(yōu)化更值得投入精力。

這篇文章記錄了我目前對(duì)多線程編程的理解,用文中介紹的手法,我能解決自己面臨的全部多線程編程任務(wù)。如果文章的觀點(diǎn)與您不合,比如您使用了我沒(méi)有推薦使用的技術(shù)或手法(共享內(nèi)存、信號(hào)量等等),只要您理由充分,但行無(wú)妨。

這篇文章本來(lái)還有兩節(jié)“多線程編程的反模式”與“多線程的應(yīng)用場(chǎng)景”,考慮到字?jǐn)?shù)已經(jīng)超過(guò)一萬(wàn)了,且聽(tīng)下回分解吧 :-)

后文預(yù)覽:Sleep 反模式
我認(rèn)為 sleep 只能出現(xiàn)在測(cè)試代碼中,比如寫(xiě)單元測(cè)試的時(shí)候。(涉及時(shí)間的單元測(cè)試不那么好寫(xiě),短的如一兩秒鐘可以用 sleep,長(zhǎng)的如一小時(shí)一天得想其他辦法,比如把算法提出來(lái)并把時(shí)間注入進(jìn)去。)產(chǎn)品代碼中線程的等待可分為兩種:一種是無(wú)所事事的時(shí)候(要么等在 select/poll/epoll 上。要么等在 condition variable 上,等待 BlockingQueue /CountDownLatch 亦可歸入此類(lèi)),一種是等著進(jìn)入臨界區(qū)(等在 mutex 上)以便繼續(xù)處理。在程序的正常執(zhí)行中,如果需要等待一段時(shí)間,應(yīng)該往 event loop 里注冊(cè)一個(gè) timer,然后在 timer 的回調(diào)函數(shù)里接著干活,因?yàn)榫程是個(gè)珍貴的共享資源,不能輕易浪費(fèi)。如果多線程的安全性和效率要靠代碼主動(dòng)調(diào)用 sleep 來(lái)保證,這是設(shè)計(jì)出了問(wèn)題。等待一個(gè)事件發(fā)生,正確的做法是用 select 或 condition variable 或(更理想地)高層同步工具。當(dāng)然,在 GUI 編程中會(huì)有主動(dòng)讓出 CPU 的做法,比如調(diào)用 sleep(0) 來(lái)實(shí)現(xiàn) yield。

2 0 0
(請(qǐng)您對(duì)文章做出評(píng)價(jià))

    飛信
    (17)飛信
    西西軟件園提供各平臺(tái)飛信官方下載,雖然微信是目前市場(chǎng)上最流行的交流討論軟件,但是飛信也有它自己獨(dú)有的特色功能,融合語(yǔ)音短信等多種通信方式,覆蓋三種不同形態(tài)完全實(shí)時(shí)準(zhǔn)實(shí)時(shí)和非實(shí)時(shí)的客戶通信需求,實(shí)現(xiàn)互聯(lián)網(wǎng)和移動(dòng)網(wǎng)間的無(wú)縫通信服務(wù)。飛信不但可以免費(fèi)從給手機(jī)發(fā)短信,而且不受任何限制,能夠隨時(shí)隨地與好友開(kāi)始語(yǔ)聊,并享受超低語(yǔ)聊資費(fèi)。...更多>>
    • 飛信2017V5.6.8860.0 官方正式版

      12-29 / 69.1M

      推薦理由:免費(fèi)短信,隨時(shí)發(fā)送,無(wú)縫溝通新體驗(yàn)。飛信是中國(guó)移動(dòng)推出融合語(yǔ)音(IVR)、GPRS、短信等多種方式的通信服務(wù)
    • 手機(jī)飛信2015 for Android5.5.7 安

      04-22 / 47.7M

      推薦理由:通訊錄列表能力增強(qiáng):用戶在通訊錄可以一鍵直接添加好友,還可以將最親密的好友添加至親密好友區(qū)置頂顯示,
    • 飛信2015 for Mac2.5 官方正式版

      05-16 / 15.4M

      推薦理由:飛信是中國(guó)移動(dòng)推出的“綜合通信服務(wù)”,即融合語(yǔ)音(IVR)、GPRS、短信等多種通信方式,覆蓋三種不同形態(tài)(完
    • 黑莓手機(jī)飛信V3.3.0 官方最新版

      06-14 / 1001KB

      推薦理由:親測(cè)比較好用的飛信手機(jī)版本,使用相對(duì)方便,各個(gè)ROM都有對(duì)應(yīng)的程序。 更新日志:黑莓手機(jī)飛信BlackBerryV
    • 移動(dòng)網(wǎng)頁(yè)飛信接口1.0綠色版

      02-07 / 12KB

      推薦理由:移動(dòng)網(wǎng)頁(yè)飛信接口:用于中國(guó)移動(dòng)通信開(kāi)通了飛信功能的用戶使用,用于自動(dòng)登錄網(wǎng)頁(yè)飛信并免費(fèi)向手機(jī)發(fā)送短信
    • 手機(jī)飛信iPhone云聊版V3.5.0 官方正

      07-30 / 45.2M

      推薦理由:飛信是中國(guó)移動(dòng)推出的“綜合通信服務(wù)”,實(shí)現(xiàn)互聯(lián)網(wǎng)和移動(dòng)網(wǎng)間的無(wú)縫通信服務(wù)。特點(diǎn):手機(jī)飛信iPhone版具有以
    QQ2017
    (24)QQ2017
    酷炫界面隨心而動(dòng),與眾不同。全新皮膚引擎,輕松上傳美圖作為皮膚,體驗(yàn)屬于自己的視覺(jué)盛宴。專業(yè)高品質(zhì)的界面構(gòu)想,為您帶來(lái)無(wú)與倫比的視覺(jué)享受。下載正式版免費(fèi)下載已經(jīng)免費(fèi)提供給廣大騰訊用戶進(jìn)行下載了,如果你還是在使用舊版本,那么你已經(jīng)了,快來(lái)下載正式版來(lái)體驗(yàn)下新版本帶給你的快樂(lè)吧正式版新增劃詞搜索功能,邊聊邊搜更輕松,新增會(huì)員超級(jí)表情功能,聊天更有生動(dòng)有趣最新版官方下載是在系列版本的基礎(chǔ)上,全新設(shè)計(jì)與定...更多>>
    • QQ20178.9.20026 官方最新版

      02-08 / 60.2M

      推薦理由:qq2017最新版官方下載,QQ2017全新設(shè)計(jì)回歸本源設(shè)計(jì),讓您的目光停留在您所關(guān)注的內(nèi)容上,大大提升了溝通的
    • QQ2014最新體驗(yàn)版6.6.13074 官方安

      11-11 / 55.8M

      推薦理由:騰訊體驗(yàn)中心本次推出的QQ性能體驗(yàn)版3.0,通過(guò)改造基礎(chǔ)架構(gòu)和框架,針對(duì)關(guān)鍵功能深度優(yōu)化,重點(diǎn)提升了啟動(dòng),
    • iPhoneQQ2016v6.5.9 正式版

      11-04 / 178M

      推薦理由:QQ手機(jī)版,致力于更完美的移動(dòng)社交、娛樂(lè)與生活體驗(yàn)——樂(lè)在溝通15年,聊天歡樂(lè)8億人!QQ2016foriPhone全新
    • QQ2014 for WP84.3 官方最新版

      06-10 / 20M

      推薦理由:QQ for WP8正式發(fā)布,視頻功能更上一層樓,新增視頻時(shí)切換到后臺(tái)、切換到后置攝像頭、本方和對(duì)方畫(huà)面切換等
    • QQ輕聊版for windowsv7.9(14305) 官

      12-02 / 46.5M

      推薦理由:QQ輕聊版for windows是騰訊為pc端客戶推出的一款精簡(jiǎn)版qq,用過(guò)手機(jī)qq輕聊版的用戶對(duì)輕聊版應(yīng)該都有個(gè)大概的
    • 手機(jī)QQ2017 for Androidv6.6.9 官方

      02-14 / 37.8M

      推薦理由:全新的手機(jī)QQ2017版正式發(fā)布。qq2017新版本在保留原有功能基礎(chǔ)上,加入2套趣味表情,并對(duì)UI進(jìn)行簡(jiǎn)化設(shè)計(jì),去
    即時(shí)通訊軟件
    (33)即時(shí)通訊軟件
    即時(shí)通訊是一個(gè)終端服務(wù),允許兩人或多人使用網(wǎng)路即時(shí)的傳遞文字訊息檔案語(yǔ)音與視頻交流。即時(shí)通訊按使用用途分為企業(yè)即時(shí)通訊和網(wǎng)站即時(shí)通訊,根據(jù)裝載的對(duì)象又可分為手機(jī)即時(shí)通訊和即時(shí)通訊,手機(jī)即時(shí)通訊代表是短信,網(wǎng)站視頻即時(shí)通訊。在網(wǎng)際網(wǎng)路上頗受用戶歡迎的即時(shí)通訊服務(wù)包含信鴿人人桌面一說(shuō)語(yǔ)音飛信企業(yè)飛信這些服務(wù)有賴于許多想法更久的與普遍的線上聊天媒介,如一樣知名...更多>>
    • 信鴿v3.0.5 官方最新版

      09-10 / 18.5M

      推薦理由:信鴿是一款專為各種組織移動(dòng)溝通設(shè)計(jì)的完全免費(fèi)的即時(shí)通訊工具。信鴿支持批量導(dǎo)入組織結(jié)構(gòu),按分組展開(kāi)顯示
    • 群英ccv4.5.2.24060 官方免費(fèi)版

      08-17 / 19.3M

      推薦理由:群英cc是一款專業(yè)的企業(yè)即時(shí)通訊軟件,它不僅是企業(yè)統(tǒng)一通訊的消息平臺(tái),更是企業(yè)在線辦公軟件(SAAS)的融
    • 騰訊通RTX 2015 客戶端正式版

      01-21 / 23.3M

      推薦理由:RTX騰訊通終于更新到2013版本了,界面更好看,但是好像很多東西有待各位嘗試看看,建議小幅度測(cè)試后再升級(jí)。
    • 飛信2017V5.6.8860.0 官方正式版

      12-29 / 69.1M

      推薦理由:免費(fèi)短信,隨時(shí)發(fā)送,無(wú)縫溝通新體驗(yàn)。飛信是中國(guó)移動(dòng)推出融合語(yǔ)音(IVR)、GPRS、短信等多種方式的通信服務(wù)
    • 康福中國(guó)6.11.529 最新簡(jiǎn)體中文版

      04-21 / 10.5M

      推薦理由:目前CamfrogVideoChat的全球注冊(cè)用戶已達(dá)到3億,全球同時(shí)在線聊友人數(shù)超過(guò)300萬(wàn),它具備QQ、MSN、UC等聊天軟
    • 阿里旺旺買(mǎi)家版2016v9.06.01 官方正

      11-23 / 50.3M

      推薦理由:淘寶天貓上面的東西很多,我們需要購(gòu)買(mǎi)裝上旺旺買(mǎi)家版,可以跟店主進(jìn)行溝通,這是阿里旺旺2016買(mǎi)家版本,針對(duì)

    相關(guān)評(píng)論

    閱讀本文后您有什么感想? 已有人給出評(píng)價(jià)!

    • 8 喜歡喜歡
    • 3 頂
    • 1 難過(guò)難過(guò)
    • 5 囧
    • 3 圍觀圍觀
    • 2 無(wú)聊無(wú)聊

    熱門(mén)評(píng)論

    最新評(píng)論

    發(fā)表評(píng)論 查看所有評(píng)論(0)

    昵稱:
    表情: 高興 可 汗 我不要 害羞 好 下下下 送花 屎 親親
    字?jǐn)?shù): 0/500 (您的評(píng)論需要經(jīng)過(guò)審核才能顯示)