LInux下C++多線程編程總結

C++多線程與多進程編程

  1. Linux下的多線程庫:NGPT和NPTL,NPTL比linuxThread效率更高,且符合POSIX編程規範,因此通常都是用POSIX下的線程庫:pthread標準

    NPTL的實現包括三個內容,創建線程和結束線程;讀取和設置線程屬性;POSIX線程同步的方式:POSIX 信號量,互斥條件和條件變量。

  2. 線程模型

    線程是程序中完成一個獨立任務的完整執行序列,即一個可調度的實體。根據調度者的身份,線程可以分爲用戶線程和內核線程兩種

    • 內核線程:運行在內核空間,由內核來調度

    • 用戶線程:運行在用戶空間,由線程庫來調度。

    當進程的一個內核線程獲得CPU的使用權時,他就加載並運行了一個用戶線程,因此,內核線程相當於用戶線程的運行“容器”,一個進程可以擁有M個內核線程和N個用戶線程,M<=N。

    根據這個比值,有三種情況:完全在用戶空間實現多線程;完全在內核空間實現多線程;雙層調度。

    完全在用戶空間實現的線程無需內核的支持,內核甚至不知道這些線程的存在。線程庫負責管理所有的線程,使用longjmp來切換線程的執行,使他們看起來像是“併發”執行的,但實際上內核仍然是把整個進程作爲最小單位來調度的,所以內核線程就是進程本身。

    完全在內核空間實現的線程將創建,調度的任務都交給了內核,線程庫無需執行管理任務。

  3. Linux下的線程庫最有名的就是LInuxThreads和NPTL,現在默認是NPTL。LinuxThreads線程庫的內核線程是使用clone系統調用創建進程模擬的,clone系統調用和fork系統調用過程類似,不過可以指定創建的子進程與調用進程共享相同的虛擬地址空間。用進程來模擬線程有許多缺點:

    1. 每個線程擁有不同的PID

    2. LInux信號處理函數是基於進程的,現在一個進程內所有線程都能且必須處理信號。

    3. 系統的最大線程數就是最大進程數

    LinuxThreads還提供管理線程,負責接收系統發送的信號,終止線程,阻塞線程和回收線程堆棧等工作,效率低下。

    NPTL庫在linux內核的完善下應運而生,Linux內核提供了真正的內核線程,NPTL有如下優點:

    1. 內核線程不再是一個進程,進程的線程可以運行在不同的CPU上,由內核調度。

    2. 不存在管理線程,回收終止等工作都由內核完成

    3. 線程的同步由內核完成,隸屬於不同進程的線程之間也能共享互斥鎖,實現跨進程的同步。

  4. 線程創建

    #include <pthread.h>
    int pthread_create(pthread_t *thread,const pthread_attr_t *attr,vid *(*start_routine)(void* ),void *arg);

    pthread_t爲一個無符號長整型,爲線程的編號;arg參數用於設置線程的屬性,NULL爲默認。

    該函數成功時返回0,失敗返回錯誤碼。

  5. 線程調度與停止

    線程一旦創建好,內核就可以調度內核線程來執行start_routine函數指向的函數了。線程退出最好使用pthread_exit,確保安全,乾淨的退出。

    #include <pthread.h>
    void pthread_exit(void *retval);//retval向回收者傳遞退出信息。
  6. 線程的回收

    一個進程中的所有線程都可以通過調用pthread_join函數來回收其他線程,即等待其他線程結束,這類似於回收進程的wait和waitpid系統調用。

    #include <pthread.h>
    int pthread_join(pthread_t thread,void *retval);

    thread參數是目標的標識符,成功返回0,失敗返回錯誤碼:

    EDEADLK:可能引起死鎖,比如兩個線程互相對對方調用pthread_join,或者線程對自身調用pthread_join
    EINVAL:目標線程是不可回收的,或者已經有其他線程在回收該線程
    ESRCH:目標線程不存在
  7. 線程的終止與取消

    一個線程可能出現異常,我們希望手動終止他,可以通過如下函數:

    #include <pthread.h>
    int pthread_cancel(pthread_t thread);

    成功返回0,失敗返回錯誤碼。

    不過,接受到取消請求的目標線程可以決定是否允許取消以及如何取消,

    #include <pthread.h>
    int pthread_setcancelstate(int state,int *oldstate);
    int pthread_setcanceltype(int type,int *oldtype);

    這兩個函數的第一參數分別設置線程的取消狀態和取消類型;第二個參數則分別記錄線程原來的取消狀態和取消類型。更多細節請看API

  8. 線程的屬性

    #include <pthread.h>
    #define __SIZEOF_PTHREAD_ATTR_T 36
    typedef union{
        char __size[__SIZEOF_PTHREAD_ATTR_T];
        long int __align;
    }pthread_attr_t

    各種線程屬性全部包含在這個字符數組中,可以通過一系列API來操作pthread_attr_t,以便獲取和設置線程的屬性:

    1.pthread_attr_init 功能: 對線程屬性變量的初始化。

    2.pthread_attr_setscope 功能: 設置線程 __scope 屬性...

    3.pthread_attr_setdetachstate 功能: 設置線程detach...

    4.pthread_attr_setschedparam 功能: 設置線程sched...

    5.pthread_attr_getschedparam 功能: 得到線程優先級。

    線程的屬性如下:

    ​
     typedef struct
    {
        int detachstate; //線程的分離狀態
        int schedpolicy; //線程的調度策略
        struct sched schedparam;//線程的調度參數
        int inheritsched; //線程的繼承性
        int scope; //線程的作用域
        size_t guardsize; //線程棧末尾的警戒緩衝區大小
        int stackaddr_set; //線程棧的設置
        void* stackaddr; //線程棧的啓始位置
        size_t stacksize; //線程棧大小
    }pthread_attr_t;

    都有相應的函數進行設置。

  9. POSIX信號量:處理多線程的同步問題

    3種處理先吃之間同步問題的機制:POSIX信號量,互斥量和條件變量

    LInux中,信號量有兩種,第一種是進程間通信使用的 System V IPC,另一種就是POSIX信號量:

    #include <semaphore.h>
    //初始化信號量
    int sem_init(sem_t *sem, int pshared,unsigned value);
    //銷燬信號量
    int sem_destroy(sem_t *sem);
    //以原子操作的方式將信號量的值+1
    int sem_post(sem_t *sem);
    //以原子操作的方式將信號量的值-1
    int sem_wait(sem_t *sem);
    //與sem_wait相同,不過始終立即返回,如果未成功返回-1並將errno置爲EAGAIN
    int sem_trywait(sem_t *sem);
    //關閉命名信號量
    int sem_close(sem_t *sem);
    //命名一個信號量
    sem_t *sem_open(const char *name, int flag);
    
    

    信號量的使用:(重點)

    1.建立兩個線程,兩個線程分別將自己的整形數i從1遞增到100,並規定兩個線程的整形不能超過5

    #include <bits/stdc++.h>
    #include <pthread.h>
    #include <semaphore.h>
    #include <unistd.h>//提供對POSIX操作系統API的訪問權限的頭文件
    #include <windows.h>
    
    //using namespace std;
    const int INF=100;
    sem_t sem_1,sem_2;//兩個信號量
    using std::cout;
    using std::endl;
    /*
        思路:因爲最大差值不能超過5,先初始化兩個信號量爲5,如果一個數+1,在wait自己sem的
        同時post另一個sem,表示你可以多加一點,因爲差值可以從[-5,5],所以必須post另一個sem才滿足條件
    */
    void* run1(void *arg){
        int i;//變量
        for(i=0;i<=INF;i++){
            sem_wait(&sem_1);//原子操作-1
            usleep(500*1000);
            cout<<"the thread 1 print "<<i<<endl;
            sem_post(&sem_2);//原子操作+1
        }
        pthread_exit(0);
    }
    void* run2(void *arg){
        int i;
        for(i=0;i<=INF;i++){
            sem_wait(&sem_2);
            usleep(500*1000);
            cout<<"the thread 2 print "<<i<<endl;
            sem_post(&sem_1);
        }
        pthread_exit(0);
    }
    
    signed main(){
        sem_init(&sem_1,0,5);
        sem_init(&sem_2,0,5);
        pthread_t tid1=5000,tid2=10000;
        pthread_create(&tid1,NULL,run1,NULL);
        pthread_create(&tid2,NULL,run2,NULL);
        pthread_join(tid1,NULL);
        pthread_join(tid2,NULL);
        sem_destroy(&sem_1);
        sem_destroy(&sem_2);
        return 0;
    }
    
    

    2.經典線程問題:生產者消費者問題

    #include <bits/stdc++.h>
    #include <unistd.h>
    #include <pthread.h>
    #include <windows.h>
    #include <semaphore.h>
    
    const int maxn=101;
    struct Stack{
        int a[maxn];
        int top;
        void init(){
            memset(a,0,sizeof(a));
            top=0;
        }
        void push(int num){
            a[top++]=num;
        }
        int pop(){
            return a[top--];
        }
    }Q;
    
    sem_t Full,Empty;
    void* push(void *arg){
        while(true){
            sem_wait(&Full);
            int num=rand()%100000;
            printf("producter has produce an integer %d,now the top is %d\n",num,Q.top);
            Q.push(num);
            usleep(500*1000);
            sem_post(&Empty);
        }
        pthread_exit(0);
    }
    void *pop(void *arg){
        while(true){
            sem_wait(&Empty);
            printf("comsumer has comsume an integer %d,now the top is %d\n",Q.pop(),Q.top);
            usleep(500*1000);
            sem_post(&Full);
        }
        pthread_exit(0);
    }
    signed main(){
        Q.init();
        sem_init(&Full,0,100);
        sem_init(&Empty,0,0);
        pthread_t tid1,tid2;
        pthread_create(&tid1,NULL,push,NULL);
        pthread_create(&tid2,NULL,pop,NULL);
        pthread_join(tid1,NULL);
        pthread_join(tid2,NULL);
        sem_destroy(&Full);
        sem_destroy(&Empty);
        return 0;
    }
    
    
  10. 互斥鎖

    互斥鎖可以用於保護關鍵代碼段,這有點向二進制的POSIX信號量

    互斥鎖具有以下特點:

    `·原子性:把一個互斥鎖定義爲一個原子操作,這意味着操作系統保證瞭如果一個線程鎖定了互斥鎖,則沒有其他線程可以在同一時間成功鎖定這個互斥量。`

    `·唯一性:如果一個線程鎖定一個互斥量,在它接觸鎖定之前,沒有其他線程可以鎖定這個互斥量。`

    `·非繁忙等待:如果一個線程已經鎖定了一個互斥鎖,第二個線程又試圖去鎖定這個互斥鎖,則第二個線程將被掛起(不佔用CPU資源),直到第一個線程解鎖,第二個線程則被喚醒並繼續執行,同時鎖定這個互斥量`

    互斥鎖的使用:

    1. 在訪問共享資源後臨界區域前,對互斥鎖進行加鎖;

    2. 在訪問完成後釋放互斥鎖導上的鎖。在訪問完成後釋放互斥鎖導上的鎖;

    3. 對互斥鎖進行加鎖後,任何其他試圖再次對互斥鎖加鎖的線程將會被阻塞,直到鎖被釋放。對互斥鎖進行加鎖後,任何其他試圖再次對互斥鎖加鎖的線程將會被阻塞,直到鎖被釋放。

 

```c++
// 初始化一個互斥鎖。
int pthread_mutex_init(pthread_mutex_t *mutex, 
						const pthread_mutexattr_t *attr);

// 對互斥鎖上鎖,若互斥鎖已經上鎖,則調用者一直阻塞,
// 直到互斥鎖解鎖後再上鎖。
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 調用該函數時,若互斥鎖未加鎖,則上鎖,返回 0;
// 若互斥鎖已加鎖,則函數直接返回失敗,即 EBUSY。
int pthread_mutex_trylock(pthread_mutex_t *mutex);

// 當線程試圖獲取一個已加鎖的互斥量時,pthread_mutex_timedlock 互斥量
// 原語允許綁定線程阻塞時間。即非阻塞加鎖互斥量。
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);

// 對指定的互斥鎖解鎖。
int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 銷燬指定的一個互斥鎖。互斥鎖在使用完畢後,
// 必須要對互斥鎖進行銷燬,以釋放資源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
```

還有一個常用的方法就是:將互斥鎖封裝成對象,構成自動鎖,這樣生命週期結束的時候鎖自動銷燬

```c++
class CAutoMutex
{
public:
    CAutoMutex()
    {
        mutex = PTHREAD_MUTEX_INITIALIZER;
        pthread_mutex_lock(&mutex);
    }
    ~CAutoMutex()
    {
        pthread_mutex_unlock(&mutex);
    }
private:
    pthread_mutex_t mutex;
};
```

死鎖產生:對已經加鎖的普通鎖再次加鎖,會構成死鎖;兩個對象按照不同的順序申請互斥鎖

```
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <windows.h>
#include <bits/stdc++.h>

pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;
int a,b;
const int INF=100;
void* run(void *arg){
    //鎖b
    int x=0;
    while(x<=INF){
        pthread_mutex_lock(&mutex_b);
        printf("the chile thread got mutex b,waiting fo mutex a\n");
        usleep(500*1000);
        b++;
        pthread_mutex_lock(&mutex_a);
        a+=b++;
        pthread_mutex_unlock(&mutex_a);
        pthread_mutex_unlock(&mutex_b);
    }
    pthread_exit(NULL);
}
signed main(){
    pthread_mutex_init(&mutex_a,NULL);
    pthread_mutex_init(&mutex_b,NULL);
    pthread_t tid;
    pthread_create(&tid,NULL,run,NULL);
    //向a鎖加鎖
    int x=0;
    while(x<=INF){
    pthread_mutex_lock(&mutex_a);
    printf("the father thread got mutex a,waiting for mutext b\n");
    a++;
    pthread_mutex_lock(&mutex_b);
    b+=a++;
    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);
    }
    pthread_join(tid,NULL);
    pthread_mutex_destroy(&mutex_a);
    pthread_mutex_destroy(&mutex_b);
    return 0;
}

```
  1. 條件變量

    與互斥鎖不同,條件變量是用來等待而不是用來上鎖的。條件變量用來自動阻塞一個線程,直 到某特殊情況發生爲止。通常條件變量和互斥鎖同時使用

    條件變量使我們可以睡眠等待某種條件出現。條件變量是利用線程間共享的全局變量進行同步 的一種機制,主要包括兩個動作:

    • 一個線程等待"條件變量的條件成立"而掛起;

    • 另一個線程使 “條件成立”(給出條件成立信號)。

    更加清晰的說法就是,條件變量提供了一種線程間的通知機制:當某個共享數據達到某個值的 時候,喚醒等到這個共享數據的線程。

    #include <pthread.h>
    // 初始化條件變量
    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); 
    
  2.  

     

     

     

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