線程的那些事

來源:互聯網
上載者:User

標籤:多線程   線程池   線程   

線程的那些事

前言:這篇文章主要小結下linux下多線程的知識點,並且有一些多線程編程中的拓展概念。以及c語言編寫線程池的思路

  • linux線程簡介
  • 線程的私人資料和公有資料
  • 線程的環境切換
  • 建立線程
  • 線程終止
  • 互斥量
  • 條件變數
  • 多線程概念簡單拓展
    • 竟態條件
    • 並發與並行
    • 同步與互斥
    • volatile
    • CAS(compare and swap)
    • 指令重排和記憶體屏障
  • c語言實現線程池思想
linux線程簡介

系統中的進程在某些情況下並不能滿足我們的需求,比如一個作業系統建立進程的數量非常有限,進程的環境切換相對來說比較慢,處理序間通訊(IPC)比較麻煩…等等
所以系統中引入線程這一概念,看看linux下的線程

linux實現線程的機制和其他動作系統有很大區別,從核心的角度,它並沒有線程的概念,所有的線程都被當作進程來實現(輕量級進程) 。線程僅僅被視為和其他進程共用某些資源的進程。

linux下線程的建立和進程的建立類似,只不過在調用clone()系統調用的時候要傳遞一些參數標誌來指明需要共用的資源。

線程調用的clone()
clone(CLON_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
clone函數中的參數就限定了線程,父子共用記憶體空間,檔案系統資源,檔案描述符和訊號處理常式等等。
進程建立fork()
clone(SIGCHLD, 0);
進程建立vfork()
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

核心線程:
有時系統要執行一些後台操作,這些任務可以通過核心線程完成,獨立運行在核心空間的標註進程,最重要的一點它們只運行在核心空間從不切換到使用者空間去。曾經linux非同步IO其中一種實現方式就是通過核心線程來操作,使用者發起一個任務,立即返回,有核心線程協助使用者完成任務,完成後通知使用者。

線程的私人資料和共有資料

每個線程都有自己的私人資料,其中包含執行環境的必須資訊,線程ID,一組寄存器值,棧,調度優先順序,訊號屏蔽字,errno變數,以及線程的私人資料(TSD,一鍵多值技術,可以被多個函數訪問,但是對其他線程是屏蔽的)。
一個進程資訊對所有線程都是共用的,包括可執行程式的代碼,程式的全域記憶體和堆記憶體,棧以及檔案描述符。

例子(看一下進程記憶體映射資料分布):
#include <stdio.h>#include <stdlib.h>#include <assert.h>#include <pthread.h>void *thread_func(void *args){    printf("tid: %u pid: %u thread id: %u\n",             getpid(), syscall(224), pthread_self());    while(1)    {        sleep(10);    }}int main(int argc, char *argv[]){    pthread_t thread;    int count = 0;    while(pthread_create(&thread, NULL, thread_func, NULL) == 0)    {        sleep(5);        ++count;    }    return EXIT_SUCCESS;}

建立了8個線程

此進程的記憶體映射

為/proc/(進程ID)/maps檔案中的內容,我們可以看見進程為建立的線程分布的私人棧(儲存線程棧變數以及cpu切換暫存線程資料等),還有最下面的stack,這個就是進程的棧了。理解了我們就能理解線程的私人資料和共有資料在進程中的大致分布。

環境切換

線程的應用廣泛離不開它的輕量,輕也體現線上程的環境切換。
線程和進程的環境切換最主要的區別是線程的切換虛擬記憶體空間是相同的,但進程是不同的,這兩種切換都會將寄存器變數切換出去(線程私人寄存器變數),會有效能消耗。
另外進程中環境切換會打亂處理器的緩衝機制,簡單的來講,一旦切換出去上下文,處理器中所有已經緩衝的記憶體位址和資料等都作廢了,尤其是進程切換會將整個虛擬記憶體空間切換出去,重新整理了許多緩衝,導致效率降低。但是線上程切換中就不會出現這個問題。

建立線程

參數分別為線程id,線程屬性設定,線程執行函數,線程執行函數參數。
pthread_attr_t是一個結構體,一般我們使用預設的屬性。
編譯時間要連接線程庫
注意:新線程要擷取自己的pid只能使用pthread_self(),因為可能在pthread_create()函數未建立完成時,線程已經開始運行。我們使用線程自己提供的api擷取線程id比較安全

線程終止

pthread_join和pthread_detach函數
pthread_join:

pthread_join參數為線程的id以及一個獲得等待線程執行完成的傳回值的一個二級指標。
pthread_join有兩個功能:
1.主線程來等待子線程結束,如果線程未調用pthread_detach,則必須調用pthread_join來等待線程,如果沒有等待,一方面主線程會不等待子線程從而提前結束,那麼子線程可能會沒機會運行,其次則會產生殭屍線程,與殭屍進程的概念類似,除了浪費系統資源外,如果存在大量殭屍線程,我們將可能無法建立新的線程。
2.線程之間是平等的,也就是說我們除了主線程等待子線程用pthread_join外,我們還可以串連任意兩兩線程,一個線程調用pthread_exit()並且返回一個數值,另外一個線程調用pthread_join()來接受傳回值並且等待另外線程結束。

pthread_detach:

pthread_detach函數參數僅為線程id,意義為將指定id的線程變為後台運行,由系統來為它回收資源。

互斥量(互斥鎖)

多個線程同時訪問一個資源可能會出現不能預料到的結果,此時我們可以通過互斥量來保證線程同步訪問資源。
當我們對一個臨界資源(某時刻只能有一個線程或進程訪問的資源)進行訪問時,必須先拿到互斥鎖,否則阻塞在鎖上,當我們搶到互斥鎖時就可以訪問資源,訪問完成時釋放鎖供其他進程或線程得到並繼續交替訪問臨界資源。
初始化:pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
上面使用的是靜態(預設)初始化,我們也能通過設定互斥鎖的屬性(動態初始化)來改變互斥鎖從而滿足我們使用的情境。使用了動態初始化我們則必須銷毀互斥鎖

//標頭檔:#include <pthread.h>//函數原型:int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);//標頭檔:#include <pthread.h> //函數原型:int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意以下幾點:
1.

1.避免線程對多個共用資源的互斥量進行訪問而導致的死結,我們應該對互斥鎖進行設定層級關係,也就是說加鎖順序必須確定

線程對多個共用資源的互斥量進行訪問

如果我們強制設定加鎖順序為mutex1後mutex2,也就是說mutex1為一級鎖,mutex2為二級鎖,則不會出現上面的情況。

2.

遞迴互斥鎖,也稱為可重新進入鎖,簡單來講就是我們可以重複對遞迴互斥鎖加鎖而不會出現普通互斥鎖未釋放鎖就加鎖的死結情況。
遞迴互斥鎖適用於如下情境

func_one(){    lock_mutex;    dosomething();    unlock_mutex;}func_two(){    lock_mutex();    dosomething();    func_one();    unlock_mutex;}

3.

pthread_mutex_errorcheck
對此類互斥鎖錯誤使用會進行報錯。

條件變數

互斥量防止多個線程同時訪問某一共用變數,條件變數允許一個線程就某個共用變數的狀態變化通知其他線程,並讓其他線程等待。
預設我們使用互斥鎖時,如果鎖此時被其他線程持有,那麼後來的線程就預設阻塞,這樣效率是非常低的,現在我們在想讓線程擷取互斥鎖的地方調用條件變數的wait函數等待條件變數的條件變化從而通知我們,我們通過條件變數來避免線程阻塞,當持有鎖的線程釋放鎖時,線程被通知。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
PTHREAD_COND_INITIALIZER是一個宏定義的預設結構體成員,上面代碼是給條件變數設定預設的屬性。我們也可以通過init來設定條件變數的屬性。

//初始化條件變數,attr是條件變數的屬性,比如更改條件等。pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);//銷毀條件變數,必須在沒有使用時銷毀pthread_cond_destroy(pthread_cond_t *cond);//阻塞某一線程,直至收到條件變數的cond的通知pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t *mutex); //帶休眠時間的條件變數 pthread_cond_timewait()//通知等待的任意一個線程pthread_cond_signal(pthread_cond_t *cond);//通知所有等待互斥量的線程pthread_cond_broadcase(pthread_cond_t *cond);
多線程概念簡單拓展竟態條件

從多線程的角度來說就是多個線程對同一塊資源進行訪問時,最終結果取決於線程執行的順序,但是線程執行的順序不固定,每一執行的結果也就是不確定的。

並發與並行

並發:同一時間,同時發生的事情
並行:同一時間,同時在做的事情
簡單的來說就並發是指同一時間我們能接受多少,並行是同一時間我們能處理多少。
舉個例子:老媽在做飯,讓我幫忙幹活,短短的一分鐘她說了四件事,去超市買鹽,去收衣服,去掃地,並囑咐我喝完桌子的飯, 沒辦法我只能答應- -,這就是並發,我不會分身術,我只有兩隻手,我可以邊掃地邊喝桌子上的飯,這是並行。
再說個socket的例子吧,都知道epoll IO複用解決了C10K問題,它可以同時接受10000左右的串連數,這指的是並發, 但是一台電腦只有兩個CPU,
同時只能有幾個線程在上面跑,這是並行。

同步與互斥

互斥:指的是同一件事或者同一個東西,在同一時刻在只允許一個使用者在使用或者在做。
同步:在互斥的基礎上有序的進行訪問。注意同步並沒有說什麼同一時刻之類的。

volatile

線程間同步的一種手段,但是並不保證能正確得到結果,volatile保證每次讀取資料都是從記憶體中最新的而不是緩衝中。
缺點:
volatile關鍵字能保證可見度,但不能保證原子性(不可在被分割),可見度只能保證每次讀取的是最新的值,但是volatile無法保證對變數等操作的原子性。
例子:
線程1對變數進行遞增操作,線程1讀取了變數i的初始值為10,然後時間片到了,輪到線程2,線程2對變數進行自增操作,線程2也去讀取i的原始值10,由於線程1隻是對i進行讀取並沒有修改,線程2修改後寫入主存11,此時輪到線程1執行,線程1修改的還是初始值10,然後寫回記憶體中修改後的11,等於兩次操作一次修改。

CAS

CAS(compare and swap)是一種策略,當A線程訪問一個變數時,先取出舊值並儲存,然後儲存變化後的新值,此時再次比較舊值和記憶體中的值是否一致(一致說明沒有被其他線程修改),如果一致則更新值。

指令重排和記憶體屏障

指令重排:一般處理器不會按照我們代碼的順序來執行而是會重新排序代碼形成的指令,按照最順暢最快的方式來執行。
比如代碼有三件事情要做,但是第一和第三件可以連著做,那麼cpu就會重排指令變為一三二,為了達到更快的執行速度。

記憶體屏障:指令重排在多線程情況下可能會產生不確定的結果
也被成為“記憶體柵欄”, 簡單的講就是嚴格的限定指令的執行順序,也就是禁止特定類型的處理器重排。也是為了避免多線程並發受到影響。

c語言實現線程池的思想

沒寫過線程池的童鞋都會有一些誤解,所以藉著這篇部落格說一下c語言實現線程池的思想。
一般對線程池糾結的地方會有如下。
線程是如何儲存在“池”中的?
線程是如何被調度的?
線程如何被借用執行任務然後在歸還?歸還過程是怎麼樣的?
線程池要執行的任務又是如何儲存的?

線程池一般是一個隊列,元素為建立的線程的id,我們通過線程id來控制線程,線程池要執行的任務也是儲存在一個隊列或鏈表中,裡面儲存的是函數指標,也稱為callback。

那麼線程池一般維持一個線程池結構體,內部成員包含:
互斥鎖(保證任務存取隊列每時每刻只有一個線程操作,避免竟態條件)
條件變數(當任務隊列從空變為非空時,要通過條件變數來喚醒)
線程id鏈表(也就是存放線程id的隊列或鏈表,管理所有線程)
任務隊列(存放callback函數指標)
線程池大小(capacity容量)
任務隊列大小(要判斷當前任務隊列是否為空白)
毀壞標記flag(如果想要銷毀線程池需要有毀壞標記)
任務結構體成員就包括:
callback函數指標(調用執行任務)
callback函數參數arg(傳遞給callback的參數)
關鍵點:
其實所有線程一開始建立完成後執行的線程函數內都是一個while(1)死迴圈,所有的線程都會去搶任務隊列的鎖,搶到了則執行任務釋放鎖,每次搶任務時會檢查任務隊列的大小,如果為0則線程阻塞,當向任務隊列中新增工作時,我們通過條件變數的同步來喚醒阻塞的線程。搶不到的則阻塞在鎖上,並沒有所想的那種線程執行完成後歸還到“池”裡需要則調用再次繼續取出線程的過程。線程執行完繼續搶任務(實際上是鎖),搶不到就阻塞。搶到了就執行,沒有所謂的歸還過程。while(1)迴圈裡會存在檢查flag標記,如果標記被修改,則銷毀線程。

完

著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

線程的那些事

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.