linux多線程--POSIX Threads Programming

linux多線程本身從接觸好久也有很多實踐,但老是以爲理解不夠深入,不夠系統。借這篇文章試着再次系統學習一下linux多線程編程,理解編程的concept,細緻看一下POSIX pthread API的實現。仍是憑藉強大的google search,找到幾篇不錯的文章和教程附在最後。我在這篇文章中的總結大多都是基於這些材料的學習和本身實踐經驗的一點總結。html

Thread基本知識

Process 地址空間

Thread附着在process內部,先看一下process在CPU上是個什麼樣的吧。啓動一個linux process,OS會開闢一塊內存用來裝載code,保存data和process狀態。看一下進程的地址空間。linux

clipboard.png
根據訪問權限,進程地址空間分爲user space和kernel space。32bit系統中高1G爲kernel space,低3G爲user space,具體劃分爲:編程

  • Process control block(從高1G kernel space中分配)windows

  • stack安全

  • memory mapping segment多線程

  • heapapp

  • bss and datasocket

  • text函數

1G的kernel space是這臺機器上全部processes共享的,每一個進程的PCB存在這個空間中,通常應用程序是沒有辦法直接訪問修改的,可是kernel 經過/proc 提供給應用程序一個接口能夠查看PCB的信息,部份內容還能夠修改,詳細能夠看一下/proc。剩下的stack/heap/text/...都駐留在process user space,是屬於process私有空間。詳細的kernel如何管理進程memory還能夠再開一篇。oop

Thread是什麼?

process是個重型的運行實體,以process爲單位切分任務和調度,os的開銷太大了。咱們能夠把process這個單位再切小些,thread的概念就誕生了。好,咱們來看一下怎樣把這個單位切小的。簡單來說,thread共享大部分的process的內容,只維護必需的一小部分做爲私有內容。

clipboard.png

Thread本身維護的私有內容

  1. Kernel space

    • Stack pointer

    • Registers

    • Scheduling properties (such as policy or priority)

    • Set of pending and blocked signals

    • Thread specific data.

  2. User space

    • stack

其餘諸如PCB中進程信息,用戶空間中的text/data/heap/...都是同一個process下全部Threads共享的。有了這些thread本身私有的信息,os就能夠以thread爲單位去調度了。由於它是輕量級的,因此相比process,thread通常具備更好的性能,更快的響應速度。可是thread的穩定性和編程複雜度要比process差些,要考慮的內容比較多。

Threads通訊和同步

正由於同一個process內的threads間自然共享了大量的內存,thread間的信息交互要比較高效,同時也增長了複雜度,總要處理好共享內存間的互斥。固然process間也能夠共享內存,好比經過進程父子關係,或者經過/dev/shm mmap特定物理內存到進程空間內或者其餘。

線程間通訊

clipboard.png
全部的IPC(inter process communication)方法都適用於thread間的通訊。比較全的IPC總結,能夠參考IPC。比較經常使用的咱們會涉及到message queue,sharememory,semaphore,socket,signal等。semaphore是共享資源互斥的方法,其餘都是冗餘的方式進行通訊。互斥是個比較複雜的話題,咱們單開一節討論一下。

共享資源的互斥

爲何要保護共享資源作互斥訪問,這裏不羅嗦了。經過對共享資源(臨界區)加鎖能夠實現互斥訪問,互斥鎖(mutex)也有多種類型。

  • simple blocking
    一方拿到臨界區鎖後,其它人再來拿鎖都會掛起。

  • Recursive(遞歸型)
    容許鎖的擁有者屢次申請鎖而不被掛起,對遞歸調用有用。

  • Reader/Writer
    容許多個reader同時share讀鎖,若是有reader在讀,writer申請鎖會block直到全部reader釋放。能夠理解爲一寫多讀,寫時互斥。這種鎖有寫餓死的風險。

其中POSIX的pthread庫支持recursive和reader/writer類型的鎖。

共享訪問帶來的風險和挑戰

共享訪問中有寫操做,必然要考慮互斥。互斥有風險,使用需謹慎。若是你最終不可避免的要使用互斥鎖,要關注互斥鎖的這些風險。

  1. deadlock(死鎖)
    死鎖通常發生在雙方或者多方在申請兩個以上的互斥鎖,而後你們各拿了部分,各執己見。開發者要儘可能避免這種編程場景發生,若是真的須要能夠編程要麼同時得到,要麼一個都不要,作人要有骨氣!

  2. race condition(競爭條件)
    共享資源在沒有互斥機制保護時,因爲線程調度的不肯定性會致使共享的資源變化無序無規律,程序的輸出也就不肯定了。共享資源無互斥保護,線程間競爭訪問,輸出沒法保證。這要求開發者要特別當心識別出程序中的那些共享資源,加鎖保護。尤爲是第三方的開源軟件,多線程調用時要注意是不是線程安全的。

  3. priority reversion(優先級反轉)
    優先級反轉是個頗有意思的問題,尤爲是在嵌入式實時OS上,進程/線程的調度是搶佔式的,高優先級的任務ready時能夠直接搶佔CPU,這事再加上互斥就容易出問題了。好比三個任務H,M,L,優先級遞減,同時H和L共享資源R。當L先申請到互斥鎖訪問臨界區還沒釋放R的時候,H這時候申請R訪問致使本身掛起,這麼巧M變ready了,OS調度讓M搶佔了L的cpu。若是L一直得不到執行並釋放R,這樣就形成了高優先級的H得不到執行,反而一些比H優先級低的M們能獲得CPU。這就是優先級反轉。實時OS的高優先級任務通常都是比較重要的任務須要立刻處理,得不處處理意味着可能要出大事。因此這個問題的影響仍是挺大的,比較著名的例子就是火星探路者的故事,能夠參考一下火星探路者故障分析。解決方法也有很多

    • 儘可能避免不一樣優先級的任務共享資源,能夠經過信息容易作任務間通訊。

    • 訪問臨界區時關閉中斷,保證臨界區的代碼執行不被強佔。嵌入式編程中經常使用。

    • 優先級繼承,當有高優先級任務想要訪問共享資源時,提升正在執行的低優先級任務的優先級到高優先級級別直至退出臨界區。上面的探路者修正程序使用了該方法。

    • 隨機提升ready且持有鎖的任務優先級,windows用了該方法。

Multi Threads應用場景

寫了這麼多,那到底何時能夠應用多線程來解決問題呢?根據經驗,通常下面一些場景咱們能夠考慮使用多線程。

  • 多核處理器,任務比較容易切分爲並行處理的小塊。若是是計算密集型的,線程數量能夠考慮跟core的數量至關。

  • 有delay比較多的IO操做,能夠考慮將IO的操做分離給單獨的線程。

  • 有人機交互和實時響應等實時性要求較高任務,能夠考慮分離爲優先級較高的線程。

  • 有大量實時要求不高的計算,能夠考慮分離爲優先級較低的後臺任務。

Thread編程模型

實事求是,具體問題具體分析是放之四海而皆準的問題解決之道,因此沒有普適的編程模型。下面列舉3種應用比較多的模型以供學習。

  1. Thread Pool (Master/Worker)
    經過線程池維護一組可用的線程,master做爲主線程負責管理維護worker線程,同時負責對外接口和工做的分發。

  2. Peer (Workcrew)
    跟master/worker相似,只是master在啓動線程池後退化爲普通一員,你們一塊兒分擔任務,沒有主從的星形拓撲結構。

  3. Pipeline
    跟CPU的pipline技術相似,將一個工做流分紅不少串行的部分,每一部分都由不一樣的線程負責,你們各司其職,我作完個人工做就轉交給下一個線程,齊心合力最後完成整個工做。流水線若是拍的好能夠很好的提升工做效率,可是這種模型風險也比較大,必定要處理好工做的切分,和線程間的交互。

POSIX API詳解

Thread management

pthread_create (thread,attr,start_routine,arg) #建立thread
pthread_exit (status) # thread退出
pthread_cancel (thread) # 退出指定的thread
pthread_attr_init (attr) #初始化thread屬性
pthread_attr_destroy(attr)
pthread_setaffinity_np or sched_setaffinity # 設置thread可運行的CPU,也就是綁定CPU
pthread_join (threadid,status) # 阻塞等待threadid指定的thread完成
pthread_detach (threadid) # 線程建立默認是joinable,調用該函數設置線程的狀態爲detached,則該線程運行結束後會自動釋放全部資源,別人再join等待它時候不會阻塞了。
pthread_attr_setdetachstate (attr,detachstate)
pthread_attr_getdetachstate (attr,detachstate)
pthread_self () # 返回本身所在的線程id
pthread_equal (thread1,thread2) # 比較兩個線程

大部分API見名思意比較簡單,詳細看一下pthread_create.

#include <pthread.h>
   int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
   
   參數說明:
   thread: 指針,所指向的內存存儲新建立thread的屬性,返回給caller來標識該線程
   attr: thread的配置參數集
   start_routine: thread建立後執行的處理函數,thread的主體
   arg: start_routine的入參
   
   功能說明:
   建立thread API,成功後返回0. 建立的thread跟建立者是平行關係,沒有等級繼承關係。
   thread有如下屬性
        Detached or joinable state
        Scheduling inheritance
        Scheduling policy
        Scheduling parameters
        Scheduling contention scope
        Stack size
        Stack address
        Stack guard (overflow) size

Mutexes

pthread_mutex_init (mutex,attr) # 動態生成一個mutex變量
pthread_mutex_destroy (mutex) # 釋放mutex變量
pthread_mutexattr_init (attr) # 設置mutex屬性
pthread_mutexattr_destroy (attr)
pthread_mutex_lock (mutex) # lock操做,若是mutex已經lock調用者會阻塞
pthread_mutex_trylock (mutex) # 嘗試lock,非阻塞調用
pthread_mutex_unlock (mutex) # unlock操做

Condition variables

pthread_cond_init (condition,attr)
pthread_cond_destroy (condition)
pthread_condattr_init (attr)
pthread_condattr_destroy (attr)
pthread_cond_wait (condition,mutex) # 調用者阻塞直到condition條件成立,注意調用者阻塞時會自動釋放mutex,喚醒時會自動lock mutex。調用前確保lock mutex,調用後確保調用unlock mutex
pthread_cond_signal (condition) # 通知對方條件知足,調用前確保lock mutex,調用後確保調用unlock mutex
pthread_cond_broadcast (condition)

條件變量是另一種線程間同步的方式,實際上是一種掛起和喚醒的通訊方式。能夠理解爲定義一個條件變量定義了一個線程間的通訊通道,wait這個變量一方實際上是在等待有人在這個通道上發個信號來,若是沒有人發信號他就一直阻塞掛起。它須要跟mutex配合使用,直接經過一個例子感覺一下。條件變量的存在就是讓wait的這一方睡起來直到有人通知它條件知足能夠起來幹活了,不然沒有條件變量只用mutex作同步,這個wait的一方須要不斷的查詢是否條件知足,低效浪費。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define NUM_THREADS  3
#define TCOUNT 10
#define COUNT_LIMIT 12

int     count = 0;
pthread_mutex_t count_mutex;
pthread_cond_t count_threshold_cv;

void *inc_count(void *t) 
{
  int i;
  long my_id = (long)t;

  for (i=0; i < TCOUNT; i++) {
    pthread_mutex_lock(&count_mutex);
    count++;

    /* 
    Check the value of count and signal waiting thread when condition is reached.  Note that this occurs while mutex is locked. 
    */
    if (count == COUNT_LIMIT) {
      printf("inc_count(): thread %ld, count = %d  Threshold reached. ",
             my_id, count);
      pthread_cond_signal(&count_threshold_cv);
      printf("Just sent signal.\n");
      }
    printf("inc_count(): thread %ld, count = %d, unlocking mutex\n", 
       my_id, count);
    pthread_mutex_unlock(&count_mutex);

    /* Do some work so threads can alternate on mutex lock */
    sleep(1);
    }
  pthread_exit(NULL);
}
void *watch_count(void *t) 
{
  long my_id = (long)t;

  printf("Starting watch_count(): thread %ld\n", my_id);

  /*
  Lock mutex and wait for signal.  Note that the pthread_cond_wait routine
  will automatically and atomically unlock mutex while it waits. 
  Also, note that if COUNT_LIMIT is reached before this routine is run by
  the waiting thread, the loop will be skipped to prevent pthread_cond_wait
  from never returning.
  */
  pthread_mutex_lock(&count_mutex);
  while (count < COUNT_LIMIT) {
    printf("watch_count(): thread %ld Count= %d. Going into wait...\n", my_id,count);
    pthread_cond_wait(&count_threshold_cv, &count_mutex);
    printf("watch_count(): thread %ld Condition signal received. Count= %d\n", my_id,count);
    printf("watch_count(): thread %ld Updating the value of count...\n", my_id,count);
    count += 125;
    printf("watch_count(): thread %ld count now = %d.\n", my_id, count);
    }
  printf("watch_count(): thread %ld Unlocking mutex.\n", my_id);
  pthread_mutex_unlock(&count_mutex);
  pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
  int i, rc; 
  long t1=1, t2=2, t3=3;
  pthread_t threads[3];
  pthread_attr_t attr;

  /* Initialize mutex and condition variable objects */
  pthread_mutex_init(&count_mutex, NULL);
  pthread_cond_init (&count_threshold_cv, NULL);

  /* For portability, explicitly create threads in a joinable state */
  pthread_attr_init(&attr);
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
  pthread_create(&threads[0], &attr, watch_count, (void *)t1);
  pthread_create(&threads[1], &attr, inc_count, (void *)t2);
  pthread_create(&threads[2], &attr, inc_count, (void *)t3);

  /* Wait for all threads to complete */
  for (i = 0; i < NUM_THREADS; i++) {
    pthread_join(threads[i], NULL);
  }
  printf ("Main(): Waited and joined with %d threads. Final value of count = %d. Done.\n", 
          NUM_THREADS, count);

  /* Clean up and exit */
  pthread_attr_destroy(&attr);
  pthread_mutex_destroy(&count_mutex);
  pthread_cond_destroy(&count_threshold_cv);
  pthread_exit (NULL);

}

Synchronization

pthread_rwlock_destroy
pthread_rwlock_init
pthread_rwlock_rdlock
pthread_rwlock_timedrdlock
pthread_rwlock_timedwrlock
pthread_rwlock_tryrdlock
pthread_rwlock_trywrlock
pthread_rwlock_unlock
pthread_rwlock_wrlock
pthread_rwlockattr_destroy
pthread_rwlockattr_getpshared
pthread_rwlockattr_init
pthread_rwlockattr_setpshared

上面提到的讀寫鎖。容許多個reader同時share讀鎖,若是有reader在讀,writer申請鎖會block直到全部reader釋放。

參考文章
POSIX Threads Programming
Multithreaded Programming (POSIX pthreads Tutorial)
Multi-Threaded Programming With POSIX Threads
POSIX thread (pthread) libraries

相關文章
相關標籤/搜索