多線程編程(1)

線程概念


線程線程就是進程的若干個執行流,

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

進程

多線程

在系統內核中其實是不存在線程的,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=0;

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

    }
    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 0;
}

![enter description here][3]
上述程序就會出現問題,就是兩個線程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=0;

void * pthread_run(void *arg)
{
    int val=0;
    int i=0;
    while(i<5000)
    {
        //在臨界區加上互斥鎖,這樣就可解決線程訪問衝突的問題了
        pthread_mutex_lock(&mutex);
        i++;
        val=count;
        printf("pthread: %lu,count:%d\n",pthread_self(),count);
        count =val+1;
        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 0;
}

這樣最終count的結果就是100

這裏寫圖片描述

死鎖


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

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

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

死鎖產生的必要條件:

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

解決死鎖的方法:

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

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

死鎖預防:
1. 破環互斥條件:允許系統資源共享,系統不會進入死鎖。
2. 破壞不可剝奪、不可搶佔條件:如果佔有某些資源的一個進程進行進一步資源請求被拒絕,則該進程必須釋放它最初佔有的資源,如果有必要,可再次請求這些資源和另外的資源。
3. 破壞請求和保持條件:採用靜態分配方法,進程在運行前一次申請完它所需要的全部資源,在它的資源未滿足前,不投入運行。一進入運行,也就不再提出其他的資源請求。
4. 破環環路等待條件,採用資源有序分配法,破環環路條件。

死鎖避免:
如果一個進程請求會導致死鎖,則不啓動它。
如果一個進程增加的資源請求會導致死鎖,則不允許此分配。

解決死鎖的基本方法:

預防死鎖:

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

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

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

避免死鎖:

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

檢測死鎖

首先爲每個進程和每個資源指定一個唯一的號碼;

然後建立資源分配表和進程等待表,例如:

解除死鎖:

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

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

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

條件變量


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

![enter description here][5]
生產者消費者,是在多線程同步的一個問題,兩個固定大小緩衝區的線程,在實際運行是會發生問題,生產者是生成數據放入緩衝區,重複過程,消費者在緩衝區取走數據。

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

三種關係:
- 生產者之間的競爭關係
- 消費者之間的競爭關係
- 生產者和消費者之間的關係

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

一個場所:有效的內存區域。

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

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

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


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(1)
    {
        sleep(1);
        pthread_mutex_lock(&mylock);
        int data=rand()%1000;
        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(1)
    {
        pthread_mutex_lock(&mylock);
        while(list->next==NULL)
        {
            pthread_cond_wait(&mycond,&mylock);
        }
        int data=0;
        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 0;
}

這裏寫圖片描述
結果我們發現生產者每次push一個節點,消費者每次去pop一個節點,上述代碼鏈表的邏輯我就不給了,大家可以去我的github下載相關的代碼。
https://github.com/wsy081414/linux_practice

未完。。。待續

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