C++多線程以及線程池

1 線程

1.1 簡介

  線程(英語:thread)是操作系統能夠進行運算調度的最小單位。大部分情況下,它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。在Unix System V及SunOS中也被稱爲輕量進程(lightweight processes),但輕量進程更多指內核線程(kernel thread),而把用戶線程(user thread)稱爲線程。
  同一進程中的多條線程將共享該進程中的全部系統資源,如虛擬地址空間,文件描述符和信號處理等等。但同一進程中的多個線程有各自的調用棧(call stack),自己的寄存器環境(register context),自己的線程本地存儲(thread-local storage)。一個進程可以有很多線程,每條線程並行執行不同的任務。在多核或多CPU,或支持Hyper-threading的CPU上使用多線程程序設計的好處是顯而易見的,即提高了程序的執行吞吐率。在單CPU單核的計算機上,使用多線程技術,也可以把進程中負責I/O處理、人機交互而常被阻塞的部分與密集計算的部分分開來執行,編寫專門的workhorse線程執行密集計算,從而提高了程序的執行效率。

1.2 線程的調度

  線程作爲CPU調度最基本的單位其狀態和進程基本相同:
在這裏插入圖片描述

  • 新建狀態:當線程被創建到開始執行任務之間的狀態,一般對應的是線程對象被創建到執行start結構之間的過程;
  • 就緒狀態:線程開始執行之後,只是表明線程準備好執行,需要CPU調度進行執行;
  • 運行狀態:線程被CPU翻牌子,開始執行任務;
  • 阻塞狀態:線程因爲等待其他事件觸發或者等待資源而發生阻塞,一般分爲:
    • 等待阻塞:線程執行了wait
    • 同步阻塞:因爲同步鎖獲取失敗,而進入阻塞;
    • 其他阻塞:因等待獲取IO資源而阻塞或者線程調用了sleep
  • 死亡狀態:線程任務執行完成,可以被銷燬。

  線程調度算法其實是CPU調度算法,一般有:

  1. 先來先服務(FCFS)算法;
  2. 時間片輪轉調度算法;
  3. 短作業優先算法;
  4. 最短剩餘時間優先算法;
  5. 高響應比優先算法;
  6. 優先級調度算法;
  7. 多級反饋隊列調度算法;

1.3 線程和進程的區別

  1. 線程是任務調度和執行的基本單位;進程是資源分配的基本單位;
  2. 單個線程可以包含多個線程,單個線程至少包含一個線程,不存在不在進程內的線程,也沒有不存在線程的進程;
  3. 因爲虛擬內存的關係不同的進程互相不知道對方的存在,相對獨立,互相有獨立的內存空間,地址空間;相同進程內的進程之間之間共享進程的內存空間,文件描述符表,部分寄存器等資源,但是每個線程都有自己獨立的線程棧和部分寄存器,線程間訪問資源需要考慮互斥問題;不同進程內的線程關係和不同進程關係相似,相對獨立;
  4. 進程創建,銷燬,切換代價大;線程創建,銷燬,切換小。

  進程和線程的優缺點:

  • 進程有利於資源管理和保護,開銷大;進程不利於資源管理和保護,需要使用鎖保證線程的安全運行,開銷小。

1.4 線程的通信方式

  不同進程間的線程通信等同於進程通信,一般是五種通信方式:

  1. 消息隊列;
  2. 共享內存;
  3. 有名管道/無名管道;
  4. 信號;
  5. scoket。

  相同進程中的不同線程因爲資源共享不存在通信問題,主要是資源的互斥訪問,一般需要對資源進行加鎖以防死鎖。

1.5 內核線程和用戶線程

  線程的實現方式分爲內核線程和用戶線程。
  內核級線程:由系統內核創建,撤銷,調度的線程。
  用戶級線程:由用戶進程自行調度的線程。
  混合級線程:同時支持用戶級線程和內核級線程。
  內核級線程和用戶級線程的區別

  1. 內核線程需要操作系統支持,系統可知;用戶級線程,系統調度資源分配是面向進程的,並不可知;
  2. 內核級線程創建、調度和撤銷類似於進程;用戶級線程創建和調度由用戶進程控制,需要用戶程序控制線程的調度工作;
  3. 內核級線程不同線程之間相對獨立;用戶線程中如果一個線程因其他原因而阻塞則導致整個進程阻塞,相同進程中的其他線程也被阻塞;
  4. 內核級線程資源競爭空間爲全局;用戶級線程資源競爭空間爲當前進程。

  內核級線程優缺點

  • 優點:
    1. 多核CPU友好,能夠實現整整意義上的多線程;
    2. 如果單個進程中的一個線程被阻塞,進程中的其他線程仍然能夠進程切換運行;
    3. 所有阻塞線程的調用都以系統調用實現,代價較小;
  • 缺點:
    1. 由內核進行調度,用戶的可定製性差;
    2. 線程在用戶態運行,而線程的切換和調度相關操作在內核實現,進行線程切換時代價相對較高。

  用戶級線程的優缺點

  • 優點:
    1. 線程調度由用戶控制,不需要內核參與,控制靈活,相對可控;
    2. 不需要內核支持,可以在不支持多線程的系統中實現;
    3. 線程創建、銷燬、調度和切換等管理操作的代價比內核級線程小;
    4. 線程能夠利用的表空間和堆棧空間比內核線程多;
  • 缺點:
    1. 資源分配和調度按照進程進行,單個進程中同一時間只有一個線程運行,多處理機不友好;
    2. 當進程中的一個線程因缺頁中斷等原因阻塞時整個進程都會阻塞。

2 多線程

  多線程(英語:multithreading),是指從軟件或者硬件上實現多個線程併發執行的技術。具有多線程能力的計算機因有硬件支持而能夠在同一時間執行多於一個線程,進而提升整體處理性能。具有這種能力的系統包括對稱多處理機、多核心處理器以及芯片級多處理(Chip-level multithreading)或同時多線程(Simultaneous multithreading)處理器。
  多線程一般分爲軟件多線程和硬件多線程,分別通過軟件實現和硬件支持。軟件支持的多線程無法實現整整意義上的併發,硬件支持的多線程能夠實現整整意義上的併發。

2.1 多線程模型

  多線程模型分爲:多對一,一對一,多對多。
在這裏插入圖片描述

  多對一模型:
  多個用戶級線程映射到一個內核級線程。用戶級線程對系統不可見,創建,調度,撤銷的成本低效率高;當單個進程中的一個用戶線程發生阻塞會導致進程阻塞。
  一對一模型:
  每一個用戶進程映射到一個內核線程。併發能力強,不同線程之間相對獨立;創建,調度,撤銷需要調用系統調用,相對開銷比較大。
  多對多模型:
  多個用戶級線程映射到少許(<<用戶線程數)內核線程。是對多對一和一對一模型的折中。

2.2 硬件層面的多線程支持

  多線程的支持分爲用戶層,系統層(內核層),硬件層。其中只有硬件層的支持才能完全實現整整意義上的併發(同一時間多條指令運行),其他都只是感官上的多線程,實際上每個時刻只有一個線程在運行。
  硬件層面的多線程實現分爲:粗粒度交替多線程,細粒度交替多線程和同步多線程。
  下圖只是爲了讓讀者更好的理解不同方式的區別,其中綠色和紅色的框是隨機畫的並不完全準確。
在這裏插入圖片描述

2.2.1 粗粒度交替多線程

  粗粒度交替多線程中一個線程會一直運行到被一個外部事件導致無法繼續運行爲止才進行線程切換。該外部事件可以是:IO阻塞,程序分支,時間片結束等等高時延事件。
  硬件成本:爲了高效的線程切換,寄存器需要有兩個實例。
  許多微控制器與嵌入式處理器有多重的寄存器列,就能夠在中斷時快速環境切換。這樣架構可以視爲程序的線程與中斷線程之間的塊狀多線程處理。

2.2.2 細粒度交替多線程

  將CPU的週期輪轉切換至不同的線程。更像是在時間片輪轉的基礎上再進行一次分割調度處理。
  硬件成本:除了討論塊狀多線程的硬件成本,交錯式多線程也因每層管線需要追蹤運行中指令的線程代碼而增加硬件成本。而且,當越來越多的線程同時在管線中運行,像是緩存與 TLB 等共享資源也要加大來避免不同線程之間的衝突。
  Intel Super-threading,Raza Microelectronics Inc XLR等處理器採用此方式。

2.2.3 同步多線程

  同時多線程(Simultaneous multithreading,縮寫SMT)也稱同步多線程,是一種提高具有硬件多線程的超標量CPU整體效率的技術。同時多線程允許多個獨立的執行線程更好地利用現代處理器架構提供的資源。

超標量(superscalar)CPU架構是指在一顆處理器內核中實行了指令級併發的一類併發運算。這種技術能夠在相同的CPU主頻下實現更高的CPU流量(throughput)。處理器的內核中一般有多個執行單元(或稱功能單元),如算術邏輯單元、位移單元、乘法器等等。未實現超標量體系結構時,CPU在每個時鐘週期僅執行單條指令,因此僅有一個執行單元在工作,其它執行單元空閒。超標量體系結構的CPU在一個時鐘週期可以同時分派(dispatching)多條指令在不同的執行單元中被執行,這就實現了指令級的並行。超標量體系結構可以視作多指令流多數據流。

  每個CPU週期,單個線程都會發布多個指令。
  硬件成本:交錯式多線程如果不計硬件成本,SMT在每個管線層次結構的追蹤線程指令會有多餘的花費。而且,像是緩存與TLB這類共享的資源可能會因爲多出來的線程而變得更大。
  Inter超線程,IBM Power5,AMD Bulldozer等都採用這種方式。

2.3 Thread API

2.3.1 C++11 API

  2011年8月12日,國際標準化組織(ISO)發佈了第三個C++標準,即ISO/IEC 14882:2011,簡稱ISO C++ 11標準。該標準第一次把線程的概念引入C++標準庫。Windows平臺運行的VS2012和Linux平臺運行的g++4.7,都完美支持C++11線程。

  標準庫文檔見:std::thread

//構造函數
thread() noexcept;                                  //構造不表示線程的thread對象
thread( thread&& other ) noexcept;                  //移動拷貝構造函數,調用之後other不再是可執行線程
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );    //構造可執行線程,f爲需要線程執行的操作,args爲可變參數,爲f的參數
thread(const thread&) = delete;                     //禁用thread的拷貝構造
//析構函數
~thread()                                           //析構前需要調用join()和detach()

//觀察器
bool joinable() const noexcept;                     //檢查 std::thread 對象是否標識活躍的執行線程,若 thread 對象標識活躍的執行線程則爲 true ,否則爲 false
std::thread::id get_id() const noexcept;            //返回線程的 id
native_handle_type native_handle();                 //返回底層實現定義的線程句柄

static unsigned int hardware_concurrency() noexcept;//返回支持的併發線程數。若該值非良定義或不可計算,則返回0,應該只把該值當做提示。


//操作
void join();                                        //阻塞當前線程直至 *this 所標識的線程結束其執行
void detach();                                      //從 thread 對象分離執行線程,允許執行獨立地持續。一旦該線程退出,則釋放任何分配的資源。調用 detach 後 *this 不再佔有任何線程
void swap( std::thread& other ) noexcept;           //交換二個 thread 對象的底層柄

  示例:

#include <thread>
#include <iostream>
#include <atomic>

void func1(int n)
{
    for (int i = 0; i < 5; i++)
    {
        std::cout << "$thread " << std::this_thread::get_id() << " " << n << std::endl;
        n++;
        std::this_thread::sleep_for(std::chrono::microseconds(1000));
    }
}

void func2(int &n)
{
    for (int i = 0; i < 5; i++)
    {
        std::cout << "#thread " << std::this_thread::get_id() << " " << n << std::endl;
        n++;
        std::this_thread::sleep_for(std::chrono::microseconds(100));
    }
}

int thread_test()
{
    int n = 0;
    n = 10;
    std::thread thread_rst; //僅僅是一個對象,不是一個線程
    std::thread thread_snd(func1, n);
    std::thread thread_thd(func2, std::ref(n));

    std::cout << thread_snd.get_id() << " joinable:" << thread_snd.joinable() << std::endl;
    std::cout << thread_thd.get_id() << " joinable:" << thread_thd.joinable() << std::endl;

    std::cout << thread_snd.get_id() << " native handle :" << thread_snd.native_handle() << std::endl;
    std::cout << thread_thd.get_id() << " native handle :" << thread_thd.native_handle() << std::endl;
    if (thread_snd.joinable())
        thread_snd.join();

    if (thread_thd.joinable())
        thread_thd.join();

    std::cout << thread_snd.get_id() << " joinable:" << thread_snd.joinable() << std::endl;
    std::cout << thread_thd.get_id() << " joinable:" << thread_thd.joinable() << std::endl;

    std::cout << "final value is " << n << std::endl;
    return 0;
}

  輸出:

$thread 140737336633088 10
#thread 140737328240384 10
140737336633088 joinable:1
140737328240384 joinable:1
140737336633088 native handle :140737336633088
140737328240384 native handle :140737328240384
#thread 140737328240384 11
#thread 140737328240384 12
#thread 140737328240384 13
#thread 140737328240384 14
$thread 140737336633088 11
$thread 140737336633088 12
$thread 140737336633088 13
$thread 140737336633088 14
thread::id of a non-executing thread joinable:0
thread::id of a non-executing thread joinable:0
final value is 15

2.3.2 C11 API

//#include <thread.h>
int thrd_create(thrd_t *thr, thrd_start_t func, void *arg);     //創建線程
_Noreturn void thrd_exit( int res );                            //結束線程
int thrd_join(thrd_t thr, int *res);                            //等待線程運行完畢
thrd_t thrd_current();                                          //返回當前線程的線程標識符

2.3.3 Win API

//#include <windows.h>
//創建用戶級線程
HANDLE WINAPI CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
//結束本線程
VOID WINAPI ExitThread(DWORD dwExitCode);
//掛起指定的線程
DWORD WINAPI SuspendThread( HANDLE hThread );
//恢復指定線程運行
DWORD WINAPI ResumeThread(HANDLE hThread);
//等待線程運行完畢
DWORD WINAPI WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
//返回當前線程的線程標識符
DWORD WINAPI GetCurrentThreadId(void);
//返回當前線程的線程句柄
HANDLE WINAPI GetCurrentThread(void);

2.3.4 Posix API

  詳細內容見POSIX線程

//#include <pthread.h>
//創建用戶級線程
int pthread_create(pthread_t * thread, const pthread_attr_t * attr, void *(*start_routine)(void *), void *arg);
//等待用戶級線程
int pthread_join(pthread_t thread, void ** retval);
//退出用戶級線程
void pthread_exit(void *retval);
//返回當前用戶級線程的線程標識符
pthread_t pthread_self(void);
//用戶級線程的取消
int pthread_cancel(pthread_t thread);

3 鎖

鎖這一塊兒,稍微瞭解下,感覺還沒有完全喫透,還需要更多實踐纔行。

3.1 死鎖

  死鎖:當兩個以上的運算單元,雙方都在等待對方停止運行,以獲取系統資源,但是沒有一方提前退出時,就稱爲死鎖。在多任務操作系統中,操作系統爲了協調不同行程,能否獲取系統資源時,爲了讓系統運作,必須要解決這個問題。
  死鎖的基本條件:

  • 禁止搶佔(no preemption):系統資源不能被強制從一個進程中退出;
  • 持有和等待(hold and wait):一個進程可以在等待時持有系統資源;
  • 互斥(mutual exclusion):資源只能同時分配給一個進程或者線程,無法多個進程或者線程共享;
  • 循環等待(circular waiting):一系列進程互相持有其他進程所需要的資源。

  死鎖的四個條件缺一不可,一般如果需要解決死鎖,破壞其中一個即可。
  死鎖理論上可以通過銀行家算法進行預測,但是實際上系統資源量,需求量往往並不是能夠準確估計的,因此死鎖無法準確預測,只能嘗試避免。

3.2 Linux系統中的鎖

  鎖是爲了避免多個運行單位對臨界區的競爭訪問,資源依賴進行限制,防止出現不可預見的錯誤,死鎖等狀況。
  可重入鎖:當線程獲取某個鎖後,還可以繼續獲取它,可以遞歸調用,而不會發生死鎖。
  不可重入鎖:與可重入相反,獲取鎖後不能重複獲取,否則會死鎖(自己鎖自己)。

3.2.1 自旋鎖

  自旋鎖是用於多線程同步的一種鎖,線程反覆檢查鎖變量是否可用。由於線程在這一過程中保持執行,因此是一種忙等待。一旦獲取了自旋鎖,線程會一直保持該鎖,直至顯式釋放自旋鎖。自旋鎖最多隻能被一個可執行線程持有。如果一個執行線程試圖獲得一個被已經持有(爭用)的自旋鎖,那麼該線程就會一直進行忙循環-旋轉-等待鎖重新可用要是鎖未被爭用,請求鎖的執行線程就可以立即得到它,繼續執行。

//#include <spinlock.h>
spin_lock()             //獲取指定的自旋鎖
spin_lock_irq()         //禁止本地中斷並獲取指定的鎖
spin_lock_irqsave()     //保存本地中斷當前狀態,禁止本地中斷,並獲取指定的鎖
spin_unlock()           //釋放指定的鎖
spin_unlock_irq()       //釋放指定的鎖,並激活本地中斷
spin_unlock_irqrestore()//釋放指定的鎖,並讓本地中斷恢復到以前的狀態
spin_lock_init()        //動態初始化指定的spinlock_t
spin_trylock()          //試圖獲取指定的鎖,如果未獲取,則返回0
spin_is_locked()        //如果指定的鎖當前正在被獲取,則返回非0,否則返回0

3.2.2 互斥鎖

  互斥鎖(Mutual exclusion,縮寫 Mutex)是一種用於多線程編程中,防止兩條線程同時對同一公共資源(比如全局變量)進行讀寫的機制。該目的通過將代碼切片成一個一個的臨界區域(critical section)達成。臨界區域指的是一塊對公共資源進行訪問的代碼,並非一種機制或是算法。一個程序、進程、線程可以擁有多個臨界區域,但是並不一定會應用互斥鎖。

pthread_mutex_t lock; // 互斥鎖定義
pthread_mutex_init(&lock, NULL); // 動態初始化,	成功返回0,失敗返回非0
pthread_mutex_t thread_mutex = PTHREAD_MUTEX_INITIALIZER; // 靜態初始化
pthread_mutex_lock(&lock); // 阻塞的鎖定互斥鎖
pthread_mutex_trylock(&thread_mutex)// 非阻塞的鎖定互斥鎖,成功獲得互斥鎖返回0,如果未能獲得互斥鎖,立即返回一個錯誤碼
pthread_mutex_unlock(&lock)// 解鎖互斥鎖
pthread_mutex_destroy(&lock) // 銷燬互斥鎖

3.2.3 讀寫鎖

  讀寫鎖是計算機程序的併發控制的一種同步機制,也稱“共享-互斥鎖”、多讀者-單寫者鎖。多讀者鎖,push lock用於解決讀寫問題(readers–writers problem)。讀操作可併發重入,寫操作是互斥的。讀寫鎖通常用互斥鎖、條件變量、信號量實現。

//#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);          //初始化讀寫鎖
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);   //銷燬讀寫鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);    //讀加鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);    //寫加鎖
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);    //解鎖
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); //非阻塞獲得讀鎖
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); //非阻塞獲得寫鎖

3.2.4 RCU鎖

  RCU鎖是對讀寫鎖的一種改進,在性能上相比更加高效。
  RCU(Read-Copy Update),對於被RCU保護的共享數據結構,讀者不需要獲得任何鎖就可以訪問它,但寫者在訪問它時首先拷貝一個副本,然後對副本進行修改,最後使用一個回調(callback)機制在適當的時機把指向原來數據的指針重新指向新的被修改的數據。這個時機就是所有引用該數據的CPU都退出對共享數據的操作。RCU實際上是一種改進的rwlock,讀者幾乎沒有什麼同步開銷,它不需要鎖。(CPU發生了上下文切換稱爲經歷一個quiescent state)

rcu_read_lock()             //讀者在讀取由RCU保護的共享數據時使用該函數標記它進入讀端臨界區
rcu_read_unlock()           //該函數與rcu_read_lock配對使用,用以標記讀者退出讀端臨界區
synchronize_rcu()           //該函數由RCU寫端調用,它將阻塞寫者,直到經過grace period後,即所有的讀者已經完成讀端臨界區,寫者纔可以繼續下一步操作
synchronize_kernel()        //其他非RCU的內核代碼使用該函數來等待所有CPU處在可搶佔狀態,目前功能等同於synchronize_rcu,但現在已經不建議使用,而使用synchronize_sched
synchronize_sched()         //該函數用於等待所有CPU都處在可搶佔狀態,它能保證正在運行的中斷處理函數處理完畢,但不能保證正在運行的softirq處理完畢
void fastcall call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu))
                            //函數 call_rcu 也由 RCU 寫端調用,它不會使寫者阻塞,因而可以在中斷上下文或 softirq 使用,而 synchronize_rcu、synchronize_kernel 和synchronize_shced 只能在進程上下文使用。該函數將把函數 func 掛接到 RCU回調函數鏈上,然後立即返回。一旦所有的 CPU 都已經完成端臨界區操作,該函數將被調用來釋放刪除的將絕不在被應用的數據
void fastcall call_rcu_bh(struct rcu_head *head, void (*func)(struct rcu_head *rcu))
                            //函數call_ruc_bh功能幾乎與call_rcu完全相同,唯一差別就是它把softirq的完成也當作經歷一個quiescent state,因此如果寫端使用了該函數,在進程上下文的讀端必須使用rcu_read_lock_bh
#define rcu_dereference(p)     ({ \
                                typeof(p) _________p1 = p; \
                                smp_read_barrier_depends(); \
                                (_________p1); \
                                })
                            //該宏用於在RCU讀端臨界區獲得一個RCU保護的指針,該指針可以在以後安全地引用,內存柵只在alpha架構上才使用
static inline void list_add_rcu(struct list_head *new, struct list_head *head)
                            //該函數把鏈表項new插入到RCU保護的鏈表head的開頭
static inline void list_add_tail_rcu(struct list_head *new,struct list_head *head)
                            //該函數類似於list_add_rcu,它將把新的鏈表項new添加到被RCU保護的鏈表的末尾
static inline void list_del_rcu(struct list_head *entry)
                            //該函數從RCU保護的鏈表中移走指定的鏈表項entry,並且把entry的prev指針設置爲LIST_POISON2,但是並沒有把entry的next指針設置爲LIST_POISON1,因爲該指針可能仍然在被讀者用於便利該鏈表
static inline void list_replace_rcu(struct list_head *old, struct list_head *new)
                            //該函數是RCU新添加的函數,並不存在非RCU版本。它使用新的鏈表項new取代舊的鏈表項old,內存柵保證在引用新的鏈表項之前,它的鏈接指針的修正對所有讀者可見
list_for_each_rcu(pos, head)//該宏用於遍歷由RCU保護的鏈表head,只要在讀端臨界區使用該函數,它就可以安全地和其它_rcu鏈表操作函數
list_for_each_safe_rcu(pos, n, head)
                            //該宏類似於list_for_each_rcu,但不同之處在於它允許安全地刪除當前鏈表項pos
list_for_each_entry_rcu(pos, head, member)
                            //該宏類似於list_for_each_rcu,不同之處在於它用於遍歷指定類型的數據結構鏈表,當前鏈表項pos爲一包含struct list_head結構的特定的數據結構
list_for_each_continue_rcu(pos, head)
                            //該宏用於在退出點之後繼續遍歷由RCU保護的鏈表head
static inline void hlist_del_rcu(struct hlist_node *n)
                            //它從由RCU保護的哈希鏈表中移走鏈表項n,並設置n的ppre指針爲LIST_POISON2,但並沒有設置next爲LIST_POISON1,因爲該指針可能被讀者使用用於遍利鏈表
static inline void hlist_add_head_rcu(struct hlist_node *n,struct hlist_head *h)
                            //該函數用於把鏈表項n插入到被RCU保護的哈希鏈表的開頭,但同時允許讀者對該哈希鏈表的遍歷。內存柵確保在引用新鏈表項之前,它的指針修正對所有讀者可見
hlist_for_each_rcu(pos, head)//該宏用於遍歷由RCU保護的哈希鏈表head,只要在讀端臨界區使用該函數,它就可以安全地和其它_rcu哈希鏈表操作函數(如hlist_add_rcu)併發運行
hlist_for_each_entry_rcu(tpos, pos, head, member)
                            //類似於hlist_for_each_rcu,不同之處在於它用於遍歷指定類型的數據結構哈希鏈表,當前鏈表項pos爲一包含struct list_head結構的特定的數據結構

3.2.5 條件變量

  條件變量是用來等待線程而不是上鎖的,通常和互斥鎖一起使用。互斥鎖的一個明顯的特點就是某些業務場景中無法藉助系統來喚醒,仍然需要業務代碼使用while來判斷,這樣效率本質上比較低。而條件變量通過允許線程阻塞和等待另一個線程發送信號來彌補互斥鎖的不足,所以互斥鎖和條件變量通常一起使用,來讓條件變量異步喚醒阻塞的線程。

int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);  //解除所有線程的阻塞

4 線程池

4.1 簡介

  線程池(thread pool)是一種線程使用模式。線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池維護着多個線程,等待着監督管理者分配可併發執行的任務。這避免了在處理短時間任務時創建與銷燬線程的代價。線程池不僅能夠保證內核的充分利用,還能防止過分調度。可用線程數量應該取決於可用的併發處理器、處理器內核、內存、網絡sockets等的數量。 例如,線程數一般取cpu數量+2比較合適,線程數過多會導致額外的線程切換開銷。
  任務調度以執行線程的常見方法是使用同步隊列,稱作任務隊列。池中的線程等待隊列中的任務,並把執行完的任務放入完成隊列中。

4.2 線程池模式

  線程池模式分爲:HS/HA半同步/半異步模式、L/F領導者與跟隨者模式。

  • 半同步/半異步模式又稱爲生產者消費者模式,是比較常見的實現方式,比較簡單。分爲同步層、隊列層、異步層三層。同步層的主線程處理工作任務並存入工作隊列,工作線程從工作隊列取出任務進行處理,如果工作隊列爲空,則取不到任務的工作線程進入掛起狀態。由於線程間有數據通信,因此不適於大數據量交換的場合;
  • 領導者跟隨者模式,在線程池中的線程可處在3種狀態之一:領導者leader、追隨者follower或工作者processor。任何時刻線程池只有一個領導者線程。事件到達時,領導者線程負責消息分離,並從處於追隨者線程中選出一個來當繼任領導者,然後將自身設置爲工作者狀態去處置該事件。處理完畢後工作者線程將自身的狀態置爲追隨者。這一模式實現複雜,但避免了線程間交換任務數據,提高了CPU cache相似性。在ACE(Adaptive Communication Environment)中,提供了領導者跟隨者模式實現。

4.3 線程池的優點

  1. 提高資源利用率。可以重複使用已經創建了的線程資源;
  2. 提高響應速度。當線程池中的線程沒有超過線程池的最大上限時,有的線程處於等待分配任務狀態,當任務到來時,無需創建線程就能被執行;
  3. 高度可控,可管理。線程池會根據當前系統特點對池內的線程進行優化處理,減少創建和銷燬線程帶來的系統開銷。

  線程池的伸縮性對性能有較大的影響:

  • 創建太多線程,將會浪費一定的資源,有些線程未被充分使用;
  • 銷燬太多線程,將導致之後浪費時間再次創建它們;
  • 創建線程太慢,將會導致長時間的等待,性能變差;
  • 銷燬線程太慢,導致其它線程資源飢餓。

4.4 簡單的線程池實現

#include <vector>
#include <mutex>
#include <thread>
#include <functional>
#include <queue>
#include <atomic>
#include <condition_variable>
#include <iostream>

class thread_pool
{
private:
    using task = std::function<void()>;

    std::atomic_bool _is_running; //顯示線程池的狀態
    std::mutex _mutex;
    std::condition_variable _cond;
    size_t _thread_no;
    std::vector<std::thread> _threads;
    std::queue<task> _tasks;

public:
    thread_pool(const thread_pool &tp) = delete;
    thread_pool &operator=(const thread_pool &tp) = delete;

public:
    thread_pool(size_t no)
        : _thread_no(no),
          _is_running(false)
    {
    }

    ~thread_pool()
    {
        if (_is_running)
            stop();
    }

private:
    void work()
    {
        std::cout << "thread " << std::this_thread::get_id() << " begin to work!" << std::endl;
        while(_is_running)
        {
            task t;
            {
                std::unique_lock<std::mutex> lock(_mutex);
                if(!_tasks.empty())
                {
                    t = _tasks.front();
                    _tasks.pop();
                }
                else if(_is_running && _tasks.empty())
                {
                    _cond.wait(lock);
                }
            }

            if(t)
            {
                t();
            }
        }

        std::cout << "thread " << std::this_thread::get_id() << " going to die!" << std::endl;
    }

public:
    void start()
    {
        this->_is_running = true;
        for (size_t i = 0; i < _thread_no; i++)
        {
            _threads.emplace_back(std::thread(&thread_pool::work, this));
        }
    }

    void stop()
    {
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _is_running = false;
            _cond.notify_all();
        }

        for (std::thread &thd : _threads)
        {
            if (thd.joinable())
            {
                thd.join();
            }
        }
    }

    void append_task(const task &t)
    {
        if (_is_running)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _tasks.push(t);
            _cond.notify_one();
        }
    }
};

void func1()
{
    std::cout << "$thread working in func1 in thread " << std::this_thread::get_id() << std::endl;
}

void func2(int n)
{
    std::cout << "#thread working in func2 in thread " << std::this_thread::get_id() << ";n is " << n << std::endl;
}

void thread_pool_test()
{
    thread_pool pool(10);
    pool.start();
    for (int i = 0; i < 20; i++)
    {
        pool.append_task(std::bind(func2, i));
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }

    pool.stop();
}

  輸出:

thread 140737336633088 begin to work!
thread 140737328240384 begin to work!
thread 140737319847680 begin to work!
thread 140737311454976 begin to work!
thread 140737303062272 begin to work!
thread 140737219917568 begin to work!
thread 140737211524864 begin to work!
thread 140737203132160 begin to work!
thread 140737194739456 begin to work!
thread 140737186346752 begin to work!
#thread working in func2 in thread 140737336633088;n is 0
#thread working in func2 in thread 140737328240384;n is 1
#thread working in func2 in thread 140737319847680;n is 2
#thread working in func2 in thread 140737311454976;n is 3
#thread working in func2 in thread 140737303062272;n is 4
#thread working in func2 in thread 140737219917568;n is 5
#thread working in func2 in thread 140737211524864;n is 6
#thread working in func2 in thread 140737203132160;n is 7
#thread working in func2 in thread 140737194739456;n is 8
#thread working in func2 in thread 140737186346752;n is 9
#thread working in func2 in thread 140737336633088;n is 10
#thread working in func2 in thread 140737328240384;n is 11
#thread working in func2 in thread 140737319847680;n is 12
#thread working in func2 in thread 140737311454976;n is 13
#thread working in func2 in thread 140737303062272;n is 14
#thread working in func2 in thread 140737219917568;n is 15
#thread working in func2 in thread 140737211524864;n is 16
#thread working in func2 in thread 140737203132160;n is 17
#thread working in func2 in thread 140737194739456;n is 18
#thread working in func2 in thread 140737186346752;n is 19
thread 140737336633088 going to die!
thread 140737328240384 going to die!
thread 140737319847680 going to die!
thread 140737303062272 going to die!
thread 140737311454976 going to die!
thread 140737211524864 going to die!
thread 140737203132160 going to die!
thread 140737194739456 going to die!
thread 140737219917568 going to die!
thread 140737186346752 going to die!

5 參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章