天天看點

多線程程式設計(1)線程概念線程控制分離線程和結合線程線程同步和互斥死鎖條件變量

線程概念

線程線程就是程序的若幹個執行流,

因為一個程序在某一時刻隻能去做一件事情,有了線程之後,我們可以在同一時間去做不同的事情,比如我正在邊利用cmd markdown寫部落格。邊用網易雲音樂聽音樂,這樣多線程的情況下,能給我們帶來很多好處。

多線程程式設計(1)線程概念線程控制分離線程和結合線程線程同步和互斥死鎖條件變量
多線程程式設計(1)線程概念線程控制分離線程和結合線程線程同步和互斥死鎖條件變量

在系統核心中其實是不存線上程的,linux使用程序模拟線程,線程的實作其實就是多個共享資料代碼等資訊的程序。是以我們把線程也叫做輕量級程序。

程序常常用來配置設定資源,線程用來排程資源。

線程中共享的資源:

- 檔案描述符表

- 信号處理方式

- 目前工作目錄

- uid,gid

線程中獨立的資源:

- 線程id(tid)

- 線程的上下文資訊,寄存器資訊,PC制作,棧指針

- 棧空間

- errno變量

- 信号屏蔽字

- 排程優先級

- 線程私有資料

線程的函數大部分都放在pthread.h的頭檔案當中,并且在編譯的時候我們需要注意的是加上-lpthread選項,這樣就會去動态連結動态庫。

線程控制

線程的控制

  • 建立線程

    線程的建立使用線程建立函數。

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
           

需要注意的是線程建立函數的參數,第三個參數start_rountine就是新建立的線程所要跑的函數,arg是你傳入的參數。

因為在linux環境下,系統内部其實是不存線上程的,我們一般說線程叫做輕量級程序,線程建立以後,兩個線程的pid和ppid都是一樣的。作業系統會為之提供一個線程的tid,這個tid我們可以通過一個函數擷取:

pthread_t pthread_self(void);
           

需要注意的是pthread_self所擷取到的是相對于程序的線程控制塊的首位址,隻是用來描述統一程序當中的不同的線程。而真正的核心當中的線程ID,對于多線程程序來說,每個tid實際是不一樣的。

另外,在linux當中如果你要查詢系統中的線程,也有一條指令:ps -aL

  • 線程等待

    在程序當中我們提到過,如果我們當子程序結束以後,這個時候需要讓父程序得到資訊,然後由父程序去進行資源的回收,以及後續的處理,是以我們要確定子程序先結束,然後父程序再結束,否則會出現僵死狀态,當值記憶體洩漏的問題。是以程序當中我們提到了wait和waitpid函數。

    線程當中同樣有上述的問題,當你的新線程結束,你的主線程也是需要等待,然後回收新線程的資源及其他資訊。這樣就能確定記憶體不洩露。是以這裡使用一個函數pthread_join函數來進行等待。

int pthread_join(pthread_t thread, void **retval);
           

thread就是線程号,retval是一個二級指針,用途是用來擷取線程的退出碼。

  • 終止線程

    線程可以等待,當然也可以終止。終止線程可以使用三種方法:

方法
1 使用return傳回,在主線程當中執行的時候類似于exit,直接結束程序
2 使用pthread_exit函數,終止自己的線程
3 使用pthread_cancel函數,可以用來終止統一程序的線程
void pthread_exit(void *retval);
           

pthread_exit用來終止線程自身,參數是傳回的錯誤碼,想要獲得這個錯誤碼,可以通過pthread_join來獲得。

int pthread_cancel(pthread_t thread);
           

pthread_cancel參數為要終止線程的tid,

分離線程和結合線程

線程是可結合或者是分離的。

一個可結合的新線程需要被主線程回收資源和殺死。一個可結合的線程會容易出現類似于僵屍程序的問題,一般我們采用join來等待。否則就會出現主線程無法擷取到新線程資訊,無法回收新線程的資源,這樣就會造成記憶體洩漏的問題。我們在預設的情況下,線程是可結合的。

int pthread_detach(pthread_t thread);
           

而對于分離線程,當我們把新線程設為可分離的時,這個時候主線程不再等待新線程,可分離以後,這個時候的新線程是由作業系統來進行考慮。不再由主線程來考慮,在主線程中分離,這個時候主線程知道與新線程分離,這樣的話主線程是無法進行等待join的。

而在新線程分離了的話,這個時候主線程有可能不知道新線程的分離,這個時候的主線程可能會依然去join。

線程同步和互斥

當一個線程可以修改的變量,其他線程也可以讀取或者修改的時候,這個時候就需要對這些線程進行同步,確定它們在通路變量的存儲内容時不會通路到無效的值。其實實質就是當存儲器讀和存儲器寫這兩個周期交叉的時候,就會使得得到與預想結果不一緻的值。

線程的同步和互斥。當我們使用多線程的時候,多個線程對臨界資源進行操作,這個時候如果非互斥的,那麼這個時候對同一份臨界資源進行操作的時候就會出現沖突的時候,比如當你對臨界資源操作的時候,可能會中途進行線程的切換,這個時候原本你所讀取的狀态會随着硬體上下文和pc指針這些東西會儲存下來,切換了線程以後,新切換的線程可能會去讀取前一次你所讀取的臨界資源,然後對這份臨界資源進行修改,然後這個時候新線程可能會再次切換,切換到你所原來儲存的線程中,然後,回複了以前儲存的硬體上下文和pc指針這些内容以後,這個時候線程所讀取的臨界資源的狀态等資訊還是在沒有修改之前的,是以這個時候就會有隐患,造成一些缺點。

從一個變量增加的操作我們看待這個問題,首先我們要清楚一個增量操作分為三步驟:

- 1)從記憶體當中讀入寄存器

- 2)寄存器進行增量操作

- 3)寫回記憶體

正因為這三步操作,是以當不同步的時候,第一個線程已經對增量操作了,但是第二個線程讀取到的依然是第一個線程增量操作之前的内容。這樣就會出現問題,本來應該由1增加到3的,結果變為了由1到2。

是以從上面所說,可以發現多線程很容易發生上述的通路沖突,是以這裡作業系統為我們提供了一個機制叫做互斥鎖,這個互斥鎖,我們可以去想前面所說的程序間通信的信号量,獲得鎖的線程可以對臨界資源進行操作,沒有獲得鎖的資源阻塞等待,二進制信号量類似,獲得鎖的資源可以進行操作,沒有獲得鎖的資源挂起程序放入挂起隊列。

使用鎖的時候我們需要對鎖進行初始化:

可以去調用初始化函數或者定義鎖利用宏進行初始化。

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
           

加鎖的函數:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
           

當我們建立了鎖以後,我們就需要去銷毀鎖。

int pthread_mutex_destroy(pthread_mutex_t *mutex);
           

這三個函數第一個lock進行阻塞式申請鎖,就是當你沒申請到,那麼就是一直阻塞等待。第二個函數trylock是若鎖資源無法獲得,這個時候不阻塞,進行詢問。沒調用一次,進行詢問一次,最後一個unlock,這個函數就是用來解鎖的,無論是lock還是trylock最後都要調用unlock來進行解鎖。

另外需要注意,鎖的所有操作都應該是原子的。要麼執行要麼不執行,并且中間不能夠被打斷。

是以這裡需要來說下關于互斥鎖的實作:

為了實作對mutex便利的讀取、判斷、修改都是原子操作,是以大多數的體系結構都提供swap或exchange指令,這個指令把寄存器和記憶體單元的資料相交換,因為隻有一條指令,是以保證了原子性。

如果你要讓線程進行切換,那麼就可以有兩種方式,一種是跑完時間片,這樣一個線程就會切換。另外一種方式就是模式的切換,從核心态切換到使用者态。這個期間會檢查内部資訊,簡單的模式切換就是調用系統調用,系統調用本身是作業系統暴露給使用者的一些接口,這些接口大部分内容都是相關于核心的,是以會發生從使用者切換到作業系統 。

說完了互斥,接下來我們需要說一下同步的概念,關于同步的概念:強調程序間協同,按照順序的去通路臨界區,一般都是在互斥下進行同步。

使用互斥鎖的例子:

#include<stdio.h>
#include<pthread.h>
#include<sys/types.h>
#include<unistd.h>
int count=;

void * pthread_run(void *arg)
{
    int val=;
    int i=;
    while(i<)
    {
        //這裡會出現問題,就是當兩個線程進行操作的時候count的+是非原子的,
        i++;
        val=count;
        printf("pthread: %lu,count:%d\n",pthread_self(),count);
        count =val+;

    }
    return NULL;
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;

    pthread_create(&tid1,NULL,&pthread_run,NULL);
    pthread_create(&tid2,NULL,&pthread_run,NULL);
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);

    printf("couint :%d\n",count);
    return ;
}
           
多線程程式設計(1)線程概念線程控制分離線程和結合線程線程同步和互斥死鎖條件變量

上述程式就會出現問題,就是兩個線程tid1和tid2在跑while中的邏輯的時候,這個時候就會發生通路沖突,count本來應該是10000的,但是因為通路沖突,是以最終的結果count結果要小于10000,原因上面我已經介紹過了,就不再重複,要想實作互斥,我們加上互斥鎖就好了。

#include<stdio.h>
#include<pthread.h>
#include<sys/types.h>
#include<unistd.h>

//關于鎖的初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int count=;

void * pthread_run(void *arg)
{
    int val=;
    int i=;
    while(i<)
    {
        //在臨界區加上互斥鎖,這樣就可解決線程通路沖突的問題了
        pthread_mutex_lock(&mutex);
        i++;
        val=count;
        printf("pthread: %lu,count:%d\n",pthread_self(),count);
        count =val+;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;

    pthread_create(&tid1,NULL,&pthread_run,NULL);
    pthread_create(&tid2,NULL,&pthread_run,NULL);
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);

    printf("couint :%d\n",count);
    return ;
}
           

這樣最終count的結果就是100

多線程程式設計(1)線程概念線程控制分離線程和結合線程線程同步和互斥死鎖條件變量

死鎖

我們想一個問題,就是我們加鎖以後,再次進行加鎖,這樣會發生什麼呢?

當我們第二次申請鎖的時候,這個時候鎖已經被占用,該線程挂起等待直到線程鎖釋放。但是這個時候擁有鎖的剛好是目前的這個線程,那麼這個線程就這樣永遠挂起等待,這個我們就叫做死鎖。

死鎖發生的情形
一個線程兩次申請鎖
兩個線程互相申請鎖,不釋放鎖

死鎖産生的必要條件:

死鎖産生的必要條件
請求與保持 一個程序因請求資源而阻塞時,對已獲得的資源保持不放
互斥條件 一個資源隻能被一個程序使用
不可剝奪和不可搶占 程序已經獲得的資源,未使用完之前,不能被處理器和排程器強行剝奪。資源隻能由占有者自己釋放,不可被優先級更高的程序
環路等待 若幹程序之間形成一種頭尾相接的循環等待資源關系

解決死鎖的方法:

-解決死鎖的方法-
破環互斥 破環了互斥,資源共享,這樣就解決了死鎖
破環不可剝奪不可搶占 放棄原先占有的資源,另外切換到比目前線程優先級高的線程
資源一次性配置設定 破壞請求和保持
資源有序配置設定 對資源按照編号進行申請鎖,確定鎖的配置設定順序,防止循環

對于死鎖,我們最常用的就是死鎖預防和死鎖避免,簡單的講死鎖預防就是防止出現死鎖的狀況。

死鎖預防:

1. 破環互斥條件:允許系統資源共享,系統不會進入死鎖。

2. 破壞不可剝奪、不可搶占條件:如果占有某些資源的一個程序進行進一步資源請求被拒絕,則該程序必須釋放它最初占有的資源,如果有必要,可再次請求這些資源和另外的資源。

3. 破壞請求和保持條件:采用靜态配置設定方法,程序在運作前一次申請完它所需要的全部資源,在它的資源未滿足前,不投入運作。一進入運作,也就不再提出其他的資源請求。

4. 破環環路等待條件,采用資源有序配置設定法,破環環路條件。

死鎖避免:

如果一個程序請求會導緻死鎖,則不啟動它。

如果一個程序增加的資源請求會導緻死鎖,則不允許此配置設定。

解決死鎖的基本方法:

預防死鎖:

資源一次性配置設定:(破壞請求和保持條件)

可剝奪資源:即當某程序新的資源未滿足時,釋放已占有的資源(破壞不可剝奪條件)

資源有序配置設定法:系統給每類資源賦予一個編号,每一個程序按編号遞增的順序請求資源,釋放則相反(破壞環路等待條件)

避免死鎖:

預防死鎖的幾種政策,會嚴重地損害系統性能。是以在避免死鎖時,要施加較弱的限制,進而獲得 較滿意的系統性能。由于在避免死鎖的政策中,允許程序動态地申請資源。因而,系統在進行資源配置設定之前預先計算資源配置設定的安全性。若此次配置設定不會導緻系統進入不安全狀态,則将資源配置設定給程序;否則,程序等待。其中最具有代表性的避免死鎖算法是銀行家算法。

檢測死鎖

首先為每個程序和每個資源指定一個唯一的号碼;

然後建立資源配置設定表和程序等待表,例如:

解除死鎖:

當發現有程序死鎖後,便應立即把它從死鎖狀态中解脫出來,常采用的方法有:

剝奪資源:從其它程序剝奪足夠數量的資源給死鎖程序,以解除死鎖狀态;

撤消程序:可以直接撤消死鎖程序或撤消代價最小的程序,直至有足夠的資源可用,死鎖狀态.消除為止;所謂代價是指優先級、運作代價、程序的重要性和價值等。

條件變量

條件變量的提出首先要涉及一個概念,就是生産者消費者模型,

多線程程式設計(1)線程概念線程控制分離線程和結合線程線程同步和互斥死鎖條件變量

生産者消費者,是在多線程同步的一個問題,兩個固定大小緩沖區的線程,在實際運作是會發生問題,生産者是生成資料放入緩沖區,重複過程,消費者在緩沖區取走資料。

生産者消費者的模型提出了三種關系,兩種角色,一個場所

三種關系:

- 生産者之間的競争關系

- 消費者之間的競争關系

- 生産者和消費者之間的關系

兩個角色:生産者和消費者

一個場所:有效的記憶體區域。

我們就可以把這個想象成生活中的超市供貨商,超市,顧客的關系,超市供貨商供貨,超市是擺放貨物的場所,然後使用者就是消費的。

條件變量屬于線程的一種同步的機制,條件變量與互斥鎖一起使用,可以使得線程進行等待特定條件的發生。條件本身是由互斥量保護的,線程在改變條件狀态之前首先會鎖住互斥量。其他線程在獲得互斥量之前不會察覺這種改變,是以互斥量鎖定後才能計算條件。

和互斥鎖一樣,使用條件變量,同樣首先進行初始化:

int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
           

和互斥鎖的初始化一樣,它也可以采用init或者是直接利用宏進行初始化。

條件變量本身就是依賴互斥鎖的,條件本身是由互斥量保護的,線程在改變條件狀态錢先要鎖住互斥量,它是利用線程間共享的全局變量進行同步的一種機制。

我們使用pthread_cond_wait進行等待條件變量變為真,如果在規定的時間不能滿足,就會生成一個傳回錯誤碼的變量。

int pthread_cond_wait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex);
           

把鎖傳遞給wait函數,函數自動把等待條件的線程挂起,放入消費者等待隊列,然後解鎖挂起線程所占有的互斥鎖,這個時候就可以去跑其他線程,然後當等待條件滿足的時候,這個時候從等待隊列中出來執行,獲得剛才自己所占有的鎖。

滿足條件的時候可以使用函數pthread_cond_signal進行喚醒。

int pthread_cond_broadcast(pthread_cond_t *cond);
       int pthread_cond_signal(pthread_cond_t *cond);
           

這兩個函數都是用來進行喚醒線程操作的,signal一次從消費者隊列中至少喚醒一個線程,broad_cast能喚醒等待該條件的所有線程。

當然和mutex類似,條件變量也需要清除。

int pthread_cond_destroy(pthread_cond_t *cond);
           

生産者消費者示例:

void * producer_run(void *arg)
{
    node_p list=(node_p)arg;    
    while()
    {
        sleep();
        pthread_mutex_lock(&mylock);
        int data=rand()%;
        push_head(list,data);
        pthread_cond_signal(&mycond);
        pthread_mutex_unlock(&mylock);
        printf("producer: data:%d\n",data);
    }

}

void * consumer_run(void *arg)
{   
    node_p list=(node_p)arg;    
    while()
    {
        pthread_mutex_lock(&mylock);
        while(list->next==NULL)
        {
            pthread_cond_wait(&mycond,&mylock);
        }
        int data=;
        pthread_mutex_unlock(&mylock);
        pop_front(list,&data);
        printf("consumer :data:%d\n",data);
    }

}

int main()
{
    node_p list=NULL;
    init_list(&list);
    printf("init_list cuccessi\n");
    pthread_t proid;
    pthread_t conid;

    pthread_create(&proid,NULL,producer_run,(void *)list);
    pthread_create(&conid,NULL,consumer_run,(void *)list);

    pthread_join(proid,NULL);
    pthread_join(conid,NULL);

    destory_mutex_destroy(&mylock);
    destory_cond_destroy(&mycond);

    destory_list(list);

    return ;
}
           
多線程程式設計(1)線程概念線程控制分離線程和結合線程線程同步和互斥死鎖條件變量

結果我們發現生産者每次push一個節點,消費者每次去pop一個節點,上述代碼連結清單的邏輯我就不給了,大家可以去我的github下載下傳相關的代碼。

https://github.com/wsy081414/linux_practice

未完。。。待續

繼續閱讀