《Linux多線程服務(wù)端編程:使用muduo C++網(wǎng)絡(luò)庫》這本書自今年一月上市以來,半年之內(nèi)已經(jīng)重印兩次(加上首印,一共是三次印刷),總印數(shù)達到了9000冊,這在技術(shù)書里已經(jīng)算是相當(dāng)不錯的成績。本書購買方式見配套網(wǎng)站 http://chenshuo.com/book 。
以下談一談這本書的寫作背景與內(nèi)容取舍的原因。
參加工作以來,我編寫并維護了若干C++/Java多線程網(wǎng)絡(luò)服務(wù)程序,這本書總結(jié)了我在開發(fā)維護這類服務(wù)程序方面的經(jīng)驗。工作中,我沒有寫過單線程的網(wǎng)絡(luò)服務(wù)程序,沒有寫過C語言的網(wǎng)絡(luò)服務(wù)程序,也沒有寫過運行在Windows下的網(wǎng)絡(luò)服務(wù)程序,因此本書不涉及這些內(nèi)容。
在“Linux服務(wù)端開發(fā)”這一背景下,這本書主要講三個方面的內(nèi)容[1]:現(xiàn)代C++、多線程、網(wǎng)絡(luò)編程,分別對應(yīng)書的第3、1、2部分。這不是一本入門書,本書的讀者應(yīng)該在以上三方面已經(jīng)具備相當(dāng)?shù)幕A(chǔ)[2]:網(wǎng)絡(luò)編程方面,能輕松讀懂6.1節(jié)的兩個Python程序;C++方面,對12.8節(jié)的代碼不感到陌生;多線程方面,能明白第1章要解決什么問題。
第9章“分布式系統(tǒng)工程實踐”詳細(xì)介紹了這本書的應(yīng)用背景,即開發(fā)公司內(nèi)部的分布式服務(wù)系統(tǒng),書中的很多決策(design decision)和技術(shù)取舍(trade-off)是在這一應(yīng)用場景下做出的。以下是各章直接的交叉引用關(guān)系圖(沒有計算引用次數(shù)),其中第0章是前言,字母章節(jié)是附錄。可見第9章是被引用最多的一章,某種意義上可以說第9章既是本書的先決條件,又是本書的終極目標(biāo)。由于章節(jié)之間存在眾多的交叉引用,去掉任何一章都會破壞內(nèi)容的完整性。
這本書的書名原本打算叫“Linux C++ 多線程系統(tǒng)編程”。寫完之后發(fā)現(xiàn),與其他Unix/Linux系統(tǒng)編程方面的書不同,這本書有明確的應(yīng)用場景,因此可以砍掉很多內(nèi)容,突出重點。甚至可以說我主要講別的書沒有講到的內(nèi)容。這不是一本面面俱到的書,因此最終的書名也就不叫“系統(tǒng)編程”了。
同時,我認(rèn)為很多教科書上介紹的一些做法是過時的(signal),一些是不推薦使用的(從外部終止線程、TCP OOB數(shù)據(jù)),一些是大多數(shù)情況下沒必要使用的(內(nèi)存池、lock-free 編程)。作為全面的教材和手冊,把這些內(nèi)容放進去可以理解。但是這本書定位是經(jīng)驗總結(jié),我略去了教科書上那些基本用不到的知識點,以免喧賓奪主,也建議讀者不要把精力花在那些次要問題上。
- 這本書沒有花很大的篇幅去講signal,而是在第4.10節(jié)說明多線程程序不要使用signal作為IPC。并且,在muduo-protorpc的示例中給出了Linux專有的signalfd(2)的用法,可以避免傳統(tǒng)signal handler的常見陷阱,也更符合UNIX的“everything is a file”哲學(xué)。第4.4節(jié)說明不要從外部終止線程,因此也就不必去細(xì)究Pthreads cancellation point了。多線程程序最好不要fork()(第4.9節(jié))。
- 這本書沒介紹daemon進程,我認(rèn)為daemon是過時的做法。因為daemon進程的父進程是init(1),配置文件在本機,不便于多機統(tǒng)一監(jiān)控與管理(第9.8節(jié))。(注:如果是第三方標(biāo)準(zhǔn)的服務(wù)程序,又不需要經(jīng)常升級或改配置重啟,并且一旦崩潰,重啟就能繼續(xù)服務(wù),那么做成 daemon 讓init(1)接管是可以的,比如ntpd、sshd等。這里談的是自己開發(fā)維護的服務(wù)程序。)另外,Java/Python/Go寫的服務(wù)程序似乎也沒有做成daemon的習(xí)慣,C++程序沒有理由要特殊對待。補充一點,Linux的進程管理機制很落后(從UNIX繼承而來),子進程退出的事件只能被父進程以SIGCHLD信號的方式收到(而且這個signal可能丟失),kill(pid) 也存在很多race condition(你怎么保證pid在kill之前的一瞬間還代表你想kill的那個進程,而不是一個新啟動的進程?close(fd)就不會有這種 race condition。)。這些困難在用戶態(tài)無法克服,只能修改內(nèi)核,引入新的系統(tǒng)調(diào)用才能治本。例如 FreeBSD 9.0 引入了 pdfork()/pdkill() 等,將子進程變成文件描述符,這樣就能用IO事件框架統(tǒng)一處理了,也符合UNIX的“everything is a file”哲學(xué)。但愿Linux內(nèi)核也能盡快引入類似的系統(tǒng)調(diào)用,減輕程序員的負(fù)擔(dān)。
- 這本書沒有講內(nèi)存池,而是說明不是每個程序都要自己寫內(nèi)存池(§12.2.8)。這本書也沒有把“避免內(nèi)存碎片”掛在嘴邊,而是論證為什么一般的程序不必在意它(§A.1.8);
- 這本書只關(guān)注Linux,不考慮移植性。它推薦使用Linux專有的gettid()系統(tǒng)調(diào)用作為線程標(biāo)識(第4.3節(jié)),而不是用pthread_self()。
- 這本書不講POSIX中五花八門的定時函數(shù),而專講用Linux特有的timerfd來實現(xiàn)高精度定時(§7.8.2),因為它能方便地融入IO事件處理框架。muduo直接使用C++標(biāo)準(zhǔn)庫來管理定時器,而不是自己實現(xiàn)小頂堆(heap),這樣可以簡化實現(xiàn)(§8.2.1)。
- 這本書只講mutex和condition variable作為最基礎(chǔ)的線程同步手段(第2章),并且我認(rèn)為一個C++多線程程序代碼里不應(yīng)該直接出現(xiàn)pthread_mutex_lock之類的基本Pthreads調(diào)用。本書進一步建議只使用非遞歸的mutex(§2.1.1),這與某些網(wǎng)上文章的推薦正好相反。這本書第2.3節(jié)甚至建議不要使用讀寫鎖和信號量(semaphore),因為一是容易用錯,二是不見得能提高性能。mutex和condition variable是完備的,能實現(xiàn)多種更易用的同步設(shè)施,例如CountDownLatch和BlockingQueue。§12.8.3的代碼展示了用BlockingQueue和ThreadPool控制并發(fā)度的手法,做到了“No locks. No condition variables. No callbacks.”
- 這本書不講lock-free編程,因為編寫可靠的lock-free代碼并分析驗證其正確性的難度遠大于編寫普通的使用mutex和condition variable的多線程代碼,后者已經(jīng)有了相當(dāng)成熟的理論和工具。我認(rèn)為lock-free不是每個多線程程序員應(yīng)該掌握的技術(shù),它投入高而用處少,可以適當(dāng)了解,但不值得每個人都去深究。只需要少數(shù)人用它實現(xiàn)封裝好的數(shù)據(jù)結(jié)構(gòu),像我這樣的普通人就可以受益。
- 這本書只講BSD Sockets作為進程間通信的手段,并且只用TCP長連接(§3.4)。這樣就砍掉了pipe、FIFO、POSIX message queue、shared memory、STREAMS、UNIX domain socket等等內(nèi)容,因為它們都只限本機進程間通信,無法擴展到多機。
- 網(wǎng)絡(luò)編程方面(第6、7章),這本書不講Sockets API的基本用法,而且代碼中也不會直接使用它們。我認(rèn)為在程序中直接使用 Sockets API是初學(xué)者的做法,當(dāng)寫一個新網(wǎng)絡(luò)服務(wù)程序,如果一開始考慮的是怎么組織accept、read、epoll_wait等調(diào)用,這種做法無異于用鉛筆刀鋸大樹,事倍功半,也不利于將來的功能擴展和維護。稍微像樣點的公司都會用成熟的網(wǎng)絡(luò)庫(不一定開源),把網(wǎng)絡(luò)編程的復(fù)雜性封裝進去,暴露出良好易用的接口,讓開發(fā)人員使用更高層的building blocks(消息傳遞或RPC)從功能的角度去設(shè)計程序,避免一次次反復(fù)掉到TCP網(wǎng)絡(luò)編程的坑里。多個服務(wù)程序共享相同的基礎(chǔ)庫和事件處理框架的益處是顯而易見的,一方面把網(wǎng)絡(luò)編程的復(fù)雜性集中到一起,避免每個團隊都去踏一遍坑;另一方面,基礎(chǔ)庫的bug修復(fù)與性能優(yōu)化能惠及用到它的全部服務(wù)程序;最后,程序結(jié)構(gòu)上的相似性讓編程經(jīng)驗更加通用,多個服務(wù)程序在功能、性能、正確性等方面具有共性,能舉一反三觸類旁通,降低將來開發(fā)維護的成本。應(yīng)該避免每個程序都另起爐灶,單獨設(shè)計其IO事件處理結(jié)構(gòu)。
- 這本書只講非阻塞IO結(jié)合IO復(fù)用(IO-Multiplexing)這一種并發(fā)風(fēng)格(歸納為三個半事件),并介紹在多線程下的擴展(one loop per thread)。IO復(fù)用方面,本書只講level-trigger,不講edge-trigger。一方面目前沒有up to date的測試表明ET更快,相反,我認(rèn)為LT在讀取數(shù)據(jù)時可以節(jié)約一次read()調(diào)用(§8.7.2);另一方面,LT模式更容易與其他第三方庫結(jié)合(§7.15)。多線程程序管理并發(fā)socket fd有很多風(fēng)格可供選擇,例如epoll fd是多個線程共享一個(多對一)還是每個線程有自己的epoll fd(一對一),每個socket fd是只屬于一個epoll fd(多對一)還是可以同時屬于多個 epoll fd(多對多),每個socket fd是只能被固定的一個線程讀寫還是可以被多個線程讀寫(如果是后者,那么讀寫的時候是加鎖還是使用ONESHOT)。以上不是每種都可行,本書也沒有一一加以分析,而是建議使用one loop per thread這種適用性較強的風(fēng)格,首先是正確性容易驗證,其次是性能也能滿足要求。
- 本書不講IPv6,因為目前世界上最大的公司的服務(wù)機群也用不完一個私有A類地址(10.0.0.0/8)。本書不講UDP,因為《Unix網(wǎng)絡(luò)編程》已經(jīng)講得很好了。
- 這本書舉的網(wǎng)絡(luò)編程的例子不再是簡單的echo服務(wù),而是有格式(因此引入codec)、多連接之間會交換數(shù)據(jù)的網(wǎng)絡(luò)程序,更接近業(yè)務(wù)場景,也借機講解如何避免TCP網(wǎng)絡(luò)編程的常見陷阱。并且在示例代碼中給出了分布式單詞計數(shù)、多機求中位數(shù)等稍微復(fù)雜一點的程序。
- 在C++方面,這本書沒有介紹動態(tài)鏈接庫熱更新這種“高級”技術(shù),而是說明,在分布式系統(tǒng)中,為了部署方便,應(yīng)該從源碼編譯全部的庫,與主程序鏈接為一個standalone的可執(zhí)行文件,以減小對運行環(huán)境的依賴(第10章)。第11章還討論了程序庫與應(yīng)用程序之間的接口設(shè)計。
“信息”按照香農(nóng)的定義,是“減少不確定性”,這本書包含的信息正是減少選用編程設(shè)施(facilities)方面的不確定性,讓讀者集中精力攻克本質(zhì)問題。這本書介紹的方法不一定對于每個應(yīng)用場景都是最好的,但肯定是簡便易行的,是時間成本、功能、性能的一種合理折中。
[1] 這本書前言的第一句話“本書主要講述采用現(xiàn)代 C++ 在 x86-64 Linux 上編寫多線程 TCP 網(wǎng)絡(luò)服務(wù)程序的主流常規(guī)技術(shù)”,封面印著“示范在多核時代采用現(xiàn)代 C++ 編寫多線程 TCP 網(wǎng)絡(luò)服務(wù)器的正規(guī)做法”。
[2] 前言寫到:讀者應(yīng)該已經(jīng)大致讀過《現(xiàn)代操作系統(tǒng)》、《UNIX 環(huán)境高級編程》、《UNIX 網(wǎng)絡(luò)編程》、《C++ Primer》或與之內(nèi)容相近的書籍,熟悉基本概念,并掌握 Pthreads 和 Sockets API 的常規(guī)用法。