• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            陳碩的Blog

            置頂隨筆

            [置頂]《Linux 多線程服務(wù)端編程:使用 muduo C++ 網(wǎng)絡(luò)庫(kù)》網(wǎng)上書店預(yù)訂

            內(nèi)容簡(jiǎn)介

            本書主要講述采用現(xiàn)代 C++ 在 x86-64 Linux 上編寫多線程 TCP 網(wǎng)絡(luò)服務(wù)程序的主流常規(guī)技術(shù),重點(diǎn)講解一種適應(yīng)性較強(qiáng)的多線程服務(wù)器的編程模型,即 one loop per thread。這是在 Linux 下以 native 語(yǔ)言編寫用戶態(tài)高性能網(wǎng)絡(luò)程序最成熟的模式,掌握之后可順利地開發(fā)各類常見的服務(wù)端網(wǎng)絡(luò)應(yīng)用程序。本書以 muduo 網(wǎng)絡(luò)庫(kù)為例,講解這種編程模型的使用方法及注意事項(xiàng)。

            本書的宗旨是貴精不貴多。掌握兩種基本的同步原語(yǔ)就可以滿足各種多線程同步的功能需求,還能寫出更易用的同步設(shè)施。掌握一種進(jìn)程間通信方式和一種多線程網(wǎng)絡(luò)編程模型就足以應(yīng)對(duì)日常開發(fā)任務(wù),編寫運(yùn)行于公司內(nèi)網(wǎng)環(huán)境的分布式服務(wù)統(tǒng)。

            基本信息

            cover

            出版社:電子工業(yè)出版社

            頁(yè)數(shù):xvi+600

            定價(jià):人民幣89元

            ISBN:9787121192821

            豆瓣及網(wǎng)上書店預(yù)訂

            豆瓣:http://book.douban.com/subject/20471211/
            互動(dòng):http://product.china-pub.com/3021861
            亞馬遜:http://www.amazon.cn/dp/B00AYS2KL0
            當(dāng)當(dāng):http://product.dangdang.com/product.aspx?product_id=23162953
            京東:http://book.360buy.com/11163782.html

            試讀樣章

            前言與目錄:https://chenshuo-public.s3.amazonaws.com/pdf/preamble.pdf
            第1章:線程安全的對(duì)象生命期管理:https://chenshuo-public.s3.amazonaws.com/pdf/chap1.pdf
            第6章:muduo網(wǎng)絡(luò)庫(kù)簡(jiǎn)介:https://chenshuo-public.s3.amazonaws.com/pdf/chap6.pdf
            附錄:https://chenshuo-public.s3.amazonaws.com/pdf/appendix.pdf
            樣章合集下載:http://vdisk.weibo.com/s/mtupb 共150頁(yè),包括第 11.5 節(jié)。

            前言(節(jié)選)

            本書主要講述采用現(xiàn)代 C++ 在 x86-64 Linux 上編寫多線程 TCP 網(wǎng)絡(luò)服務(wù)程序的主流常規(guī)技術(shù),這也是我對(duì)過(guò)去 5 年編寫生產(chǎn)環(huán)境下的多線程服務(wù)端程序的經(jīng)驗(yàn)總結(jié)。本書重點(diǎn)講解多線程網(wǎng)絡(luò)服務(wù)器的一種 IO 模型,即 one loop per thread。這是一種適應(yīng)性較強(qiáng)的模型,也是 Linux 下以 native 語(yǔ)言編寫用戶態(tài)高性能網(wǎng)絡(luò)程序最成熟的模式, 掌握之后可順利地開發(fā)各類常見的服務(wù)端網(wǎng)絡(luò)應(yīng)用程序。本書以 muduo 網(wǎng)絡(luò)庫(kù)為例,講解這種編程模型的使用方法及注意事項(xiàng)。

            muduo 是一個(gè)基于非阻塞 IO 和事件驅(qū)動(dòng)的現(xiàn)代 C++ 網(wǎng)絡(luò)庫(kù),原生支持 one loop per thread 這種 IO 模型。muduo 適合開發(fā) Linux 下的面向業(yè)務(wù)的多線程服務(wù)端網(wǎng)絡(luò)應(yīng)用程序,其中“面向業(yè)務(wù)的網(wǎng)絡(luò)編程”的定義見附錄 A。 “現(xiàn)代 C++”指的不是 C++11 新標(biāo)準(zhǔn),而是 2005 年 TR1 發(fā)布之后的 C++ 語(yǔ)言和庫(kù)。 與傳統(tǒng) C++ 相比,現(xiàn)代 C++ 的變化主要有兩方面:資源管理(見第 1 章)與事件回調(diào)(見第 449 頁(yè))。

            本書不是多線程編程教程,也不是網(wǎng)絡(luò)編程教程,更不是 C++ 教程。讀者應(yīng)該已經(jīng)大致讀過(guò)《UNIX 環(huán)境高級(jí)編程》、《UNIX 網(wǎng)絡(luò)編程》、《C++ Primer》或與之內(nèi)容相近的書籍。本書不談 C++11,因?yàn)槟壳埃?012 年)主流的 Linux 服務(wù)端發(fā)行版的 g++ 版本都還停留在 4.4,C++11 進(jìn)入實(shí)用尚需一段時(shí)日。

            本書適用的硬件環(huán)境是主流 x86-64 服務(wù)器,多路多核 CPU、幾十 GB 內(nèi)存、千兆以太網(wǎng)互聯(lián)。除了第 5 章講診斷日志之外,本書不涉及文件 IO。

            本書分為四大部分,第 1 部分“C++ 多線程系統(tǒng)編程”考察多線程下的對(duì)象生命期管理、線程同步方法、多線程與 C++ 的結(jié)合、高效的多線程日志等。第 2 部分“muduo 網(wǎng)絡(luò)庫(kù)”介紹使用現(xiàn)成的非阻塞網(wǎng)絡(luò)庫(kù)編寫網(wǎng)絡(luò)應(yīng)用程序的方法,以及 muduo 的設(shè)計(jì)與實(shí)現(xiàn)。第 3 部分“工程實(shí)踐經(jīng)驗(yàn)談”介紹分布式系統(tǒng)的工程化開發(fā)方法和 C++ 在工程實(shí)踐中的功能特性取舍。第 4 部分“附錄”分享網(wǎng)絡(luò)編程和 C++ 語(yǔ)言的學(xué)習(xí)經(jīng)驗(yàn)。

            本書的宗旨是貴精不貴多。掌握兩種基本的同步原語(yǔ)就可以滿足各種多線程同步的功能需求,還能寫出更易用的同步設(shè)施。掌握一種進(jìn)程間通信方式和一種多線程網(wǎng)絡(luò)編程模型就足以應(yīng)對(duì)日常開發(fā)任務(wù),編寫運(yùn)行于公司內(nèi)網(wǎng)環(huán)境的分布式服務(wù)系統(tǒng)。(本書不涉及分布式存儲(chǔ)系統(tǒng),也不涉及 UDP。)

            術(shù)語(yǔ)與排版范例


            本書大量使用英文術(shù)語(yǔ),甚至有少量英文引文。設(shè)計(jì)模式的名字一律用英文,例如 Observer、Reactor、Singleton。在中文術(shù)語(yǔ)不夠突出時(shí),也會(huì)使用英文,例如 class、heap、event loop、STL algorithm 等。注意幾個(gè)中文 C++ 術(shù)語(yǔ):對(duì)象實(shí)體(instance) 、函數(shù)重載決議(resolution) 、模板具現(xiàn)化(instantiation) 、覆寫(override)虛函數(shù)、提領(lǐng)(dereference)指針。本書中的英語(yǔ)可數(shù)名詞一般不用復(fù)數(shù)形式,例如兩個(gè) class,6 個(gè) syscall;但有時(shí)會(huì)用 (s) 強(qiáng)調(diào)中文名詞是復(fù)數(shù)。fd 是文件描述符(file descriptor)的縮寫。“CPU 數(shù)目”一般指的是核(core)的數(shù)目。用諸如§11.5 表示本書第 11.5 節(jié),L42 表示上下文中出現(xiàn)的第 42 行代碼。[JCP]、[CC2e] 等是參考文獻(xiàn),見書末清單。

            代碼

            本書的示例代碼以開源項(xiàng)目的形式發(fā)布在 GitHub 上,
            地址是 http://github.com/chenshuo/recipes/http://github.com/chenshuo/muduo/ 。本書配套頁(yè)面提供全部源代碼打包下載,正文中出現(xiàn)的類似 recipes/thread 的路徑是壓縮包內(nèi)的相對(duì)路徑,讀者不難找到其對(duì)應(yīng)的 GitHub URL。

            本書假定讀者熟悉 diff -u 命令的輸出格式,用于表示代碼的改動(dòng)。

            本書正文中出現(xiàn)的代碼有時(shí)為了照顧排版而略有改寫,例如改變縮進(jìn)規(guī)則,去掉單行條件語(yǔ)句前后的花括號(hào)等。就編程風(fēng)格而論,應(yīng)以電子版代碼為準(zhǔn)。

            聯(lián)系方式


            郵箱:giantchen_at_gmail.com

            主頁(yè):http://chenshuo.com/book (正文和腳注中出現(xiàn)的 URL 可從這里找到。 )

            微博:http://weibo.com/giantchen

            博客:http://blog.csdn.net/Solstice

            代碼:http://github.com/chenshuo

            陳碩

            中國(guó)•香港

            posted @ 2013-01-11 12:43 陳碩 閱讀(4865) | 評(píng)論 (6)編輯 收藏

            2014年12月3日

            在 Boolan 網(wǎng)開講《網(wǎng)絡(luò)編程實(shí)戰(zhàn)》課程

            《網(wǎng)絡(luò)編程實(shí)戰(zhàn)》是一門以講解實(shí)例為主的課程,每一節(jié)都講一兩個(gè)網(wǎng)絡(luò)編程的例子程序,課程偏重 Linux 服務(wù)端 TCP 網(wǎng)絡(luò)編程。

            本課程要求聽課人員已經(jīng)讀過(guò)《Unix 網(wǎng)絡(luò)編程》,能寫簡(jiǎn)單的 TCP echo 服務(wù)。

             

            課程地址:http://boolan.com/course/4

            配套頁(yè)面:http://chenshuo.com/pnp

            posted @ 2014-12-03 01:50 陳碩 閱讀(4164) | 評(píng)論 (1)編輯 收藏

            2013年11月1日

            《Linux 多線程服務(wù)端編程:使用 muduo C++ 網(wǎng)絡(luò)庫(kù)》電子版上市

            《Linux 多線程服務(wù)端編程:使用 muduo C++ 網(wǎng)絡(luò)庫(kù)》 電子版已在京東上市銷售。

            購(gòu)買地址:http://e.jd.com/30149978.html

            閱讀效果:

            PC

            ebookpc

            iPad

            ebookipad

            目前京東的閱讀器沒有切白邊功能,值得改進(jìn)。

            posted @ 2013-11-01 10:35 陳碩 閱讀(8053) | 評(píng)論 (1)編輯 收藏

            2013年10月11日

            C++面試中string類的一種正確簡(jiǎn)明的寫法

            本文首發(fā)于酷殼網(wǎng) http://coolshell.cn/articles/10478.html

            先說(shuō)說(shuō)程序員(應(yīng)屆生)面試的一般過(guò)程,一輪面試(面對(duì)一到兩個(gè)面試官)一般是四、五十分鐘,面試官會(huì)問(wèn)兩三個(gè)編程問(wèn)題(通常是兩大一小),因此留給每個(gè)編程題的時(shí)間只有 20 分鐘。這 20 分鐘不光是寫代碼,還要跟面試官討論你的答案并解答提問(wèn),比如面試官拿過(guò)你的答案紙,問(wèn)某一行代碼如果修改會(huì)有什么后果。因此真正留給在紙上或白板上寫代碼的時(shí)間也就 10 分鐘上下。本文給出了一個(gè)能用 10 分鐘時(shí)間在紙上寫出來(lái)且不會(huì)有錯(cuò)的 String class,強(qiáng)調(diào)正確性及易實(shí)現(xiàn)(白板上寫也不會(huì)錯(cuò)),不強(qiáng)調(diào)效率與功能完備。

            本文的配套代碼位于 https://github.com/chenshuo/recipes/blob/master/string/StringTrivial.h

            全文:https://chenshuo.googlecode.com/files/CppEngineering.pdf

            posted @ 2013-10-11 10:59 陳碩 閱讀(4800) | 評(píng)論 (0)編輯 收藏

            2013年10月10日

            讀者來(lái)信與解答 1

            讀者來(lái)信用黑色,我的回答用藍(lán)色。經(jīng)過(guò)整理,接近對(duì)話體。


            > 陳碩,你好,
            >
            > 閱讀了你的書,很有收獲。
            > 但是沒有在moduo的源代碼里面找到實(shí)現(xiàn)線程模型11的例子。即one thread per loop + thread pool。
            > 謝謝。

            書第 173 頁(yè)圖 6-14 下面的第一段話,具體改動(dòng)方法參考前一頁(yè)的 diff。


            > 謝謝。
            >
            > 另外TcpConnection和Channel的生命周期管理有點(diǎn)問(wèn)題。
            > TcpConnection如果已經(jīng)被回收了,其包含的Channel也已經(jīng)被回收了。而這個(gè)時(shí)候在Channel::handleEvent()里面檢查tied_和tie_是危險(xiǎn)的。因?yàn)槠鋬?nèi)存已經(jīng)被回收了。
            >
            > 如果用戶保證TcpConnection被回收之后,不會(huì)再用Channel的裸指針,則沒有必要在TcpConnection::connectEstablished()中call tie().

            TcpConnection 回收之前,會(huì)調(diào)用 connectDestroyed,其中調(diào)用 channel_->remove();,這樣就不可能再會(huì)有 Channel::handleEvent() 被調(diào)用了。

            tie() 的作用是防止 Channel::handleEvent() 運(yùn)行期間其 owner 對(duì)象析構(gòu),導(dǎo)致 Channel 本身被銷毀。


            > > TcpConnection 回收之前,會(huì)調(diào)用 connectDestroyed,其中調(diào)用 channel_->remove();,這樣就不可能再會(huì)有 Channel::handleEvent() 被調(diào)用了
            > 這個(gè)時(shí)候會(huì)不會(huì)有race condition?假設(shè)現(xiàn)在有兩個(gè)active channels,處理頭一個(gè)的時(shí)候回收TcpConnection,而第二個(gè)channel剛好對(duì)應(yīng)這個(gè)connection。

            這時(shí)你沒有辦法強(qiáng)制銷毀 TcpConnection,只能降低其引用計(jì)數(shù),所以不會(huì)有問(wèn)題。你可以寫段代碼試試。

            > 另外底層的poller OS api是否保證unregister channel之后一定不會(huì)再有這個(gè)channel的事件,會(huì)清空內(nèi)核的已經(jīng)就緒的事件隊(duì)列?

            跟內(nèi)核沒關(guān)系,Poller class 在 unregister channel 之后就不可能調(diào)用其 handleEvent() 成員函數(shù)。

            > 那EPollPoller::fillActiveChannels()的改一改,“assert(it != channels_.end());”不再適用了,而且每次都個(gè)event都要查一次map。效率會(huì)有問(wèn)題。

            assert() 只有在 debug build 才執(zhí)行,不會(huì)影響效率。
            再說(shuō)每個(gè) event 都要涉及 read/write 等系統(tǒng)調(diào)用,開銷比“查一次 map”大得多,優(yōu)化這里是無(wú)用功。

            > 但這個(gè)assert()不是invalid了嗎?你可能之前在unregister channel的時(shí)候已經(jīng)從map里面remove掉了它。

            這個(gè) assert 是有效的,你再想想。


            > > tie() 的作用是防止 Channel::handleEvent() 運(yùn)行期間其 owner 對(duì)象析構(gòu),導(dǎo)致 Channel 本身被銷毀。

            > 這個(gè)也不太make sense。仍然有race conditon。在Channel::handleEvent()擁有g(shù)uard鎖定ownner之前,Channel::handleEvent()需要檢查其tied_。

            你再想想,tie 的作用是防止調(diào)用 handleEvent() 期間對(duì)象銷毀(比如調(diào)用 closeCallback 期間),不是也不可能防止調(diào)用 handleEvent() 之前對(duì)象銷毀。

            > 恩,是的。整個(gè)TcpConnection, Channel, EventLoop都是一個(gè)thread里面run的。

            posted @ 2013-10-10 12:17 陳碩 閱讀(1474) | 評(píng)論 (0)編輯 收藏

            2013年10月6日

            [轉(zhuǎn)載]賴勇浩:推薦《Linux 多線程服務(wù)器端編程》

            推薦《Linux 多線程服務(wù)器端編程》 

            賴勇浩(http://laiyonghao.com)

            最近,有一位朋友因?yàn)楣ぷ餍枰枰獜木W(wǎng)游的客戶端編程轉(zhuǎn)向服務(wù)器端編程,找我推薦一本書。我推薦了《Linux 多線程服務(wù)器端編程——使用 muduo C++ 網(wǎng)絡(luò)庫(kù)》給他,他在網(wǎng)上書店看了以后問(wèn)我為什么推薦這么厚一本書給他,正好這本書我已經(jīng)早就看完了,一直也想寫篇“書評(píng)”,就在這里多扯幾句。其實(shí)實(shí)在算不上書評(píng),原因有二:一是讀書的時(shí)候囫圇吞棗,理解不夠深刻,不深刻自然不能評(píng);二是這幾年雖然在 Linux 下寫服務(wù)器端的網(wǎng)絡(luò)程序,但很少用多線程,也很少用 C++,書里談的東西,算是不熟悉的領(lǐng)域,自然也不能亂評(píng)。所以今天這篇,應(yīng)當(dāng)是推薦,是為陳碩老師背書。

            繼續(xù)閱讀:

            http://blog.csdn.net/gzlaiyonghao/article/details/10366863

            http://book.douban.com/review/6249351/

            posted @ 2013-10-06 02:15 陳碩 閱讀(1595) | 評(píng)論 (0)編輯 收藏

            2013年9月9日

            用條件變量實(shí)現(xiàn)事件等待器的正確與錯(cuò)誤做法

            TL;DR 如果你能一眼看出 https://gist.github.com/chenshuo/6430925 中的那 8 個(gè) Waiter classes 哪些是對(duì)的哪些是錯(cuò)的,本文就不必看了。

            前幾天,我發(fā)了一條微博 http://weibo.com/1701018393/A7FrW7ZVd ,質(zhì)疑某本書對(duì) Pthreads 條件變量的封裝是錯(cuò)的,因?yàn)樗鼪]有把 mutex 的 lock()/unlock() 函數(shù)暴露出來(lái),導(dǎo)致無(wú)法實(shí)用。后來(lái)大家討論的分歧是這個(gè) cond class 是不是通用的條件變量封裝,還是只是一個(gè)特殊的“事件等待器”。作為事件等待器,其實(shí)現(xiàn)也是錯(cuò)的,因?yàn)榇嬖趤G失事件的可能,可以算是初學(xué)者使用條件變量的典型錯(cuò)誤。

            本文的代碼位于 recipes/thread/test/Waiter_test.cc,這里提到的某書的版本相當(dāng)于 Waiter1 class。

            我在拙作《Linux 多線程服務(wù)端編程:使用 muduo C++ 網(wǎng)絡(luò)庫(kù)》第 2.2 節(jié)總結(jié)了條件變量的使用要點(diǎn):

            條件變量只有一種正確使用的方式,幾乎不可能用錯(cuò)。對(duì)于 wait 端:
            1. 必須與 mutex 一起使用,該布爾表達(dá)式的讀寫需受此 mutex 保護(hù)。
            2. 在 mutex 已上鎖的時(shí)候才能調(diào)用 wait()。
            3. 把判斷布爾條件和 wait() 放到 while 循環(huán)中。

            對(duì)于 signal/broadcast 端:
            1. 不一定要在 mutex 已上鎖的情況下調(diào)用 signal (理論上)。
            2. 在 signal 之前一般要修改布爾表達(dá)式。
            3. 修改布爾表達(dá)式通常要用 mutex 保護(hù)(至少用作 full memory barrier)。
            4. 注意區(qū)分 signal 與 broadcast:“broadcast 通常用于表明狀態(tài)變化,signal 通常用于表示資源可用。(broadcast should generally be used to indicate state change rather than resource availability。)”

            如果用條件變量來(lái)實(shí)現(xiàn)一個(gè)“事件等待器/Waiter”,正確的做法是怎樣的?我的最終答案見 WaiterInMuduo class。“事件等待器”的一種用途是程序啟動(dòng)時(shí)等待初始化完成,也可以直接用 muduo::CountDownLatch 到達(dá)相同的目的,將初值設(shè)為 1 即可。

            以下根據(jù)微博上的討論過(guò)程給出幾個(gè)正確或錯(cuò)誤的版本,博大家一笑。只要記住 Pthread 的條件變量是邊沿觸發(fā)(edge trigger),即 signal()/broadcast() 只會(huì)喚醒已經(jīng)等在 wait() 上的線程(s),我們?cè)诰幋a時(shí)必須要考慮 signal() 早于 wait() 的可能,那么就很容易判斷以下各個(gè)版本的正誤了。代碼見 recipes/thread/test/Waiter_test.cc

            版本一:錯(cuò)誤。某書上的原始版,有丟失事件的可能。

            1

            版本二:錯(cuò)誤。lock() 之后再 signal(),同樣有丟失事件的可能。

            2

            版本三:錯(cuò)誤。引入了 bool signaled_; 條件,但沒有正確處理 spurious wakeup。

            版本四五六:正確。僅限 single waiter 使用。

            版本七:最佳。可供 multiple waiters 使用。

            版本八:錯(cuò)誤。存在 data race,且有丟失事件的可能。理由見 http://stackoverflow.com/questions/4544234/calling-pthread-cond-signal-without-locking-mutex

            總結(jié):使用條件變量,調(diào)用 signal() 的時(shí)候無(wú)法知道是否已經(jīng)有線程等待在 wait() 上。因此一般總是要先修改“條件”,使其為 true,再調(diào)用 signal();這樣 wait 線程先檢查“條件”,只有當(dāng)條件不成立時(shí)才去 wait(),避免了丟事件的可能。換言之,通過(guò)使用“條件”,將邊沿觸發(fā)(edge trigger)改為電平觸發(fā)(level trigger)。這里“修改條件”和“檢查條件”都必須在 mutex 保護(hù)下進(jìn)行,而且這個(gè) mutex 必須用于配合 wait()。

            思考題:如果用兩個(gè) mutex,一個(gè)用于保護(hù)“條件”,另一個(gè)專門用于和 cond 配合 wait(),會(huì)出現(xiàn)什么情況?

            最后注明一點(diǎn),http://stackoverflow.com/questions/6419117/signal-and-unlock-order 這篇帖子里對(duì) spurious wakeup 的解釋是錯(cuò)的,spurious wakeup 指的是一次 signal() 調(diào)用喚醒兩個(gè)或以上 wait()ing 的線程,或者沒有調(diào)用 signal() 卻有線程從 wait() 返回。manpage 里對(duì) Pthreads 系列函數(shù)的介紹非常到位,值得細(xì)讀。

            posted @ 2013-09-09 03:01 陳碩 閱讀(14150) | 評(píng)論 (21)編輯 收藏

            2013年8月18日

            用muduo實(shí)現(xiàn)memcached協(xié)議的例子

            最近花了兩天時(shí)間用 muduo 部分實(shí)現(xiàn)了 memcached 服務(wù)器協(xié)議,代碼位于 examples/memcached/server,能通過(guò) memcached 的大部分測(cè)試用例(incr/decr 還沒有實(shí)現(xiàn))。

            這不是 memcached 的替代品(它沒有實(shí)現(xiàn)LRU和超時(shí)功能,也沒有實(shí)現(xiàn)二進(jìn)制協(xié)議,更沒有自己管理內(nèi)存),而是一個(gè)網(wǎng)絡(luò)編程的示例(代碼只有 1000 行,比 memcached 小很多),展示 muduo 風(fēng)格的事件驅(qū)動(dòng)編程,以及將來(lái)性能優(yōu)化的試驗(yàn)品(換句話說(shuō),現(xiàn)在這個(gè)版本完全沒有在性能上做出任何努力)。讀過(guò) memcached 代碼的人可以對(duì)比這兩種編程風(fēng)格的區(qū)別,memcached 的 read/write 操作穿插于正常邏輯處理,而 muduo 的網(wǎng)絡(luò)數(shù)據(jù)讀寫是由庫(kù)完成,應(yīng)用程序只關(guān)心消息收發(fā),目前二者的基本 get/set 操作的性能相當(dāng)。

            現(xiàn)在 muduo 的 inspector 內(nèi)置了 gperftools 的遠(yuǎn)程 profiling 功能,memcached-debug 展示了其用法。

            為什么不必優(yōu)化 set 操作(含 set/add/update/append/prepend/cas 等)的性能?

            1. 比例。既然是 memcache,那么 get:set 的比例很高,10:1 甚至更高,因此優(yōu)化的重心應(yīng)該是 get 而非 set。

            假設(shè) memcached 能處理 100k QPS,再假設(shè)這些操作都是 set(其實(shí)應(yīng)該不到 10% 是 set),再假設(shè)所有的 set 都是串行執(zhí)行的(沒有并發(fā)),那么每次 set 的 CPU 時(shí)間不應(yīng)該超過(guò) 10 us(含服務(wù)器本地的網(wǎng)絡(luò)代碼運(yùn)行時(shí)間,但不含網(wǎng)絡(luò)延遲)。而實(shí)際上一次 set 的 CPU 時(shí)間最多是 2~3 us (用 memcached-footprint 程序測(cè)得),根本不值得優(yōu)化。

            2. 網(wǎng)絡(luò)帶寬。假設(shè)一次 set 操作的 key + value 的長(zhǎng)度是 1k bytes,TCP 的有效載荷帶寬按110MB/s估算,那么1kB數(shù)據(jù)在千兆網(wǎng)上的慣性延遲是 9us(傳輸延遲是幾十上百微秒,與此無(wú)關(guān)),也就是說(shuō)服務(wù)器的網(wǎng)卡收到這 1kB 數(shù)據(jù)需要花 9us 時(shí)間(從第一個(gè)字節(jié)到達(dá)到服務(wù)器到收完最后一個(gè)字節(jié)),那么在 set 耗時(shí) 2~3 us 的情況下再去優(yōu)化它是做無(wú)用功。

            3. 產(chǎn)生“需要更新的數(shù)據(jù)”的成本遠(yuǎn)大于 memcached set 的開銷。memcached 需要更新,往往是將已寫入數(shù)據(jù)庫(kù)的新數(shù)據(jù)放到 memcached 中,那么寫數(shù)據(jù)庫(kù)的開銷遠(yuǎn)遠(yuǎn)大于 memcached set 的開銷,優(yōu)化 set 對(duì)提升系統(tǒng)整體性能沒意義。

            posted @ 2013-08-18 12:59 陳碩 閱讀(3461) | 評(píng)論 (0)編輯 收藏

            2013年8月12日

            近期微博吐槽言論存檔,涉及“性能優(yōu)化”、C++陋習(xí)等

            寫C++程序的幾個(gè)陋習(xí):class 名以大寫 C 開頭,例如 CDate;成員變量以 m_ 開頭;變量采用匈牙利命名法;不知道何時(shí)禁用 copy-ctor/assign operator。前三個(gè)可能是從MFC那里傳下來(lái)的,當(dāng)時(shí)C++、class、OO是新玩意兒,要與 C struct 區(qū)分,現(xiàn)在還這么做就土了。C++的成員變量可用特殊命名格式,加下劃線后綴即可(加下劃線前綴是錯(cuò)的)。但在 Java 里不必模仿 C++ 的這種成員變量命名方式,IDE 可以讓成員變量以不同的顏色顯示,與局部變量區(qū)分,根本無(wú)需特殊命名。寫程序就怕把以前的編程經(jīng)驗(yàn)不加區(qū)分地應(yīng)用到新語(yǔ)言中,寫成四不像,不地道。

            知道禁用 copy-ctor/assign operator 是 C++ 程序員的試金石。在看到一個(gè)開源項(xiàng)目時(shí),我一般會(huì)先查看其 RAII handle class 是否禁用了 copy-ctor/assign operator(例如 Thread、Mutex、CondVar、Connection),如果沒有,對(duì)其第一印象就很差了。

            關(guān)于 class 命名風(fēng)格,Google、LLVM、Mozilla、muduo 都采用 Pascal 風(fēng)格(LikeThis),例如 EventLoop、SudokuSolver 等等。正巧它們也都是用 2 格縮進(jìn)的,可以用 clang-format 自動(dòng)格式化代碼。

            順便說(shuō)說(shuō)我不認(rèn)同的兩個(gè) C++ 教條:1. 用nullptr替換NULL,2. 用cstdio頭文件替換stdio.h。

            因?yàn)槔?gettimeofday(&tv, NULL) 這種系統(tǒng)函數(shù)傳個(gè) nullptr 進(jìn)去實(shí)在是違和,現(xiàn)在用 NULL 也能達(dá)到 nullptr 的好處,大不了在某個(gè)頭文件里define一下就行。這條將來(lái)或許會(huì)變。

            另外 ctime 頭文件沒定義 std::gmtime_r,而 time.h 定義了 ::gmtime_r。我可不想去背哪些函數(shù)是 C 語(yǔ)言的哪些是 Posix 的,哪些頭文件是 C 語(yǔ)言的哪些是 Posix 的(在Linux下,二者基本不分家)。為了用幾個(gè)系統(tǒng)函數(shù)(例如 fcntl() ),我該 include cfcntl 還是 fcntl.h?用線程是 cpthread 還是 pthread.h?我總是記不住 memset() 的參數(shù)順序,因此一般用 bzero() 代替,但是 manpage 說(shuō) bzero() 聲明于 strings.h,那我要不要考慮試試 cstrings 呢?何必給自己找麻煩,C++ 標(biāo)準(zhǔn)庫(kù)之外的內(nèi)容干脆統(tǒng)一用 .h 頭文件好了。

            性能優(yōu)化?

            有些人常常把“性能”掛在嘴邊,而且其以“提高性能”為理由的“優(yōu)化措施”往往不到點(diǎn)子上,只增加了復(fù)雜性和維護(hù)難度,降低了代碼質(zhì)量。這屬于決策點(diǎn)找偏了。我發(fā)現(xiàn)初學(xué)者往往過(guò)分關(guān)注微觀(語(yǔ)句級(jí))性能,比方說(shuō)關(guān)心 while(true) 和 for(;;) 哪個(gè)更快,++i 與 i=i+1 哪個(gè)更快,i/=16 和 i >>= 4 哪個(gè)快等等,而忽視了現(xiàn)代編譯器的優(yōu)化能力。

            有的人談性能優(yōu)化,一是拿不出具體的合理的性能目標(biāo),只想越快越好,二是不能實(shí)際準(zhǔn)確測(cè)量驗(yàn)證性能數(shù)據(jù),憑感覺和過(guò)時(shí)經(jīng)驗(yàn)行事。在編碼的時(shí)候,遇到兩種做法都可行,決策辦法是憑感覺猜選“性能會(huì)更好”的一種,而忽視了其他更重要的因素。可讀性和性能的典型關(guān)系如下圖,有多少場(chǎng)合是值得為了性能而犧牲代碼的可讀性和可維護(hù)性呢?我希望自己的代碼位于第 3 區(qū),而一些人以為自己的代碼是在第 4 區(qū),其實(shí)是在第 1 區(qū)。

            read-perf

            能在第 4 區(qū)寫代碼的人屬于鳳毛麟角,有時(shí)候你費(fèi)勁優(yōu)化了半天,結(jié)果新CPU加了幾條指令,直接在硬件層面把問(wèn)題解決了。現(xiàn)在一些人動(dòng)不動(dòng)就要挽起袖子自己寫內(nèi)存池,號(hào)稱能提高性能,真當(dāng) Ulrich Drepper 是水貨?(書第 12.2.8 節(jié)“有必要自行定制內(nèi)存分配器嗎”)你打算如何測(cè)試內(nèi)存分配器(malloc)的性能?有哪些指標(biāo)?有哪些影響因素需要控制或模擬(比如線程數(shù))?你的測(cè)試結(jié)果是否反映實(shí)際場(chǎng)景?

            雜項(xiàng)

            有人問(wèn)為什么我說(shuō)“poco不是服務(wù)端C++網(wǎng)絡(luò)庫(kù)”( http://www.oschina.net/question/12_120943 ),雖然它也提供了reactor?因?yàn)樗膔eactor用的是 Socket::select(),雖然后者包裝了epoll,但看其實(shí)現(xiàn)就知道,它每次調(diào)用都會(huì)創(chuàng)建并銷毀 epoll fd,然后重建整個(gè)watch list,沒有哪個(gè)服務(wù)端網(wǎng)絡(luò)庫(kù)會(huì)這么做。

            嗯,世界上有兩種網(wǎng)絡(luò)編程:網(wǎng)絡(luò)編程和Windows網(wǎng)絡(luò)編程。

            posted @ 2013-08-12 13:54 陳碩 閱讀(3737) | 評(píng)論 (6)編輯 收藏

            2013年7月17日

            《Linux多線程服務(wù)端編程:使用muduo C++網(wǎng)絡(luò)庫(kù)》上市半年重印兩次,總印數(shù)達(dá)到了9000冊(cè)

            《Linux多線程服務(wù)端編程:使用muduo C++網(wǎng)絡(luò)庫(kù)》這本書自今年一月上市以來(lái),半年之內(nèi)已經(jīng)重印兩次(加上首印,一共是三次印刷),總印數(shù)達(dá)到了9000冊(cè),這在技術(shù)書里已經(jīng)算是相當(dāng)不錯(cuò)的成績(jī)。本書購(gòu)買方式見配套網(wǎng)站 http://chenshuo.com/book

            以下談一談這本書的寫作背景與內(nèi)容取舍的原因。

            參加工作以來(lái),我編寫并維護(hù)了若干C++/Java多線程網(wǎng)絡(luò)服務(wù)程序,這本書總結(jié)了我在開發(fā)維護(hù)這類服務(wù)程序方面的經(jīng)驗(yàn)。工作中,我沒有寫過(guò)單線程的網(wǎng)絡(luò)服務(wù)程序,沒有寫過(guò)C語(yǔ)言的網(wǎng)絡(luò)服務(wù)程序,也沒有寫過(guò)運(yùn)行在Windows下的網(wǎng)絡(luò)服務(wù)程序,因此本書不涉及這些內(nèi)容。

            在“Linux服務(wù)端開發(fā)”這一背景下,這本書主要講三個(gè)方面的內(nèi)容[1]:現(xiàn)代C++、多線程、網(wǎng)絡(luò)編程,分別對(duì)應(yīng)書的第3、1、2部分。這不是一本入門書,本書的讀者應(yīng)該在以上三方面已經(jīng)具備相當(dāng)?shù)幕A(chǔ)[2]:網(wǎng)絡(luò)編程方面,能輕松讀懂6.1節(jié)的兩個(gè)Python程序;C++方面,對(duì)12.8節(jié)的代碼不感到陌生;多線程方面,能明白第1章要解決什么問(wèn)題。

            第9章“分布式系統(tǒng)工程實(shí)踐”詳細(xì)介紹了這本書的應(yīng)用背景,即開發(fā)公司內(nèi)部的分布式服務(wù)系統(tǒng),書中的很多決策(design decision)和技術(shù)取舍(trade-off)是在這一應(yīng)用場(chǎng)景下做出的。以下是各章直接的交叉引用關(guān)系圖(沒有計(jì)算引用次數(shù)),其中第0章是前言,字母章節(jié)是附錄。可見第9章是被引用最多的一章,某種意義上可以說(shuō)第9章既是本書的先決條件,又是本書的終極目標(biāo)。由于章節(jié)之間存在眾多的交叉引用,去掉任何一章都會(huì)破壞內(nèi)容的完整性。

            ref

            這本書的書名原本打算叫“Linux C++ 多線程系統(tǒng)編程”。寫完之后發(fā)現(xiàn),與其他Unix/Linux系統(tǒng)編程方面的書不同,這本書有明確的應(yīng)用場(chǎng)景,因此可以砍掉很多內(nèi)容,突出重點(diǎn)。甚至可以說(shuō)我主要講別的書沒有講到的內(nèi)容。這不是一本面面俱到的書,因此最終的書名也就不叫“系統(tǒng)編程”了。

            同時(shí),我認(rèn)為很多教科書上介紹的一些做法是過(guò)時(shí)的(signal),一些是不推薦使用的(從外部終止線程、TCP OOB數(shù)據(jù)),一些是大多數(shù)情況下沒必要使用的(內(nèi)存池、lock-free 編程)。作為全面的教材和手冊(cè),把這些內(nèi)容放進(jìn)去可以理解。但是這本書定位是經(jīng)驗(yàn)總結(jié),我略去了教科書上那些基本用不到的知識(shí)點(diǎn),以免喧賓奪主,也建議讀者不要把精力花在那些次要問(wèn)題上。

            • 這本書沒有花很大的篇幅去講signal,而是在第4.10節(jié)說(shuō)明多線程程序不要使用signal作為IPC。并且,在muduo-protorpc的示例中給出了Linux專有的signalfd(2)的用法,可以避免傳統(tǒng)signal handler的常見陷阱,也更符合UNIX的“everything is a file”哲學(xué)。第4.4節(jié)說(shuō)明不要從外部終止線程,因此也就不必去細(xì)究Pthreads cancellation point了。多線程程序最好不要fork()(第4.9節(jié))。
            • 這本書沒介紹daemon進(jìn)程,我認(rèn)為daemon是過(guò)時(shí)的做法。因?yàn)閐aemon進(jìn)程的父進(jìn)程是init(1),配置文件在本機(jī),不便于多機(jī)統(tǒng)一監(jiān)控與管理(第9.8節(jié))。(注:如果是第三方標(biāo)準(zhǔn)的服務(wù)程序,又不需要經(jīng)常升級(jí)或改配置重啟,并且一旦崩潰,重啟就能繼續(xù)服務(wù),那么做成 daemon 讓init(1)接管是可以的,比如ntpd、sshd等。這里談的是自己開發(fā)維護(hù)的服務(wù)程序。)另外,Java/Python/Go寫的服務(wù)程序似乎也沒有做成daemon的習(xí)慣,C++程序沒有理由要特殊對(duì)待。補(bǔ)充一點(diǎn),Linux的進(jìn)程管理機(jī)制很落后(從UNIX繼承而來(lái)),子進(jìn)程退出的事件只能被父進(jìn)程以SIGCHLD信號(hào)的方式收到(而且這個(gè)signal可能丟失),kill(pid) 也存在很多race condition(你怎么保證pid在kill之前的一瞬間還代表你想kill的那個(gè)進(jìn)程,而不是一個(gè)新啟動(dòng)的進(jìn)程?close(fd)就不會(huì)有這種 race condition。)。這些困難在用戶態(tài)無(wú)法克服,只能修改內(nèi)核,引入新的系統(tǒng)調(diào)用才能治本。例如 FreeBSD 9.0 引入了 pdfork()/pdkill() 等,將子進(jìn)程變成文件描述符,這樣就能用IO事件框架統(tǒng)一處理了,也符合UNIX的“everything is a file”哲學(xué)。但愿Linux內(nèi)核也能盡快引入類似的系統(tǒng)調(diào)用,減輕程序員的負(fù)擔(dān)。
            • 這本書沒有講內(nèi)存池,而是說(shuō)明不是每個(gè)程序都要自己寫內(nèi)存池(§12.2.8)。這本書也沒有把“避免內(nèi)存碎片”掛在嘴邊,而是論證為什么一般的程序不必在意它(§A.1.8);
            • 這本書只關(guān)注Linux,不考慮移植性。它推薦使用Linux專有的gettid()系統(tǒng)調(diào)用作為線程標(biāo)識(shí)(第4.3節(jié)),而不是用pthread_self()。
            • 這本書不講POSIX中五花八門的定時(shí)函數(shù),而專講用Linux特有的timerfd來(lái)實(shí)現(xiàn)高精度定時(shí)(§7.8.2),因?yàn)樗芊奖愕厝谌隝O事件處理框架。muduo直接使用C++標(biāo)準(zhǔn)庫(kù)來(lái)管理定時(shí)器,而不是自己實(shí)現(xiàn)小頂堆(heap),這樣可以簡(jiǎn)化實(shí)現(xiàn)(§8.2.1)。
            • 這本書只講mutex和condition variable作為最基礎(chǔ)的線程同步手段(第2章),并且我認(rèn)為一個(gè)C++多線程程序代碼里不應(yīng)該直接出現(xiàn)pthread_mutex_lock之類的基本Pthreads調(diào)用。本書進(jìn)一步建議只使用非遞歸的mutex(§2.1.1),這與某些網(wǎng)上文章的推薦正好相反。這本書第2.3節(jié)甚至建議不要使用讀寫鎖和信號(hào)量(semaphore),因?yàn)橐皇侨菀子缅e(cuò),二是不見得能提高性能。mutex和condition variable是完備的,能實(shí)現(xiàn)多種更易用的同步設(shè)施,例如CountDownLatch和BlockingQueue。§12.8.3的代碼展示了用BlockingQueue和ThreadPool控制并發(fā)度的手法,做到了“No locks. No condition variables. No callbacks.”
            • 這本書不講lock-free編程,因?yàn)榫帉懣煽康膌ock-free代碼并分析驗(yàn)證其正確性的難度遠(yuǎn)大于編寫普通的使用mutex和condition variable的多線程代碼,后者已經(jīng)有了相當(dāng)成熟的理論和工具。我認(rèn)為lock-free不是每個(gè)多線程程序員應(yīng)該掌握的技術(shù),它投入高而用處少,可以適當(dāng)了解,但不值得每個(gè)人都去深究。只需要少數(shù)人用它實(shí)現(xiàn)封裝好的數(shù)據(jù)結(jié)構(gòu),像我這樣的普通人就可以受益。
            • 這本書只講BSD Sockets作為進(jìn)程間通信的手段,并且只用TCP長(zhǎng)連接(§3.4)。這樣就砍掉了pipe、FIFO、POSIX message queue、shared memory、STREAMS、UNIX domain socket等等內(nèi)容,因?yàn)樗鼈兌贾幌薇緳C(jī)進(jìn)程間通信,無(wú)法擴(kuò)展到多機(jī)。
            • 網(wǎng)絡(luò)編程方面(第6、7章),這本書不講Sockets API的基本用法,而且代碼中也不會(huì)直接使用它們。我認(rèn)為在程序中直接使用 Sockets API是初學(xué)者的做法,當(dāng)寫一個(gè)新網(wǎng)絡(luò)服務(wù)程序,如果一開始考慮的是怎么組織accept、read、epoll_wait等調(diào)用,這種做法無(wú)異于用鉛筆刀鋸大樹,事倍功半,也不利于將來(lái)的功能擴(kuò)展和維護(hù)。稍微像樣點(diǎn)的公司都會(huì)用成熟的網(wǎng)絡(luò)庫(kù)(不一定開源),把網(wǎng)絡(luò)編程的復(fù)雜性封裝進(jìn)去,暴露出良好易用的接口,讓開發(fā)人員使用更高層的building blocks(消息傳遞或RPC)從功能的角度去設(shè)計(jì)程序,避免一次次反復(fù)掉到TCP網(wǎng)絡(luò)編程的坑里。多個(gè)服務(wù)程序共享相同的基礎(chǔ)庫(kù)和事件處理框架的益處是顯而易見的,一方面把網(wǎng)絡(luò)編程的復(fù)雜性集中到一起,避免每個(gè)團(tuán)隊(duì)都去踏一遍坑;另一方面,基礎(chǔ)庫(kù)的bug修復(fù)與性能優(yōu)化能惠及用到它的全部服務(wù)程序;最后,程序結(jié)構(gòu)上的相似性讓編程經(jīng)驗(yàn)更加通用,多個(gè)服務(wù)程序在功能、性能、正確性等方面具有共性,能舉一反三觸類旁通,降低將來(lái)開發(fā)維護(hù)的成本。應(yīng)該避免每個(gè)程序都另起爐灶,單獨(dú)設(shè)計(jì)其IO事件處理結(jié)構(gòu)。
            • 這本書只講非阻塞IO結(jié)合IO復(fù)用(IO-Multiplexing)這一種并發(fā)風(fēng)格(歸納為三個(gè)半事件),并介紹在多線程下的擴(kuò)展(one loop per thread)。IO復(fù)用方面,本書只講level-trigger,不講edge-trigger。一方面目前沒有up to date的測(cè)試表明ET更快,相反,我認(rèn)為L(zhǎng)T在讀取數(shù)據(jù)時(shí)可以節(jié)約一次read()調(diào)用(§8.7.2);另一方面,LT模式更容易與其他第三方庫(kù)結(jié)合(§7.15)。多線程程序管理并發(fā)socket fd有很多風(fēng)格可供選擇,例如epoll fd是多個(gè)線程共享一個(gè)(多對(duì)一)還是每個(gè)線程有自己的epoll fd(一對(duì)一),每個(gè)socket fd是只屬于一個(gè)epoll fd(多對(duì)一)還是可以同時(shí)屬于多個(gè) epoll fd(多對(duì)多),每個(gè)socket fd是只能被固定的一個(gè)線程讀寫還是可以被多個(gè)線程讀寫(如果是后者,那么讀寫的時(shí)候是加鎖還是使用ONESHOT)。以上不是每種都可行,本書也沒有一一加以分析,而是建議使用one loop per thread這種適用性較強(qiáng)的風(fēng)格,首先是正確性容易驗(yàn)證,其次是性能也能滿足要求。
            • 本書不講IPv6,因?yàn)槟壳笆澜缟献畲蟮墓镜姆?wù)機(jī)群也用不完一個(gè)私有A類地址(10.0.0.0/8)。本書不講UDP,因?yàn)椤禪nix網(wǎng)絡(luò)編程》已經(jīng)講得很好了。
            • 這本書舉的網(wǎng)絡(luò)編程的例子不再是簡(jiǎn)單的echo服務(wù),而是有格式(因此引入codec)、多連接之間會(huì)交換數(shù)據(jù)的網(wǎng)絡(luò)程序,更接近業(yè)務(wù)場(chǎng)景,也借機(jī)講解如何避免TCP網(wǎng)絡(luò)編程的常見陷阱。并且在示例代碼中給出了分布式單詞計(jì)數(shù)、多機(jī)求中位數(shù)等稍微復(fù)雜一點(diǎn)的程序。
            • 在C++方面,這本書沒有介紹動(dòng)態(tài)鏈接庫(kù)熱更新這種“高級(jí)”技術(shù),而是說(shuō)明,在分布式系統(tǒng)中,為了部署方便,應(yīng)該從源碼編譯全部的庫(kù),與主程序鏈接為一個(gè)standalone的可執(zhí)行文件,以減小對(duì)運(yùn)行環(huán)境的依賴(第10章)。第11章還討論了程序庫(kù)與應(yīng)用程序之間的接口設(shè)計(jì)。

            “信息”按照香農(nóng)的定義,是“減少不確定性”,這本書包含的信息正是減少選用編程設(shè)施(facilities)方面的不確定性,讓讀者集中精力攻克本質(zhì)問(wèn)題。這本書介紹的方法不一定對(duì)于每個(gè)應(yīng)用場(chǎng)景都是最好的,但肯定是簡(jiǎn)便易行的,是時(shí)間成本、功能、性能的一種合理折中。


            [1] 這本書前言的第一句話“本書主要講述采用現(xiàn)代 C++ 在 x86-64 Linux 上編寫多線程 TCP 網(wǎng)絡(luò)服務(wù)程序的主流常規(guī)技術(shù)”,封面印著“示范在多核時(shí)代采用現(xiàn)代 C++ 編寫多線程 TCP 網(wǎng)絡(luò)服務(wù)器的正規(guī)做法”。

            [2] 前言寫到:讀者應(yīng)該已經(jīng)大致讀過(guò)《現(xiàn)代操作系統(tǒng)》、《UNIX 環(huán)境高級(jí)編程》、《UNIX 網(wǎng)絡(luò)編程》、《C++ Primer》或與之內(nèi)容相近的書籍,熟悉基本概念,并掌握 Pthreads 和 Sockets API 的常規(guī)用法。

            posted @ 2013-07-17 11:17 陳碩 閱讀(2577) | 評(píng)論 (3)編輯 收藏

            2013年1月28日

            為什么多線程讀寫 shared_ptr 要加鎖?

            陳碩(giantchen_AT_gmail_DOT_com)

            2012-01-28

            我在《Linux 多線程服務(wù)端編程:使用 muduo C++ 網(wǎng)絡(luò)庫(kù)》第 1.9 節(jié)“再論 shared_ptr 的線程安全”中寫道:

            (shared_ptr)的引用計(jì)數(shù)本身是安全且無(wú)鎖的,但對(duì)象的讀寫則不是,因?yàn)?shared_ptr 有兩個(gè)數(shù)據(jù)成員,讀寫操作不能原子化。根據(jù)文檔(http://www.boost.org/doc/libs/release/libs/smart_ptr/shared_ptr.htm#ThreadSafety), shared_ptr 的線程安全級(jí)別和內(nèi)建類型、標(biāo)準(zhǔn)庫(kù)容器、std::string 一樣,即:

            • 一個(gè) shared_ptr 對(duì)象實(shí)體可被多個(gè)線程同時(shí)讀取(文檔例1);

            • 兩個(gè) shared_ptr 對(duì)象實(shí)體可以被兩個(gè)線程同時(shí)寫入(例2),“析構(gòu)”算寫操作;

            如果要從多個(gè)線程讀寫同一個(gè) shared_ptr 對(duì)象,那么需要加鎖(例3~5)。

            請(qǐng)注意,以上是 shared_ptr 對(duì)象本身的線程安全級(jí)別,不是它管理的對(duì)象的線程安全級(jí)別。

            后文(p.18)則介紹如何高效地加鎖解鎖。本文則具體分析一下為什么“因?yàn)?shared_ptr 有兩個(gè)數(shù)據(jù)成員,讀寫操作不能原子化”使得多線程讀寫同一個(gè) shared_ptr 對(duì)象需要加鎖。這個(gè)在我看來(lái)顯而易見的結(jié)論似乎也有人抱有疑問(wèn),那將導(dǎo)致災(zāi)難性的后果。本文以 boost::shared_ptr 為例,與 std::shared_ptr 可能略有區(qū)別。

            shared_ptr 的數(shù)據(jù)結(jié)構(gòu)

            shared_ptr 是引用計(jì)數(shù)型(reference counting)智能指針,幾乎所有的實(shí)現(xiàn)都采用在堆(heap)上放個(gè)計(jì)數(shù)值(count)的辦法(除此之外理論上還有用循環(huán)鏈表的辦法,不過(guò)沒有實(shí)例)。具體來(lái)說(shuō),shared_ptr<Foo> 包含兩個(gè)成員,一個(gè)是指向 Foo 的指針 ptr,另一個(gè)是 ref_count 指針(其類型不一定是原始指針,有可能是 class 類型,但不影響這里的討論),指向堆上的 ref_count 對(duì)象。ref_count 對(duì)象有多個(gè)成員,具體的數(shù)據(jù)結(jié)構(gòu)如圖 1 所示,其中 deleter 和 allocator 是可選的。

            sp0

            圖 1:shared_ptr 的數(shù)據(jù)結(jié)構(gòu)。

            為了簡(jiǎn)化并突出重點(diǎn),后文只畫出 use_count:

            sp1

            以上是 shared_ptr<Foo> x(new Foo); 對(duì)應(yīng)的內(nèi)存數(shù)據(jù)結(jié)構(gòu)。

            如果再執(zhí)行 shared_ptr<Foo> y = x; 那么對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)如下。

            sp2

            但是 y=x 涉及兩個(gè)成員的復(fù)制,這兩步拷貝不會(huì)同時(shí)(原子)發(fā)生。

            中間步驟 1,復(fù)制 ptr 指針:

            sp3

            中間步驟 2,復(fù)制 ref_count 指針,導(dǎo)致引用計(jì)數(shù)加 1:

            sp4

            步驟1和步驟2的先后順序跟實(shí)現(xiàn)相關(guān)(因此步驟 2 里沒有畫出 y.ptr 的指向),我見過(guò)的都是先1后2。

            既然 y=x 有兩個(gè)步驟,如果沒有 mutex 保護(hù),那么在多線程里就有 race condition。

            多線程無(wú)保護(hù)讀寫 shared_ptr 可能出現(xiàn)的 race condition

            考慮一個(gè)簡(jiǎn)單的場(chǎng)景,有 3 個(gè) shared_ptr<Foo> 對(duì)象 x、g、n:

            • shared_ptr<Foo> g(new Foo); // 線程之間共享的 shared_ptr
            • shared_ptr<Foo> x; // 線程 A 的局部變量
            • shared_ptr<Foo> n(new Foo); // 線程 B 的局部變量

            一開始,各安其事。

            sp5

            線程 A 執(zhí)行 x = g; (即 read g),以下完成了步驟 1,還沒來(lái)及執(zhí)行步驟 2。這時(shí)切換到了 B 線程。

            sp6

            同時(shí)編程 B 執(zhí)行 g = n; (即 write G),兩個(gè)步驟一起完成了。

            先是步驟 1:

            sp7

            再是步驟 2:

            sp8

            這是 Foo1 對(duì)象已經(jīng)銷毀,x.ptr 成了空懸指針!

            最后回到線程 A,完成步驟 2:

            sp9

            多線程無(wú)保護(hù)地讀寫 g,造成了“x 是空懸指針”的后果。這正是多線程讀寫同一個(gè) shared_ptr 必須加鎖的原因。

            當(dāng)然,race condition 遠(yuǎn)不止這一種,其他線程交織(interweaving)有可能會(huì)造成其他錯(cuò)誤。

            思考,假如 shared_ptr 的 operator= 實(shí)現(xiàn)是先復(fù)制 ref_count(步驟 2)再?gòu)?fù)制 ptr(步驟 1),會(huì)有哪些 race condition?

            雜項(xiàng)

            shared_ptr 作為 unordered_map 的 key

            如果把 boost::shared_ptr 放到 unordered_set 中,或者用于 unordered_map 的 key,那么要小心 hash table 退化為鏈表。http://stackoverflow.com/questions/6404765/c-shared-ptr-as-unordered-sets-key/12122314#12122314

            直到 Boost 1.47.0 發(fā)布之前,unordered_set<std::shared_ptr<T> > 雖然可以編譯通過(guò),但是其 hash_value 是 shared_ptr 隱式轉(zhuǎn)換為 bool 的結(jié)果。也就是說(shuō),如果不自定義hash函數(shù),那么 unordered_{set/map} 會(huì)退化為鏈表。https://svn.boost.org/trac/boost/ticket/5216

            Boost 1.51 在 boost/functional/hash/extensions.hpp 中增加了有關(guān)重載,現(xiàn)在只要包含這個(gè)頭文件就能安全高效地使用 unordered_set<std::shared_ptr> 了。

            這也是 muduo 的 examples/idleconnection 示例要自己定義 hash_value(const boost::shared_ptr<T>& x) 函數(shù)的原因(書第 7.10.2 節(jié),p.255)。因?yàn)?Debian 6 Squeeze、Ubuntu 10.04 LTS 里的 boost 版本都有這個(gè) bug。

            為什么圖 1 中的 ref_count 也有指向 Foo 的指針?

            shared_ptr<Foo> sp(new Foo) 在構(gòu)造 sp 的時(shí)候捕獲了 Foo 的析構(gòu)行為。實(shí)際上 shared_ptr.ptr 和 ref_count.ptr 可以是不同的類型(只要它們之間存在隱式轉(zhuǎn)換),這是 shared_ptr 的一大功能。分 3 點(diǎn)來(lái)說(shuō):

            1. 無(wú)需虛析構(gòu);假設(shè) Bar 是 Foo 的基類,但是 Bar 和 Foo 都沒有虛析構(gòu)。

            shared_ptr<Foo> sp1(new Foo); // ref_count.ptr 的類型是 Foo*

            shared_ptr<Bar> sp2 = sp1; // 可以賦值,自動(dòng)向上轉(zhuǎn)型(up-cast)

            sp1.reset(); // 這時(shí) Foo 對(duì)象的引用計(jì)數(shù)降為 1

            此后 sp2 仍然能安全地管理 Foo 對(duì)象的生命期,并安全完整地釋放 Foo,因?yàn)槠?ref_count 記住了 Foo 的實(shí)際類型。

            2. shared_ptr<void> 可以指向并安全地管理(析構(gòu)或防止析構(gòu))任何對(duì)象;muduo::net::Channel class 的 tie() 函數(shù)就使用了這一特性,防止對(duì)象過(guò)早析構(gòu),見書 7.15.3 節(jié)。

            shared_ptr<Foo> sp1(new Foo); // ref_count.ptr 的類型是 Foo*

            shared_ptr<void> sp2 = sp1; // 可以賦值,F(xiàn)oo* 向 void* 自動(dòng)轉(zhuǎn)型

            sp1.reset(); // 這時(shí) Foo 對(duì)象的引用計(jì)數(shù)降為 1

            此后 sp2 仍然能安全地管理 Foo 對(duì)象的生命期,并安全完整地釋放 Foo,不會(huì)出現(xiàn) delete void* 的情況,因?yàn)?delete 的是 ref_count.ptr,不是 sp2.ptr。

            3. 多繼承。假設(shè) Bar 是 Foo 的多個(gè)基類之一,那么:

            shared_ptr<Foo> sp1(new Foo);

            shared_ptr<Bar> sp2 = sp1; // 這時(shí) sp1.ptr 和 sp2.ptr 可能指向不同的地址,因?yàn)?Bar subobject 在 Foo object 中的 offset 可能不為0。

            sp1.reset(); // 此時(shí) Foo 對(duì)象的引用計(jì)數(shù)降為 1

            但是 sp2 仍然能安全地管理 Foo 對(duì)象的生命期,并安全完整地釋放 Foo,因?yàn)?delete 的不是 Bar*,而是原來(lái)的 Foo*。換句話說(shuō),sp2.ptr 和 ref_count.ptr 可能具有不同的值(當(dāng)然它們的類型也不同)。

            為什么要盡量使用 make_shared()?

            為了節(jié)省一次內(nèi)存分配,原來(lái) shared_ptr<Foo> x(new Foo); 需要為 Foo 和 ref_count 各分配一次內(nèi)存,現(xiàn)在用 make_shared() 的話,可以一次分配一塊足夠大的內(nèi)存,供 Foo 和 ref_count 對(duì)象容身。數(shù)據(jù)結(jié)構(gòu)是:

            sp10

            不過(guò) Foo 的構(gòu)造函數(shù)參數(shù)要傳給 make_shared(),后者再傳給 Foo::Foo(),這只有在 C++11 里通過(guò) perfect forwarding 才能完美解決。

            (.完.)

            posted @ 2013-01-28 05:15 陳碩 閱讀(16616) | 評(píng)論 (7)編輯 收藏

            僅列出標(biāo)題  下一頁(yè)
            <2025年5月>
            27282930123
            45678910
            11121314151617
            18192021222324
            25262728293031
            1234567

            導(dǎo)航

            統(tǒng)計(jì)

            常用鏈接

            隨筆分類

            隨筆檔案

            相冊(cè)

            搜索

            最新評(píng)論

            閱讀排行榜

            評(píng)論排行榜

            www.久久热.com| …久久精品99久久香蕉国产 | 国产成人久久777777| 青青草国产精品久久久久| 久久久网中文字幕| 亚洲国产精品无码久久一区二区| 99久久精品午夜一区二区| 久久久久久国产a免费观看不卡| 久久综合久久综合亚洲| 青青热久久综合网伊人| 亚洲国产美女精品久久久久∴| 亚洲一本综合久久| 久久久久久久人妻无码中文字幕爆| 国产99久久九九精品无码| 久久精品国产亚洲AV麻豆网站| 理论片午午伦夜理片久久| 麻豆精品久久久一区二区| 一本色道久久HEZYO无码| 亚洲色欲久久久久综合网| 亚洲一区二区三区日本久久九| 人妻无码久久一区二区三区免费 | 精品久久一区二区| 久久精品国产免费观看三人同眠| 国产日韩久久免费影院| 久久96国产精品久久久| 久久精品国产亚洲77777| 伊人久久精品无码二区麻豆| 亚洲日韩欧美一区久久久久我 | 久久久久无码中| 99久久精品免费看国产| 一本一道久久精品综合| 国产一区二区三区久久精品| 69SEX久久精品国产麻豆| 久久久婷婷五月亚洲97号色| 久久99精品久久久大学生| 一本色道久久99一综合| 亚洲日本va中文字幕久久| 无码国产69精品久久久久网站| 色偷偷88888欧美精品久久久| 久久久久亚洲AV无码麻豆| 2021久久精品国产99国产精品|