日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

多线程服务器的常用编程模型

發(fā)布時間:2023/12/18 编程问答 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 多线程服务器的常用编程模型 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

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


陳碩(giantchen_AT_gmail)

Blog.csdn.net/Solstice

2009Feb12

建議閱讀本文 PDF 版下載:http://files.cppblog.com/Solstice/multithreaded_server.pdf

本文主要講我個人在多線程開發(fā)方面的一些粗淺經(jīng)驗。總結(jié)了一兩種常用的線程模型,歸納了進程間通訊與線程同步的最佳實踐,以期用簡單規(guī)范的方式開發(fā)多線程程序。

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

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

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

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

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

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

目錄

1進程與線程 2

2典型的單線程服務(wù)器編程模型 3

3典型的多線程服務(wù)器的線程模型 3

Oneloopperthread 4

線程池 4

歸納 5

4進程間通信與線程間通信 5

5進程間通信 6

6線程間同步 7

互斥器(mutex) 7

跑題:非遞歸的mutex 8

條件變量 10

讀寫鎖與其他 11

封裝MutexLockMutexLockGuardCondition 11

線程安全的Singleton實現(xiàn) 14

歸納 15

7總結(jié) 15

后文預(yù)覽:Sleep反模式 16

1進程與線程

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

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

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

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

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

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

“多線程”的價值,我認(rèn)為是為了更好地發(fā)揮對稱多路處理(SMP)的效能。在SMP之前,多線程沒有多大價值。AlanCox說過Acomputerisastatemachine.Threadsareforpeoplewhocan'tprogramstatemachines.(計算機是一臺狀態(tài)機。線程是給那些不能編寫狀態(tài)機程序的人準(zhǔn)備的。)如果只有一個執(zhí)行單元,一個CPU,那么確實如AlanCox所說,按狀態(tài)機的思路去寫程序是最高效的,這正好也是下一節(jié)展示的編程模型。

2典型的單線程服務(wù)器編程模型

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

llighttpd,單線程服務(wù)器。(nginx估計與之類似,待查)

llibevent/libev

lACE,PocoC++librariesQT待查)

lJavaNIO(Selector/SelectableChannel),ApacheMina,Netty(Java)

lPOE(Perl)

lTwisted(Python)

相反,boost::asioWindowsI/OCompletionPorts實現(xiàn)了Proactor模式,應(yīng)用面似乎要窄一些。當(dāng)然,ACE也實現(xiàn)了Proactor模式,不表。

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

while(!done)

{

inttimeout_ms=max(1000,getNextTimedCallback());

intretval=::poll(fds,nfds,timeout_ms);

if(retval<0){

處理錯誤

}else{

處理到期的timers

if(retval>0){

處理IO事件

}

}

}

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

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

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

3典型的多線程服務(wù)器的線程模型

這方面我能找到的文獻不多,大概有這么幾種:

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

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

3.使用non-blockingIO+IOmultiplexing。即JavaNIO的方式。

4.Leader/Follower等高級模式

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

Oneloopperthread

此種模型下,程序里的每個IO線程有一個eventloop(或者叫Reactor),用于處理讀寫和定時事件(無論周期性的還是單次的),代碼框架跟第2節(jié)一樣。

這種方式的好處是:

l線程數(shù)目基本固定,可以在程序啟動的時候設(shè)置,不會頻繁創(chuàng)建與銷毀。

l可以很方便地在線程間調(diào)配負(fù)載。

eventloop代表了線程的主循環(huán),需要讓哪個線程干活,就把timerIOchannel(TCPconnection)注冊到那個線程的loop里即可。對實時性有要求的connection可以單獨用一個線程;數(shù)據(jù)量大的connection可以獨占一個線程,并把數(shù)據(jù)處理任務(wù)分?jǐn)偟搅韼讉€線程中;其他次要的輔助性connections可以共享一個線程。

對于non-trivial的服務(wù)端程序,一般會采用non-blockingIO+IOmultiplexing,每個connection/acceptor都會注冊到某個Reactor上,程序里有多個Reactor,每個線程至多有一個Reactor

多線程程序?qū)?span style="font-family:Cambria;">Reactor提出了更高的要求,那就是“線程安全”。要允許一個線程往別的線程的loop里塞東西,這個loop必須得是線程安全的。

線程池

不過,對于沒有IO光有計算任務(wù)的線程,使用eventloop有點浪費,我會用有一種補充方案,即用blockingqueue實現(xiàn)的任務(wù)隊列(TaskQueue)

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

voidworker_thread()

{

while(!quit){

boost::function<void()>task=taskQueue.take();//thisblocks

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

}

}

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

啟動容量為N的線程池:

intN=num_of_computing_threads;

for(inti=0;i<N;++i){

create_thread(&worker_thread);//偽代碼:啟動線程

}

使用起來也很簡單:

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

taskQueue.post(task);

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

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

blocking_queue<T>是多線程編程的利器,它的實現(xiàn)可參照Java5util.concurrent里的(Array|Linked)BlockingQueue,通常C++可以用deque來做底層的容器。Java5里的代碼可讀性很高,代碼的基本結(jié)構(gòu)和教科書一致(1mutex2conditionvariables),健壯性要高得多。如果不想自己實現(xiàn),用現(xiàn)成的庫更好。(我沒有用過免費的庫,這里就不亂推薦了,有興趣的同學(xué)可以試試IntelThreadingBuildingBlocks里的concurrent_queue<T>。)

歸納

總結(jié)起來,我推薦的多線程服務(wù)端編程模式為:eventloopperthread+threadpool

leventloop用作non-blockingIO和定時器。

lthreadpool用來做計算,具體可以是任務(wù)隊列或消費者-生產(chǎn)者隊列。

以這種方式寫服務(wù)器程序,需要一個優(yōu)質(zhì)的基于Reactor模式的網(wǎng)絡(luò)庫來支撐,我只用過in-house的產(chǎn)品,無從比較并推薦市面上常見的C++網(wǎng)絡(luò)庫,抱歉。

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

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

此外,程序里或許還有個別執(zhí)行特殊任務(wù)的線程,比如logging,這對應(yīng)用程序來說基本是不可見的,但是在分配資源(CPUIO)的時候要算進去,以免高估了系統(tǒng)的容量。

4進程間通信與線程間通信

Linux下進程間通信(IPC)的方式數(shù)不勝數(shù),光UNPv2列出的就有:pipeFIFOPOSIX消息隊列、共享內(nèi)存、信號(signals)等等,更不必說Sockets了。同步原語(synchronizationprimitives)也很多,互斥器(mutex)、條件變量(conditionvariable)、讀寫鎖(reader-writerlock)、文件鎖(Recordlocking)、信號量(Semaphore)等等。

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

5進程間通信

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

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

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

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

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

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

使用TCP這種字節(jié)流(bytestream)方式通信,會有marshal/unmarshal的開銷,這要求我們選用合適的消息格式,準(zhǔn)確地說是wireformat。這將是我下一篇blog的主題,目前我推薦GoogleProtocolBuffers

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

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

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

6線程間同步

線程同步的四項原則,按重要性排列:

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

2.其次是使用高級的并發(fā)編程構(gòu)件,如TaskQueueProducer-ConsumerQueueCountDownLatch等等;

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

4.不自己編寫lock-free代碼,不去憑空猜測“哪種做法性能會更好”,比如spinlockvs.mutex

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

互斥器(mutex)

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

l用RAII手法封裝mutex的創(chuàng)建、銷毀、加鎖、解鎖這四個操作。

l只用非遞歸的mutex(即不可重入的mutex)。

l不手工調(diào)用lock()unlock()函數(shù),一切交給棧上的Guard對象的構(gòu)造和析構(gòu)函數(shù)負(fù)責(zé),Guard對象的生命期正好等于臨界區(qū)(分析對象在什么時候析構(gòu)是C++程序員的基本功)。這樣我們保證在同一個函數(shù)里加鎖和解鎖,避免在foo()里加鎖,然后跑到bar()里解鎖。

l在每次構(gòu)造Guard對象的時候,思考一路上(調(diào)用棧上)已經(jīng)持有的鎖,防止因加鎖順序不同而導(dǎo)致死鎖(deadlock)。由于Guard對象是棧上對象,看函數(shù)調(diào)用棧就能分析用鎖的情況,非常便利。

次要原則有:

l不使用跨進程的mutex,進程間通信只用TCPsockets

l加鎖解鎖在同一個線程,線程a不能去unlock線程b已經(jīng)鎖住的mutex。(RAII自動保證)

l別忘了解鎖。(RAII自動保證)

l不重復(fù)解鎖。(RAII自動保證)

l必要的時候可以考慮用PTHREAD_MUTEX_ERRORCHECK來排錯

RAII封裝這幾個操作是通行的做法,這幾乎是C++的標(biāo)準(zhǔn)實踐,后面我會給出具體的代碼示例,相信大家都已經(jīng)寫過或用過類似的代碼了。Java里的synchronized語句和C#using語句也有類似的效果,即保證鎖的生效期間等于一個作用域,不會因異常而忘記解鎖。

Mutex恐怕是最簡單的同步原語,安照上面的幾條原則,幾乎不可能用錯。我自己從來沒有違背過這些原則,編碼時出現(xiàn)問題都很快能招到并修復(fù)。

跑題:非遞歸的mutex

談?wù)勎覉猿质褂梅沁f歸的互斥器的個人想法。

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

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

毫無疑問recursivemutex使用起來要方便一些,因為不用考慮一個線程會自己把自己給鎖死了,我猜這也是JavaWindows默認(rèn)提供recursivemutex的原因。(Java語言自帶的intrinsiclock是可重入的,它的concurrent庫里提供ReentrantLockWindowsCRITICAL_SECTION也是可重入的。似乎它們都不提供輕量級的non-recursivemutex。)

正因為它方便,recursivemutex可能會隱藏代碼里的一些問題。典型情況是你以為拿到一個鎖就能修改對象了,沒想到外層代碼已經(jīng)拿到了鎖,正在修改(或讀取)同一個對象呢。具體的例子:

std::vector<Foo>foos;

MutexLockmutex;

voidpost(constFoo&f)

{

MutexLockGuardlock(mutex);

foos.push_back(f);

}

voidtraverse()

{

MutexLockGuardlock(mutex);

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

it->doit();

}

}

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

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

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

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

程序反正要死,不如死得有意義一點,讓驗尸官的日子好過些。

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

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

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

就像這樣:

voidpost(constFoo&f)

{

MutexLockGuardlock(mutex);

postWithLockHold(f);//不用擔(dān)心開銷,編譯器會自動內(nèi)聯(lián)的

}


//引入這個函數(shù)是為了體現(xiàn)代碼作者的意圖,盡管push_back通常可以手動內(nèi)聯(lián)

voidpostWithLockHold(constFoo&f)

{

foos.push_back(f);

}

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

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

voidpostWithLockHold(constFoo&f)

{

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

//...

}

另外,WithLockHold這個顯眼的后綴也讓程序中的誤用容易暴露出來。

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

我還沒有遇到過需要使用recursivemutex的情況,我想將來遇到了都可以借助wrapper改用non-recursivemutex,代碼只會更清晰。

===回到正題===

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

性能注腳:Linuxpthreadsmutex采用futex實現(xiàn),不必每次加鎖解鎖都陷入系統(tǒng)調(diào)用,效率不錯。WindowsCRITICAL_SECTION也是類似。

條件變量

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

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

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

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

寫成代碼是:

MutexLockmutex;

Conditioncond(mutex);

std::deque<int>queue;

intdequeue()

{

MutexLockGuardlock(mutex);

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

cond.wait();//這一步會原子地unlockmutex并進入blocking,不會與enqueue死鎖

}

assert(!queue.empty());

inttop=queue.front();

queue.pop_front();

returntop;

}

對于signal/broadcast端:

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

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

3.修改布爾表達(dá)式通常要用mutex保護(至少用作fullmemorybarrier

寫成代碼是:

voidenqueue(intx)

{

MutexLockGuardlock(mutex);

queue.push_back(x);

cond.notify();

}

上面的dequeue/enqueue實際上實現(xiàn)了一個簡單的unboundedBlockingQueue

條件變量是非常底層的同步原語,很少直接使用,一般都是用它來實現(xiàn)高層的同步措施,如BlockingQueueCountDownLatch

讀寫鎖與其他

讀寫鎖(Reader-Writerlock),讀寫鎖是個優(yōu)秀的抽象,它明確區(qū)分了readwrite兩種行為。需要注意的是,readerlock是可重入的,writerlock是不可重入(包括不可提升readerlock)的。這正是我說它“優(yōu)秀”的主要原因。

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

信號量(Semaphore),我沒有遇到過需要使用信號量的情況,無從談及個人經(jīng)驗。

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

封裝MutexLockMutexLockGuardCondition

本節(jié)把前面用到的MutexLockMutexLockGuardConditionclasses的代碼列出來,前面兩個classes沒多大難度,后面那個有點意思。

MutexLock封裝臨界區(qū)(Criticalsecion),這是一個簡單的資源類,用RAII手法[CCS:13]封裝互斥器的創(chuàng)建與銷毀。臨界區(qū)在Windows上是CRITICAL_SECTION,是可重入的;在Linux下是pthread_mutex_t

總結(jié)

以上是生活随笔為你收集整理的多线程服务器的常用编程模型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。