天天看點

muduo網絡庫——C++多線程系統程式設計精要

4.1  基本線程原語的選用

11個最基本的Pthreads函數是:

2個:線程的建立和等待結束(join)。封裝為muduo::Thread。

4個:mutex的建立、銷毀、加鎖、解鎖。封裝為muduo::MutexLock。

5個:條件變量的建立、銷毀、等待、通知、廣播。封裝為muduo::Condition。

這三樣東西(thread、mutex、condition)可以完成任何多線程程式設計任務。當然一般不會直接使用它們(mutex除外),而是使用更高層的封裝,例如 mutex::ThreadPool 和 mutex::CountDownLatch 等。

4.2  C/C++ 系統庫的線程安全性

基本原則:凡是非共享的對象都是彼此獨立的,如果一個對象從始至終隻被一個線程用到,那麼它就是安全的。共享的對象的 read-only 操作是安全的, 前提是不能有并發的寫操作。

4.3  Linux上的線程辨別

POSIX threads 庫提供了pthread_self 函數用于傳回目前程序的辨別符,其類型為pthread_t。但是有一系列問題:

muduo網絡庫——C++多線程系統程式設計精要

并且glibc的Pthreads實作實際上把pthread_t用作一個結構體指針,指向一塊動态配置設定的記憶體,這塊記憶體反複使用。這就造成pthread_t的值很容易重複。Pthreads隻保證同一程序内各個線程的id不同;不能保證同一程序先後多個程序具有不同的id。

是以,pthread_t并不适合用于程式中對線程的辨別符。

在Linux上,使用gettid系統調用的傳回值作為線程id。 好處有:

muduo網絡庫——C++多線程系統程式設計精要

muduo::CurrentThread::tid() 采取的辦法是用 __thread 變量來緩存 gettid()的傳回值。這樣隻有在本線程第一次調用的時候才進行系統調用,以後直接從 thread local 緩存的線程id 拿到結果。

4.4  線程的建立與銷毀的守則

線程建立的幾條簡單的原則:

1.程式庫不應該在未提前告知的情況下建立自己的“背景線程”。

2.盡量用相同的方式建立線程,例如 muduo::Thread。

3.在進入main()函數之前不應該啟動線程。

4.程式中線程的建立最好能在初始化階段全部完成。

一台機器可以同時并行運作的線程數目受限于CPU的數目,是以根據CPU的數目來設定工作線程的數目。

線程銷毀的幾種方式:

1.自然死亡。 從線程主函數傳回,線程正常退出。

2.非正常死亡。 從線程主函數抛出異常或線程出發segfault信号等非法操作。

3.自殺。 線上程中調用pthread_exit() 來立刻退出線程。

4.他殺。 其他線程調用pthread_cancel() 來強制終止某個線程。

如果能做到前面提到的 “程式中線程的建立最好能在初始化階段全部完成“,則線程是不必銷毀的,伴随程序一直運作,徹底避開了線程安全退出可能面臨的各種困難,包括Thread對象生命期管理,資源釋放等等。

4.5  善用 __thread 關鍵字

__thread變量使每個線程有一份獨立實體,各個線程的變量值互不幹擾。除了這個主要用途,還可以修飾那些 “值可能會變,帶有全局性,但是不值得用全局鎖保護” 的變量。

 __thread使用規則:隻能修飾POD類型(類似整型指針的标量,不帶自定義的構造、拷貝、指派、析構的類型,二進制内容可以任意複制memset,memcpy,且内容可以複原),不能修飾class類型,因為無法自動調用構造函數和析構函數,可以用于修飾全局變量,函數内的靜态變量,不能修飾函數的局部變量或者class的普通成員變量,且__thread變量值隻能初始化為編譯器常量。

4.6多線程與IO

多線程應該遵循的原則是:

每個檔案描述符隻由一個線程操作,進而輕松解決消息收發的順序性問題,也避免了關閉檔案描述符的各種 race condition。 

一個線程可以操作多個檔案描述符,但一個線程不能操作别的線程擁有的檔案描述符。

這條規則有兩個例外:

對于磁盤檔案,在必要的時候多個線程可以同時調用pread()/pwrite()來讀寫同一個檔案;

對于UDP,由于協定本身保護消息的原子性,在适當的條件下可以多個線程同時讀寫同一個UDP檔案描述符。

4.7 用RAII包裝檔案描述符

用Socket對象包裝檔案描述符,所有對此檔案描述符的讀寫操作都通過此對象進行,在對象的析構函數裡關閉檔案描述符。隻要Socket對象還活着,就不會有其他Socket對象跟它有一樣的檔案描述符,也就不可能串話。

4.9  多線程與fork()

多線程與fork()的協作性很差。fork一般不能在多線程程式中調用,因為Linux的fork()隻克隆目前線程的 thread of control,不克隆其他線程。fork之後,除了目前線程之外,其他線程都消失了。也就是說不能一下子fork()出一個和父程序一樣的多線程子程序。

fork()之後子程序中隻有一個線程,其他線程都消失了,這就造成一個危險的局面。其他線程可能正好處于臨界區之内,持有某個鎖,而它突然死亡,再也沒有機會去解鎖了。如果子程序試圖再對同一個mutex加鎖,就會立刻死鎖。在fork()之後,子程序就相當于處于signal handler之中。

4.10  多線程與 signal

Linux/Unix的信号與多線程水火不容!在多線程程式中,使用signal的第一原則是 不要使用signal

小結:

編寫多線程C++程式的原則如下:

1.線程是寶貴的,一個程式可以使用幾個或十幾個線程。

2.線程的建立和銷毀是有代價的,一個程式最好在一開始建立所需的線程,并一直反複使用。不要在運作期間反複建立、銷毀線程。

3.每個線程應該有明确的職責,例如IO線程( 運作EventLoop::loop(),處理IO事件 ),計算線程(位于ThreadPool中,負責計算)等等。

4.線程之間的互動應該盡量簡單,理想情況下,線程之間隻用消息傳遞方式互動。如果必須用鎖,那麼最好避免一個線程同時持有兩把或更多的鎖,這樣可徹底防止死鎖。

5.要預先考慮清楚一個 mutable shared 對象将會暴露給哪些線程,每個線程是讀還是寫, 讀寫有無可能并發進行。

繼續閱讀