1. 程式人生 > 其它 >Linux 系統程式設計 學習:11-執行緒:執行緒同步

Linux 系統程式設計 學習:11-執行緒:執行緒同步

情景匯入

我們都知道引入執行緒在合理的範圍內可以加快提高程式的效率。但我們先來看看如果多執行緒同時訪問一個臨界資源會怎麼樣。

例程:模擬多視窗售票

c
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>

int ticket_sum = 20;

static int no = 0; // 為了登記是第幾條執行緒在工作
void *sell_ticket(void *arg){
    no++;
    int
number = no; for(int i=0; i<20; i++) { if(ticket_sum>0) { sleep(1); printf("No.%d sell the %d th\n", number, 20 - ticket_sum + 1); ticket_sum--; }else{ break; } } return 0; } intmain(){ int flag, i; pthread_t
tids[4]; for(i = 0; i < 4; i++) { flag = pthread_create(&tids[i],NULL, &sell_ticket, NULL); if(flag) { printf("Create failed"); return flag; } } sleep(2); void *ans; for(int i=0; i<4; i++) { flag=pthread_join(tids[i],&ans); if
(flag) { return flag; } } return 0; }

由於沒有引入同步機制,執行結果並不合理。

同步的有關概念

臨界資源:每次只允許一個執行緒進行訪問的資源
執行緒間互斥:多個執行緒在同一時刻都需要訪問臨界資源
同步:在特殊情況下,控制多執行緒間的相對執行順序

多執行緒程式設計的本質有三個方面:

  • 併發性是多執行緒的本質
  • 在巨集觀上,所有執行緒並行執行
  • 執行緒間相互獨立,互不干涉

在多工作業系統中,同時執行的多個任務可能:

  • 都需要訪問/使用同一種資源
  • 多個任務之間有依賴關係,某個任務的執行依賴於另一個任務。

有關同步機制

在多執行緒環境中,當我們需要保持執行緒同步時,通常通過 鎖 來實現。執行緒同步的常見方法:互斥鎖、條件變數、讀寫鎖、自旋鎖、訊號量、執行緒柵欄。

鎖 是大家都應該遵守的“君子”條約,因為如果有一個執行緒沒有遵守,那麼就沒有意義了:

  • 對共享資源操作前一定要獲得鎖。
  • 完成操作以後一定要釋放鎖。(多個鎖時, 若獲得順序是ABC連環扣, 釋放順序也應該是ABC。)
  • 儘量短時間地佔用鎖。
  • 執行緒錯誤返回時應該釋放它所獲得的鎖。

互斥鎖

在多工作業系統中,同時執行的多個任務可能都需要使用同一種資源。

這個過程有點類似於,公司部門裡,我在使用著印表機列印東西的同時(還沒有列印完),別人剛好也在此刻使用印表機列印東西,如果不做任何處理的話,打印出來的東西肯定是錯亂的。
線上程裡也有這麼一把鎖——互斥鎖(mutex),互斥鎖是一種簡單的加鎖的方法來控制對共享資源的訪問,互斥鎖只有兩種狀態,即上鎖( lock )和解鎖( unlock )。

互斥鎖的特點:

  • 原子性:把一個互斥鎖鎖定為一個原子操作,這意味著作業系統(或pthread函式庫)保證瞭如果一個執行緒鎖定了一個互斥鎖,沒有其他執行緒在同一時間可以成功鎖定這個互斥鎖
  • 唯一性:如果一個執行緒鎖定了一個互斥鎖,在它解除鎖定之前,沒有其他執行緒可以鎖定這個互斥鎖
  • 非繁忙等待:如果一個執行緒已經鎖定了一個互斥鎖,第二個執行緒又試圖去鎖定這個互斥鎖,則第二個執行緒將被掛起(不佔用任何cpu資源),直到第一個執行緒解除對這個互斥鎖的鎖定為止,第二個執行緒則被喚醒並繼續執行,同時鎖定這個互斥鎖。

應用互斥鎖需要注意的幾點

1、互斥鎖需要時間來加鎖和解鎖。鎖住較少互斥鎖的程式通常執行得更快。所以,互斥鎖應該儘量少,夠用即可,每個互斥鎖保護的區域應則儘量大。

2、互斥鎖的本質是序列執行。如果很多執行緒需要領繁地加鎖同一個互斥鎖,則執行緒的大部分時間就會在等待,這對效能是有害的。如果互斥鎖保護的資料(或程式碼)包含彼此無關的片段,則可以特大的互斥鎖分解為幾個小的互斥鎖來提高效能。這樣,任意時刻需要小互斥鎖的執行緒減少,執行緒等待時間就會減少。所以,互斥鎖應該足夠多(到有意義的地步),每個互斥鎖保護的區域則應儘量的少。

互斥鎖初始化與銷燬

互斥鎖的初始化有動態初始化與靜態初始化2種方式。

動態初始化、銷燬

c
intpthread_mutex_init(pthread_mutex_t *restrict mutex, constpthread_mutexattr_t *restrict attr);

intpthread_mutex_destroy(pthread_mutex_t *mutex);

描述:動態互斥鎖初始化函式(不再使用時應該呼叫pthread_mutex_destroy進行銷燬)

引數解析:

mutex:互斥鎖物件的地址
attr:互斥鎖屬性物件的地址,當為NULL,代表預設互斥鎖初始化。

靜態初始化(不需要銷燬)

僅侷限於靜態初始化的時候使用:將“宣告”、“定義”、“初始化”一氣呵成,除此之外的情況都只能使用pthread_mutex_init函式。

c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

初始化例程

c
#if 0
	// 靜態互斥鎖初始化。 (不需要pthread_mutex_destroy)
	pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;	
#else
	// 動態互斥鎖初始化。
	pthread_mutex_t mutex1;
	pthread_mutex_init(&mutex1, NULL);
	...
	pthread_mutex_destroy(&mutex1);
#endif

上鎖與解鎖

c
intpthread_mutex_lock(pthread_mutex_t *mutex);
intpthread_mutex_trylock(pthread_mutex_t *mutex);
intpthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
				   const struct timespec *restrict abstime);

intpthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_lock

上鎖,如果上鎖前已經鎖上,則掛起等待。

pthread_mutex_trylock

嘗試上鎖,如果上鎖前已經鎖上時,返回EBUSY而不是掛起等待。

pthread_mutex_timedlock

pthread_mutex_lock是基本等價的,但是在達到超時時間值時,pthread_mutex_timedlock不會對互斥鎖進行加鎖,而是返回錯誤碼ETIMEDOUT

pthread_mutex_unlock

使用結束後在程序退出前,解鎖。這樣,別的程序就可以獲取到鎖。

如果解鎖一個未加鎖的mutex互斥鎖,行為是未知的。

互斥鎖有關例程

c
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>

pthread_mutex_t mutex;

int count = 100;

void *routine(void *arg){
    printf("start thread\n");

    pthread_mutex_lock(&mutex);

    printf("child in mutex lock\n");

    if(count >= 100)
    {
        printf("in child\n");
        sleep(3);
        count -= 100;
    }

    pthread_mutex_unlock(&mutex);

    printf("child count = %d\n", count);

    return NULL;
}

intmain(void){
    pthread_t tid;
    pthread_mutex_init(&mutex, NULL);

    errno = pthread_create(&tid, NULL, routine, NULL);
    if(errno != 0)
    {
        perror("create thread failed\n");
        return -1;
    }

    pthread_mutex_lock(&mutex);

    if(count >= 100)
    {
        printf("in parent\n");
        sleep(3);
        count -= 100;
    }

    pthread_mutex_unlock(&mutex);

    printf("parent count = %d\n", count);

    pthread_join(tid, NULL);

    pthread_mutex_destroy(&mutex);

    return 0;
}

條件變數

操作互斥鎖時就會一直在迴圈判斷,而每次判斷都要加鎖、解鎖(即使本次並沒有修改臨界資源)。這就帶來了問題:

1)CPU浪費嚴重。
2)響應處理可能會導致不夠及時。

條件變數是一種同步機制,允許執行緒掛起,直到共享資料上的某些條件得到滿足。

條件變數函式不是非同步訊號安全的,不應當在訊號處理程式中進行呼叫。

特別要注意,如果在訊號處理程式中呼叫pthread_cond_signalpthread_cond_boardcast函式,可能導致呼叫執行緒死鎖。

條件變數一般配合互斥鎖進行使用。

條件變數的使用流程:

  • 執行緒A得到了互斥鎖,出於某個原因開始等待條件T。
  • 等待時,系統先解開執行緒A得到的鎖,然後將它掛起。
  • 如果還有執行緒B和A一樣,那麼同樣也進入等待(同樣地解鎖,同樣地掛起)
  • 此時,獨立於執行緒AB以外的另外一個執行緒C,重置了條件T,C可以選擇喚醒處於等待的AB:要麼C 廣播,引發驚群,讓AB來搶;要麼C 單獨通知,告訴最先等待的執行緒(在這裡是A)。
  • 假設這裡C選擇單獨通知,那麼A就會被喚醒,看到條件滿足了以後就重新加鎖。這樣一來,就省去了對於資源讀取時不用去輪訓等待了。

什麼是驚群:

舉一個很簡單的例子,當你往一群鴿子中間扔一塊食物,雖然最終只有一個鴿子搶到食物,但所有鴿子都會被驚動來爭奪,沒有搶到食物的鴿子只好回去繼續睡覺, 等待下一塊食物到來。每扔一塊食物,都會驚動所有的鴿子,即為驚群。

對於作業系統來說,多個程序/執行緒在等待同一資源是,也會產生類似的效果,其結果就是每當資源可用,所有的程序/執行緒都來競爭資源,造成的後果:
1)系統對使用者程序/執行緒頻繁的做無效的排程、上下文切換,系統系能大打折扣。
2)為了確保只有一個執行緒得到資源,使用者必須對資源操作進行加鎖保護,進一步加大了系統開銷。

條件變數的初始化與銷燬

條件變數的初始化也有動態初始化與靜態初始化2種方式。這次我們放在一起講:

c
// 動態初始化一個條件變數 
intpthread_cond_init(pthread_cond_t *restrict cond, constpthread_condattr_t *restrict attr);
	attr :預設NULL(LinuxThreads 實現條件變數不支援屬性,因此 cond_attr 引數實際被忽略。)
// 銷燬一個動態初始化的條件變數(在不使用條件變數時使用)
intpthread_cond_destroy(pthread_cond_t *cond);
	
// 靜態初始化一個條件變數
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

進入等待

c
intpthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

intpthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

描述:掛起執行緒直到其他執行緒觸發條件後才被喚醒。函式執行時先自動釋放指定的鎖(允許其他執行緒訪問),然後等待條件變數的變化。在條件滿足從而離開pthread_cond_wait()之前,mutex將被重新加鎖,以與進入pthread_cond_wait()前的加鎖動作對應。

無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個執行緒同時請求等待條件pthread_cond_wait()(或pthread_cond_timedwait())的競爭條件(Race Condition)。

條件變數的使用前提:

mutex互斥鎖必須是普通鎖(PTHREAD_MUTEX_TIMED_NP)或者適應鎖 (PTHREAD_MUTEX_ADAPTIVE_NP),且在呼叫pthread_cond_wait()前必須由本執行緒加鎖 (pthread_mutex_lock()),而在更新條件等待佇列以前,mutex保持鎖定狀態,並在執行緒掛起進入等待前解鎖。

喚醒一個等待者

c
intpthread_cond_signal(pthread_cond_t * cond);

描述:通過條件變數cond傳送訊息,若多個執行緒在等待,它只喚醒最先等待的那一個。

注: 呼叫 pthread_cond_signal 後要立刻釋放互斥鎖

c
intpthread_cond_broadcast(pthread_cond_t *cond);
intpthread_cond_signal(pthread_cond_t *cond);
// 重啟動等待該條件變數的所有執行緒。如果沒有等待的執行緒,則什麼也不做。(引發 驚群 效應)

喚醒所有等待者

c
intpthread_cond_broadcast(pthread_cond_t *cond);

描述:喚醒等待該條件變數的所有執行緒。如果沒有等待的執行緒,則什麼也不做。(引發 驚群 效應)

例程:生產者消費者模型

在經典的生產者-消費者場合中,生產者首先必須檢查緩衝是否已滿(numUsedBytes==BufferSize),如果緩衝區已滿,執行緒停下來等待 bufferNotFull條件。如果沒有滿,在緩衝中生產資料,增加numUsedBytes,啟用條件 bufferNotEmpty。使用mutex來保護對numUsedBytes的訪問。

pthread_cond_wait接收一個mutex作為引數,mutex被呼叫執行緒初始化為鎖定狀態。線上程進入休眠狀態之前,mutex會被解鎖。而當執行緒被喚醒時,mutex會處於鎖定狀態;從鎖定狀態到等待狀態的轉換是原子操作。當程式開始執行時,只有生產者可以工作,消費者被阻塞等待bufferNotEmpty條件,一旦生產者在緩衝中放入一個位元組,bufferNotEmpty條件被激發,消費者執行緒於是被喚醒。

c
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>

pthread_mutex_t mutex;
pthread_cond_t buffer_is_not_empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t buffer_is_not_full  = PTHREAD_COND_INITIALIZER;

const int DataSize = 10;
const int BufferSize = 1;
static int usedSpace = 0;

void *producer(void *arg){
    printf("producer thread\n");

    for (int i = 0; i < DataSize; ++i)
    {
        pthread_mutex_lock(&mutex);
        while (usedSpace == BufferSize)
        {
            pthread_cond_wait(&buffer_is_not_full, &mutex);
        }
        ++usedSpace;
        printf("P, %d\n", usedSpace);

        pthread_cond_broadcast(&buffer_is_not_empty);
        pthread_mutex_unlock(&mutex);
    }
    return "OK.";
}

void * consumer(void *arg){
    printf("consumer thread\n");
    for (int i = 0; i < DataSize; ++i)
    {
        pthread_mutex_lock(&mutex);
        while (usedSpace == 0)
        {
            pthread_cond_wait(&buffer_is_not_empty, &mutex);
        }
        --usedSpace;
        printf("C, %d\n", usedSpace);
        pthread_cond_broadcast(&buffer_is_not_full);
        pthread_mutex_unlock(&mutex);
    }
    printf("\n");
    return "OK.";
}

intmain(void){
    pthread_t tid_producer;
    pthread_t tid_consumer;
    errno = pthread_create(&tid_producer, NULL, producer, NULL);
    if(errno != 0)
    {
        perror("create thread for producer failed\n");
        return -1;
    }
    errno = pthread_create(&tid_consumer, NULL, consumer, NULL);
    if(errno != 0)
    {
        perror("create thread for consumer failed\n");
        return -1;
    }

    sleep(1);

    pthread_join(tid_consumer, NULL);
    pthread_join(tid_producer, NULL);


    return 0;
}

讀寫鎖

根據把對共享資源的訪問情況,劃分成讀者和寫者:

  • 讀者只對共享資源進行讀訪問。
  • 寫者則需要對共享資源進行寫操作。

讀寫鎖與互斥鎖的功能類似,但比它有更高的並行性:

1)在加上讀鎖後不能寫;加上寫鎖後不能讀寫,否則阻塞

2)允許多個讀者可以同時進行讀

3)寫者互斥(只允許一個寫者寫)

4)寫者優先於讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者)

讀寫鎖的初始化與銷燬

讀寫鎖的初始化也有動態初始化與靜態初始化2種方式。我們放在一起講:

c
// 動態初始化 與 銷燬
intpthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           constpthread_rwlockattr_t *restrict attr);
intpthread_rwlock_destroy(pthread_rwlock_t *rwlock);

// 靜態初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

上讀鎖

c
intpthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 

描述:加讀鎖(允許其他讀鎖訪問,不允許寫鎖訪問)

上寫鎖

c
intpthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

描述:加寫鎖(不允許其他讀 或 寫鎖訪問)

解鎖

c
intpthread_rwlock_unlock(pthread_rwlock_t *rwlock);

描述:解開當前加的鎖

例程:讀寫鎖例程

c
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>

int num=5;
pthread_rwlock_t rwlock;

void *reader(void *arg){
    pthread_rwlock_rdlock(&rwlock);
    printf("Reader %ld got the lock\n", (long)arg );

    pthread_rwlock_unlock(&rwlock);
    return 0;
}

void *writer(void *arg){
    pthread_rwlock_wrlock(&rwlock);
    printf("Writer %ld got the lock\n", (long)arg );
    pthread_rwlock_unlock(&rwlock);
    return 0;
}

intmain(){
    int flag;
    long n = 1, m = 1;
    pthread_t wid[5],rid[5];
    pthread_attr_t attr;

    flag = pthread_rwlock_init(&rwlock,NULL);
    if(flag)
    {
        printf("rwlock init error\n");
        return flag;
    }

    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//thread sepatate

    for(int i=0;i<num;i++)
    {
        if(i%3)
        {
            pthread_create(&rid[n-1],&attr,reader,(void *)n);
            printf("create reader %ld\n", n);
            n++;
        }else
        {
            pthread_create(&wid[m-1],&attr,writer,(void *)m);
            printf("create Writer %ld\n", m);
            m++;
        }
    }

    sleep(5);//wait other done

    return 0;
}

自旋鎖

自旋鎖與互斥鎖的區別

在多處理器環境中,自旋鎖最多隻能被一個可執行執行緒持有。如果一個可執行執行緒試圖獲得一個被爭用(已經被持有的)自旋鎖,那麼該執行緒就會一直進行忙等待,自旋,也就是空轉,等待鎖重新可用。如果鎖未被爭用,請求鎖的執行執行緒便立刻得到它,繼續向下執行。

一個被爭用的自旋鎖使得請求它的執行緒在等待鎖重新可用時自旋,特別的浪費CPU時間,所以自旋鎖不應該被長時間的持有。實際上,這就是自旋鎖的設計初衷,在短時間內進行輕量級加鎖

訊號量和讀寫訊號量適合於保持時間較長的情況,它們會導致呼叫者睡眠,因此只能在程序上下文使用而不能在中斷上下文使用,因為中斷的上下文不允許休眠(trylock可以),因此在中斷上下文只能使用自旋鎖。
自旋鎖保持期間是搶佔失效的(核心不允許被搶佔) ,而訊號量和讀寫訊號量保持期間是可以被搶佔的。

自旋鎖保護的臨界區預設是可以相應中斷的,但是如果在中斷處理程式中請求相同的自旋鎖,那麼會發生死鎖(核心自旋鎖可以關閉中斷)。

自旋鎖 的 初始化與銷燬

c
intpthread_spin_destroy(pthread_spinlock_t *lock);
intpthread_spin_init(pthread_spinlock_t *lock, int pshared);

描述:初始化/銷燬一個自旋鎖物件。

引數解析:

pshared:共享方式

  • PTHREAD_PROCESS_PRIVATE:表示這個自旋鎖是當前程序的區域性自旋鎖
  • PTHREAD_PROCESS_SHARED:這個自旋鎖可以在多個程序之間共享

如果想要使用自旋鎖同步多程序,那麼設定pshared = PTHREAD_PROCESS_SHARED,然後在程序共享記憶體中分配pthread_spinlock_t 物件即可(pthread_mutex_t亦如此,但其需要進行pthread_mutexattr_setpshared())。

自旋鎖操作

c
intpthread_spin_lock(pthread_spinlock_t *lock);
intpthread_spin_trylock(pthread_spinlock_t *lock);
intpthread_spin_unlock(pthread_spinlock_t *lock);

與互斥鎖基本一樣。

POSIX訊號量

POSIX代表 “可移植作業系統介面” Portable Operation System Interface 。只要按照這個API標準寫程式,理論上就可以在各個作業系統和硬體平臺上編譯執行。

POSIX 與System V區別

  • System V IPC存在時間比較老,許多系統都支援,但是介面複雜,並且可能各平臺上實現略有區別(如ftok的實現及限制)
  • posix訊號量只能單個操作,IPC訊號量可以多個訊號量同時操作,IPC訊號量更加強大
  • POSIX是新標準,語法簡單,並且各平臺上實現都一樣

訊號量:一個定義在核心系統中的特殊的變數,有以下定義:
1)P操作:減操作
2)V操作:加操作
3)該變數最小為0,如果等於0的情況下還去進行P操作,就會阻塞

The canonical names V and P come from the initials of Dutch words. V is generally explained as verhogen ("increase"). Several explanations have been offered for P, including proberen ("to test" or "to try"), passeren ("pass"), and pakken ("grab"). Dijkstra's earliest paper on the subject givespassering ("passing") as the meaning for P, and vrijgave ("release") as the meaning for V. It also mentions that the terminology is taken from that used in railroad signals. Dijkstra subsequently wrote that he intended P to stand for the portmanteauprolaag, short for probeer te verlagen, literally "try to reduce," or to parallel the terms used in the other case, "try to decrease."

訊號量(sem)和互斥鎖的區別:

互斥鎖只允許一個執行緒進入臨界區,而訊號量允許多個執行緒進入臨界區

使用時,需要以下標頭檔案:

c
#include<semaphore.h>

POSIX訊號量有兩種:有名訊號量(可以在程序中使用)和無名訊號量。有名訊號量與無名訊號量使用同一套函式(但是在初始化和銷燬的函式有所不同)

我們這裡介紹無名訊號量。

c
初始化並開啟有名訊號量:sem_open();       初始化無名訊號量:sem_init()

操作訊號量:sem_wait()/sem_trywait()/sem_timedwait()/sem_post()/sem_getvalue()

    
關閉有名訊號量:sem_close(); //關閉有名訊號量
銷燬有名訊號量:sem_unlink(); //試圖銷燬訊號量,一旦所有佔用該訊號量的程序都關閉了該訊號量,那麼就會銷燬這個訊號量

銷燬無名訊號量:sem_destroy()

初始化與銷燬訊號量

c
intsem_init(sem_t *sem, int pshared, unsignedint value);
intsem_destory(sem_t *sem);

描述:初始化/銷燬一個訊號量物件

引數解析:

sem:訊號量物件

pshared:共享方式

  • PTHREAD_PROCESS_PRIVATE:表示這個訊號量是當前程序的區域性訊號量
  • PTHREAD_PROCESS_SHARED:這個訊號量可以在多個程序之間共享

value:初始值

返回值:成功符合0,失敗返回-1。

訊號量操作

P操作

c
intsem_wait(sem_t *sem);
intsem_trywait(sem_t *sem); //試圖佔用訊號量,如果訊號量已經為0,立即報錯(errno 設定為 EAGAIN))
intsem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

描述:以原子操作的方式將訊號量的值減去1。

返回值:成功返回0;失敗返回-1,設定errno。

V操作

c
intsem_post(sem_t *sem);

描述::以原子操作的方式將訊號量的值加上1。

返回值:成功返回0;失敗返回-1,設定errno。

取值

c
intsem_getvalue(sem_t *sem, int *sval);

描述:獲得訊號量當前的值,放到sval中。

引數解析:

sval:存放結果的容器。

注意:如果有執行緒正在block這個訊號量,sval可能返回兩個值其中的一個,0或“-正在block的執行緒的數目”,在Linux中返回0。

If one or more processes or threads are blocked waiting to lock the semaphore withsem_wait(), POSIX.1 permits two possibilities for the value returned in #sval: either 0 is returned; or a negative number whose absolute value is the count of the number of processes and threads currently blocked insem_wait(). Linux adopts the former behavior.

例程:訊號量

通過訊號量模擬2個視窗,10個客人進行服務的過程。

c
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<semaphore.h>


int num=10;
sem_t sem;

void *get_service(void *cid){
    int id=*((int*)cid);
    if(sem_wait(&sem)==0)
    {
        printf("customer %d get the service \n", id);
        sleep(2);
        printf("customer %d done \n", id);
        sem_post(&sem);
    }
    return 0;
}

intmain(){
    sem_init(&sem,0,2);
    pthread_t customer[num];
    int flag;

    for(int i=0;i<num;i++)
    {
        int id=i;
        flag=pthread_create(&customer[i],NULL,get_service,&id);
        if(flag)
        {
            return flag;
        }else
        {
        }
        sleep(1);
    }

    //wait all thread done
    for(int j=0;j<num;j++)
    {
        pthread_join(customer[j],NULL);
    }
    sem_destroy(&sem);
    return 0;
}

執行緒柵欄

把先後到達的多個執行緒擋在同一柵欄前,直到所有執行緒(可指定個數)到齊,然後撤下柵欄同時放行。

使用場景

這種“柵欄”機制最大的特點就是最後一個執行wait的動作最為重要,就像賽跑時的起跑槍一樣,它來之前所有人都必須等著。所以實際使用中,pthread_barrier_wait常常用來讓所有執行緒等待“起跑槍”響起後再一起行動。

比如我們可以用pthread_create()生成100個執行緒,每個子執行緒在被create出的瞬間就會自顧自的立刻進入回撥函式執行。但我們可能不希望它們這樣做,因為這時主程序還沒準備好,和它們一起配合的其它執行緒還沒準備好,我們希望它們在回撥函式中申請完執行緒空間、初始化後停下來,一起等待主程序釋放一個“開始”訊號,然後所有執行緒再開始執行業務邏輯程式碼。

為了解決上述場景問題,我們可以在init時指定n+1個等待,其中n是執行緒數。而在每個執行緒執行函式的首部呼叫wait()。這樣100個pthread_create()結束後所有執行緒都停下來等待最後一個wait()函式被呼叫。這個wait()由主程序在它覺得合適的時候呼叫就好。最後這個wait()就是鳴響的起跑槍。

柵欄的初始化與銷燬

c
intpthread_barrier_init(pthread_barrier_t *restrict barrier, constpthread_barrierattr_t *restrict attr, unsigned count);

intpthread_barrier_destroy(pthread_barrier_t *barrier);

描述:初始化/銷燬一個柵欄物件,初始化時要指定等待者個數。

引數解析:

count:等待者個數

在柵欄前等待

c
intpthread_barrier_wait(pthread_barrier_t *barrier);
到柵欄前等待放行,如果該條函式執行的次數等於 #count 時,放行。

描述:讓一個執行緒在柵欄前告訴大家它已經就緒,等待放行。

例程:柵欄的使用

c
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>

pthread_barrier_t barrier;

void *Task1(void *arg);
void *Task2(void *arg);

intmain(void){
    int policy,inher;
    pthread_t tid;
    pthread_attr_t attr;
    structsched_paramparam;

    //初始化執行緒屬性
    pthread_attr_init(&attr);
    pthread_barrier_init(&barrier,NULL,2 + 1);//2+1個等待(2個執行緒,1個自己)

    //建立執行緒1
    pthread_create(&tid, &attr,Task1,NULL);

    //建立執行緒2
    pthread_create(&tid, &attr,Task2,NULL);
    
    printf("main process will sleep 6s.\n");
    sleep(6);/*等待6s後,才讓執行緒執行*/
    pthread_barrier_wait(&barrier);//起跑槍“砰!”

    pthread_join(tid, NULL);
    pthread_barrier_destroy(&barrier);
}

void *Task1(void *arg){
    printf("Task1 will be blocked.\n");
    pthread_barrier_wait(&barrier);//所有執行緒都被阻塞在這裡
    printf("Task1 is running.\n");
    sleep(3);//延時3s
    pthread_exit(NULL);
}

void *Task2(void *arg){
    printf("Task2 will be blocked.\n");
    pthread_barrier_wait(&barrier);//所有執行緒都被阻塞在這裡
    printf("Task2 is running.\n");
    sleep(3);//延時3s
    pthread_exit(NULL);
}

為同步物件設定屬性

https://blog.csdn.net/bytxl/article/details/8822551

在上文的學習中,為了降低學習的理解難度,我們沒有介紹互斥鎖的屬性。現在我們就來介紹有關內容。

執行緒和執行緒的同步物件(互斥鎖,讀寫鎖,條件變數,柵欄)都具有屬性。在修改屬性前都需要對該結構進行初始化。使用後要把該結構回收。

除了互斥鎖還可以設定鎖的型別以外,所有的同步物件只能設定作用域。

互斥鎖屬性

初始化與銷燬

c
#include<pthread.h>

intpthread_mutexattr_destroy(pthread_mutexattr_t *attr);
intpthread_mutexattr_init(pthread_mutexattr_t *attr);

描述:類似於執行緒屬性(但沒有靜態初始化方法),使用時初始化(設定了預設屬性),使用後銷燬(設定了無效屬性)

返回值:成功返回0,失敗返回錯誤號。

設定互斥鎖的共享方式

c
#include<pthread.h>

intpthread_mutexattr_getpshared(constpthread_mutexattr_t
                                 *restrict attr, int *restrict pshared);
intpthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
                                 int pshared);

描述:設定互斥鎖的作用域。

引數解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(預設,由這個屬性物件建立的互斥鎖只能在程序內使用)

  • PTHREAD_PROCESS_SHARED(允許互斥鎖在程序間中使用)

在程序間的用法:設定以後,將一個互斥鎖放置到共享記憶體中即可。

設定互斥鎖的型別

c
#include<pthread.h>

intpthread_mutexattr_gettype(constpthread_mutexattr_t *restrict attr,
                              int *restrict type);
intpthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

描述:設定互斥鎖的型別。

引數解析:

type: 由於 DEFAULT ( NORMAL) 屬性有太多的未定義行為,應該儘可能避免使用。

  • PTHREAD_MUTEX_DEFAULT(預設)
  • PTHREAD_MUTEX_NORMAL
  • PTHREAD_MUTEX_ERRORCHECK
  • PTHREAD_MUTEX_RECURSIVE
PTHREAD_MUTEX_NORMAL

這種型別的互斥鎖不會自動檢測死鎖。如果一個執行緒試圖對一個互斥鎖重複鎖定,將會引起這個執行緒的死鎖。如果試圖解鎖一個由別的執行緒鎖定的互斥鎖會引發不可預料的結果。如果一個執行緒試圖解鎖已經被解鎖的互斥鎖也會引發不可預料的結果。

PTHREAD_MUTEX_ERRORCHECK

這種型別的互斥鎖會自動檢測死鎖。如果一個執行緒試圖對一個互斥鎖重複鎖定,將會返回一個錯誤程式碼。如果試圖解鎖一個由別的執行緒鎖定的互斥鎖將會返回一個錯誤程式碼。如果一個執行緒試圖解鎖已經被解鎖的互斥鎖也將會返回一個錯誤程式碼。

PTHREAD_MUTEX_RECURSIVE

如果一個執行緒對這種型別的互斥鎖重複上鎖,不會引起死鎖,一個執行緒對這類互斥鎖的多次重複上鎖必須由這個執行緒來重複相同數量的解鎖,這樣才能解開這個互斥鎖,別的執行緒才能得到這個互斥鎖。如果試圖解鎖一個由別的執行緒鎖定的互斥鎖將會返回一個錯誤程式碼。如果一個執行緒試圖解鎖已經被解鎖的互斥鎖也將會返回一個錯誤程式碼。這種型別的互斥鎖只能是程序私有的(作用域屬性為PTHREAD_PROCESS_PRIVATE)。

條件變數屬性

初始化與銷燬

c
intpthread_condattr_init(pthread_condattr_t *cattr);
intpthread_condattr_destroy(pthread_condattr_t *cattr);

類似上文,不再描述。

設定共享方式

c
intpthread_condattr_getpshared(constpthread_condattr_t *restrict attr,
                                int *restrict pshared);
intpthread_condattr_setpshared(pthread_condattr_t *attr,
                                int pshared);

引數解析:既然條件變數是搭配互斥鎖用的,那麼同樣地,就可以設定共享方式。

pshared:

  • PTHREAD_PROCESS_PRIVATE(預設,由這個屬性物件建立的條件變數只能在程序內使用)

  • PTHREAD_PROCESS_SHARED(允許條件變數在程序間中使用)

在程序間的用法:設定以後,將條件變數放置到共享記憶體中即可。

讀寫鎖屬性

初始化與銷燬

c
intpthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
intpthread_rwlockattr_init(pthread_rwlockattr_t *attr);

類似上文,不再描述。

設定共享方式

c
intpthread_rwlockattr_getpshared(constpthread_rwlockattr_t
                                  *restrict attr, int *restrict pshared);
intpthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,
                                  int pshared);

引數解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(預設,由這個屬性物件建立的條件變數只能在程序內使用)

  • PTHREAD_PROCESS_SHARED(允許條件變數在程序間中使用)

在程序間的用法:設定以後,將讀寫鎖放置到共享記憶體中即可。

柵欄屬性

初始化與銷燬

c
intpthread_barrierattr_destroy(pthread_barrierattr_t *attr);
intpthread_barrierattr_init(pthread_barrierattr_t *attr);

類似上文,不再描述。

設定共享方式

c
intpthread_barrierattr_getpshared(constpthread_barrierattr_t
                                   *restrict attr, int *restrict pshared);
intpthread_barrierattr_setpshared(pthread_barrierattr_t *attr,
                                   int pshared);

引數解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(預設,由這個屬性物件建立的條件變數只能在程序內使用)

  • PTHREAD_PROCESS_SHARED(允許條件變數在程序間中使用)

在程序間的用法:設定以後,將柵欄放置到共享記憶體中即可。

情景匯入

我們都知道引入執行緒在合理的範圍內可以加快提高程式的效率。但我們先來看看如果多執行緒同時訪問一個臨界資源會怎麼樣。

例程:模擬多視窗售票

c
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>

int ticket_sum = 20;

static int no = 0; // 為了登記是第幾條執行緒在工作
void *sell_ticket(void *arg){
    no++;
    int number = no;

    for(int i=0; i<20; i++)
    {
        if(ticket_sum>0)
        {
            sleep(1);
            printf("No.%d sell the %d th\n", number, 20 - ticket_sum + 1);
            ticket_sum--;
        }else{
            break;
        }
    }
    return 0;
}

intmain(){
    int flag, i;
    pthread_t tids[4];

    for(i = 0; i < 4; i++)
    {
        flag = pthread_create(&tids[i],NULL, &sell_ticket, NULL);
        if(flag)
        {
            printf("Create failed");
            return flag;
        }
    }

    sleep(2);
    void *ans;
    for(int i=0; i<4; i++)
    {
        flag=pthread_join(tids[i],&ans);
        if(flag)
        {
            return flag;
        }
    }
    return 0;
}

由於沒有引入同步機制,執行結果並不合理。

同步的有關概念

臨界資源:每次只允許一個執行緒進行訪問的資源
執行緒間互斥:多個執行緒在同一時刻都需要訪問臨界資源
同步:在特殊情況下,控制多執行緒間的相對執行順序

多執行緒程式設計的本質有三個方面:

  • 併發性是多執行緒的本質
  • 在巨集觀上,所有執行緒並行執行
  • 執行緒間相互獨立,互不干涉

在多工作業系統中,同時執行的多個任務可能:

  • 都需要訪問/使用同一種資源
  • 多個任務之間有依賴關係,某個任務的執行依賴於另一個任務。

有關同步機制

在多執行緒環境中,當我們需要保持執行緒同步時,通常通過 鎖 來實現。執行緒同步的常見方法:互斥鎖、條件變數、讀寫鎖、自旋鎖、訊號量、執行緒柵欄。

鎖 是大家都應該遵守的“君子”條約,因為如果有一個執行緒沒有遵守,那麼就沒有意義了:

  • 對共享資源操作前一定要獲得鎖。
  • 完成操作以後一定要釋放鎖。(多個鎖時, 若獲得順序是ABC連環扣, 釋放順序也應該是ABC。)
  • 儘量短時間地佔用鎖。
  • 執行緒錯誤返回時應該釋放它所獲得的鎖。

互斥鎖

在多工作業系統中,同時執行的多個任務可能都需要使用同一種資源。

這個過程有點類似於,公司部門裡,我在使用著印表機列印東西的同時(還沒有列印完),別人剛好也在此刻使用印表機列印東西,如果不做任何處理的話,打印出來的東西肯定是錯亂的。
線上程裡也有這麼一把鎖——互斥鎖(mutex),互斥鎖是一種簡單的加鎖的方法來控制對共享資源的訪問,互斥鎖只有兩種狀態,即上鎖( lock )和解鎖( unlock )。

互斥鎖的特點:

  • 原子性:把一個互斥鎖鎖定為一個原子操作,這意味著作業系統(或pthread函式庫)保證瞭如果一個執行緒鎖定了一個互斥鎖,沒有其他執行緒在同一時間可以成功鎖定這個互斥鎖
  • 唯一性:如果一個執行緒鎖定了一個互斥鎖,在它解除鎖定之前,沒有其他執行緒可以鎖定這個互斥鎖
  • 非繁忙等待:如果一個執行緒已經鎖定了一個互斥鎖,第二個執行緒又試圖去鎖定這個互斥鎖,則第二個執行緒將被掛起(不佔用任何cpu資源),直到第一個執行緒解除對這個互斥鎖的鎖定為止,第二個執行緒則被喚醒並繼續執行,同時鎖定這個互斥鎖。

應用互斥鎖需要注意的幾點

1、互斥鎖需要時間來加鎖和解鎖。鎖住較少互斥鎖的程式通常執行得更快。所以,互斥鎖應該儘量少,夠用即可,每個互斥鎖保護的區域應則儘量大。

2、互斥鎖的本質是序列執行。如果很多執行緒需要領繁地加鎖同一個互斥鎖,則執行緒的大部分時間就會在等待,這對效能是有害的。如果互斥鎖保護的資料(或程式碼)包含彼此無關的片段,則可以特大的互斥鎖分解為幾個小的互斥鎖來提高效能。這樣,任意時刻需要小互斥鎖的執行緒減少,執行緒等待時間就會減少。所以,互斥鎖應該足夠多(到有意義的地步),每個互斥鎖保護的區域則應儘量的少。

互斥鎖初始化與銷燬

互斥鎖的初始化有動態初始化與靜態初始化2種方式。

動態初始化、銷燬

c
intpthread_mutex_init(pthread_mutex_t *restrict mutex, constpthread_mutexattr_t *restrict attr);

intpthread_mutex_destroy(pthread_mutex_t *mutex);

描述:動態互斥鎖初始化函式(不再使用時應該呼叫pthread_mutex_destroy進行銷燬)

引數解析:

mutex:互斥鎖物件的地址
attr:互斥鎖屬性物件的地址,當為NULL,代表預設互斥鎖初始化。

靜態初始化(不需要銷燬)

僅侷限於靜態初始化的時候使用:將“宣告”、“定義”、“初始化”一氣呵成,除此之外的情況都只能使用pthread_mutex_init函式。

c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

初始化例程

c
#if 0
	// 靜態互斥鎖初始化。 (不需要pthread_mutex_destroy)
	pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;	
#else
	// 動態互斥鎖初始化。
	pthread_mutex_t mutex1;
	pthread_mutex_init(&mutex1, NULL);
	...
	pthread_mutex_destroy(&mutex1);
#endif

上鎖與解鎖

c
intpthread_mutex_lock(pthread_mutex_t *mutex);
intpthread_mutex_trylock(pthread_mutex_t *mutex);
intpthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
				   const struct timespec *restrict abstime);

intpthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_lock

上鎖,如果上鎖前已經鎖上,則掛起等待。

pthread_mutex_trylock

嘗試上鎖,如果上鎖前已經鎖上時,返回EBUSY而不是掛起等待。

pthread_mutex_timedlock

pthread_mutex_lock是基本等價的,但是在達到超時時間值時,pthread_mutex_timedlock不會對互斥鎖進行加鎖,而是返回錯誤碼ETIMEDOUT

pthread_mutex_unlock

使用結束後在程序退出前,解鎖。這樣,別的程序就可以獲取到鎖。

如果解鎖一個未加鎖的mutex互斥鎖,行為是未知的。

互斥鎖有關例程

c
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>

pthread_mutex_t mutex;

int count = 100;

void *routine(void *arg){
    printf("start thread\n");

    pthread_mutex_lock(&mutex);

    printf("child in mutex lock\n");

    if(count >= 100)
    {
        printf("in child\n");
        sleep(3);
        count -= 100;
    }

    pthread_mutex_unlock(&mutex);

    printf("child count = %d\n", count);

    return NULL;
}

intmain(void){
    pthread_t tid;
    pthread_mutex_init(&mutex, NULL);

    errno = pthread_create(&tid, NULL, routine, NULL);
    if(errno != 0)
    {
        perror("create thread failed\n");
        return -1;
    }

    pthread_mutex_lock(&mutex);

    if(count >= 100)
    {
        printf("in parent\n");
        sleep(3);
        count -= 100;
    }

    pthread_mutex_unlock(&mutex);

    printf("parent count = %d\n", count);

    pthread_join(tid, NULL);

    pthread_mutex_destroy(&mutex);

    return 0;
}

條件變數

操作互斥鎖時就會一直在迴圈判斷,而每次判斷都要加鎖、解鎖(即使本次並沒有修改臨界資源)。這就帶來了問題:

1)CPU浪費嚴重。
2)響應處理可能會導致不夠及時。

條件變數是一種同步機制,允許執行緒掛起,直到共享資料上的某些條件得到滿足。

條件變數函式不是非同步訊號安全的,不應當在訊號處理程式中進行呼叫。

特別要注意,如果在訊號處理程式中呼叫pthread_cond_signalpthread_cond_boardcast函式,可能導致呼叫執行緒死鎖。

條件變數一般配合互斥鎖進行使用。

條件變數的使用流程:

  • 執行緒A得到了互斥鎖,出於某個原因開始等待條件T。
  • 等待時,系統先解開執行緒A得到的鎖,然後將它掛起。
  • 如果還有執行緒B和A一樣,那麼同樣也進入等待(同樣地解鎖,同樣地掛起)
  • 此時,獨立於執行緒AB以外的另外一個執行緒C,重置了條件T,C可以選擇喚醒處於等待的AB:要麼C 廣播,引發驚群,讓AB來搶;要麼C 單獨通知,告訴最先等待的執行緒(在這裡是A)。
  • 假設這裡C選擇單獨通知,那麼A就會被喚醒,看到條件滿足了以後就重新加鎖。這樣一來,就省去了對於資源讀取時不用去輪訓等待了。

什麼是驚群:

舉一個很簡單的例子,當你往一群鴿子中間扔一塊食物,雖然最終只有一個鴿子搶到食物,但所有鴿子都會被驚動來爭奪,沒有搶到食物的鴿子只好回去繼續睡覺, 等待下一塊食物到來。每扔一塊食物,都會驚動所有的鴿子,即為驚群。

對於作業系統來說,多個程序/執行緒在等待同一資源是,也會產生類似的效果,其結果就是每當資源可用,所有的程序/執行緒都來競爭資源,造成的後果:
1)系統對使用者程序/執行緒頻繁的做無效的排程、上下文切換,系統系能大打折扣。
2)為了確保只有一個執行緒得到資源,使用者必須對資源操作進行加鎖保護,進一步加大了系統開銷。

條件變數的初始化與銷燬

條件變數的初始化也有動態初始化與靜態初始化2種方式。這次我們放在一起講:

c
// 動態初始化一個條件變數 
intpthread_cond_init(pthread_cond_t *restrict cond, constpthread_condattr_t *restrict attr);
	attr :預設NULL(LinuxThreads 實現條件變數不支援屬性,因此 cond_attr 引數實際被忽略。)
// 銷燬一個動態初始化的條件變數(在不使用條件變數時使用)
intpthread_cond_destroy(pthread_cond_t *cond);
	
// 靜態初始化一個條件變數
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

進入等待

c
intpthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

intpthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

描述:掛起執行緒直到其他執行緒觸發條件後才被喚醒。函式執行時先自動釋放指定的鎖(允許其他執行緒訪問),然後等待條件變數的變化。在條件滿足從而離開pthread_cond_wait()之前,mutex將被重新加鎖,以與進入pthread_cond_wait()前的加鎖動作對應。

無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個執行緒同時請求等待條件pthread_cond_wait()(或pthread_cond_timedwait())的競爭條件(Race Condition)。

條件變數的使用前提:

mutex互斥鎖必須是普通鎖(PTHREAD_MUTEX_TIMED_NP)或者適應鎖 (PTHREAD_MUTEX_ADAPTIVE_NP),且在呼叫pthread_cond_wait()前必須由本執行緒加鎖 (pthread_mutex_lock()),而在更新條件等待佇列以前,mutex保持鎖定狀態,並在執行緒掛起進入等待前解鎖。

喚醒一個等待者

c
intpthread_cond_signal(pthread_cond_t * cond);

描述:通過條件變數cond傳送訊息,若多個執行緒在等待,它只喚醒最先等待的那一個。

注: 呼叫 pthread_cond_signal 後要立刻釋放互斥鎖

c
intpthread_cond_broadcast(pthread_cond_t *cond);
intpthread_cond_signal(pthread_cond_t *cond);
// 重啟動等待該條件變數的所有執行緒。如果沒有等待的執行緒,則什麼也不做。(引發 驚群 效應)

喚醒所有等待者

c
intpthread_cond_broadcast(pthread_cond_t *cond);

描述:喚醒等待該條件變數的所有執行緒。如果沒有等待的執行緒,則什麼也不做。(引發 驚群 效應)

例程:生產者消費者模型

在經典的生產者-消費者場合中,生產者首先必須檢查緩衝是否已滿(numUsedBytes==BufferSize),如果緩衝區已滿,執行緒停下來等待 bufferNotFull條件。如果沒有滿,在緩衝中生產資料,增加numUsedBytes,啟用條件 bufferNotEmpty。使用mutex來保護對numUsedBytes的訪問。

pthread_cond_wait接收一個mutex作為引數,mutex被呼叫執行緒初始化為鎖定狀態。線上程進入休眠狀態之前,mutex會被解鎖。而當執行緒被喚醒時,mutex會處於鎖定狀態;從鎖定狀態到等待狀態的轉換是原子操作。當程式開始執行時,只有生產者可以工作,消費者被阻塞等待bufferNotEmpty條件,一旦生產者在緩衝中放入一個位元組,bufferNotEmpty條件被激發,消費者執行緒於是被喚醒。

c
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>

pthread_mutex_t mutex;
pthread_cond_t buffer_is_not_empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t buffer_is_not_full  = PTHREAD_COND_INITIALIZER;

const int DataSize = 10;
const int BufferSize = 1;
static int usedSpace = 0;

void *producer(void *arg){
    printf("producer thread\n");

    for (int i = 0; i < DataSize; ++i)
    {
        pthread_mutex_lock(&mutex);
        while (usedSpace == BufferSize)
        {
            pthread_cond_wait(&buffer_is_not_full, &mutex);
        }
        ++usedSpace;
        printf("P, %d\n", usedSpace);

        pthread_cond_broadcast(&buffer_is_not_empty);
        pthread_mutex_unlock(&mutex);
    }
    return "OK.";
}

void * consumer(void *arg){
    printf("consumer thread\n");
    for (int i = 0; i < DataSize; ++i)
    {
        pthread_mutex_lock(&mutex);
        while (usedSpace == 0)
        {
            pthread_cond_wait(&buffer_is_not_empty, &mutex);
        }
        --usedSpace;
        printf("C, %d\n", usedSpace);
        pthread_cond_broadcast(&buffer_is_not_full);
        pthread_mutex_unlock(&mutex);
    }
    printf("\n");
    return "OK.";
}

intmain(void){
    pthread_t tid_producer;
    pthread_t tid_consumer;
    errno = pthread_create(&tid_producer, NULL, producer, NULL);
    if(errno != 0)
    {
        perror("create thread for producer failed\n");
        return -1;
    }
    errno = pthread_create(&tid_consumer, NULL, consumer, NULL);
    if(errno != 0)
    {
        perror("create thread for consumer failed\n");
        return -1;
    }

    sleep(1);

    pthread_join(tid_consumer, NULL);
    pthread_join(tid_producer, NULL);


    return 0;
}

讀寫鎖

根據把對共享資源的訪問情況,劃分成讀者和寫者:

  • 讀者只對共享資源進行讀訪問。
  • 寫者則需要對共享資源進行寫操作。

讀寫鎖與互斥鎖的功能類似,但比它有更高的並行性:

1)在加上讀鎖後不能寫;加上寫鎖後不能讀寫,否則阻塞

2)允許多個讀者可以同時進行讀

3)寫者互斥(只允許一個寫者寫)

4)寫者優先於讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者)

讀寫鎖的初始化與銷燬

讀寫鎖的初始化也有動態初始化與靜態初始化2種方式。我們放在一起講:

c
// 動態初始化 與 銷燬
intpthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           constpthread_rwlockattr_t *restrict attr);
intpthread_rwlock_destroy(pthread_rwlock_t *rwlock);

// 靜態初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

上讀鎖

c
intpthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 

描述:加讀鎖(允許其他讀鎖訪問,不允許寫鎖訪問)

上寫鎖

c
intpthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

描述:加寫鎖(不允許其他讀 或 寫鎖訪問)

解鎖

c
intpthread_rwlock_unlock(pthread_rwlock_t *rwlock);

描述:解開當前加的鎖

例程:讀寫鎖例程

c
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>

int num=5;
pthread_rwlock_t rwlock;

void *reader(void *arg){
    pthread_rwlock_rdlock(&rwlock);
    printf("Reader %ld got the lock\n", (long)arg );

    pthread_rwlock_unlock(&rwlock);
    return 0;
}

void *writer(void *arg){
    pthread_rwlock_wrlock(&rwlock);
    printf("Writer %ld got the lock\n", (long)arg );
    pthread_rwlock_unlock(&rwlock);
    return 0;
}

intmain(){
    int flag;
    long n = 1, m = 1;
    pthread_t wid[5],rid[5];
    pthread_attr_t attr;

    flag = pthread_rwlock_init(&rwlock,NULL);
    if(flag)
    {
        printf("rwlock init error\n");
        return flag;
    }

    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//thread sepatate

    for(int i=0;i<num;i++)
    {
        if(i%3)
        {
            pthread_create(&rid[n-1],&attr,reader,(void *)n);
            printf("create reader %ld\n", n);
            n++;
        }else
        {
            pthread_create(&wid[m-1],&attr,writer,(void *)m);
            printf("create Writer %ld\n", m);
            m++;
        }
    }

    sleep(5);//wait other done

    return 0;
}

自旋鎖

自旋鎖與互斥鎖的區別

在多處理器環境中,自旋鎖最多隻能被一個可執行執行緒持有。如果一個可執行執行緒試圖獲得一個被爭用(已經被持有的)自旋鎖,那麼該執行緒就會一直進行忙等待,自旋,也就是空轉,等待鎖重新可用。如果鎖未被爭用,請求鎖的執行執行緒便立刻得到它,繼續向下執行。

一個被爭用的自旋鎖使得請求它的執行緒在等待鎖重新可用時自旋,特別的浪費CPU時間,所以自旋鎖不應該被長時間的持有。實際上,這就是自旋鎖的設計初衷,在短時間內進行輕量級加鎖

訊號量和讀寫訊號量適合於保持時間較長的情況,它們會導致呼叫者睡眠,因此只能在程序上下文使用而不能在中斷上下文使用,因為中斷的上下文不允許休眠(trylock可以),因此在中斷上下文只能使用自旋鎖。
自旋鎖保持期間是搶佔失效的(核心不允許被搶佔) ,而訊號量和讀寫訊號量保持期間是可以被搶佔的。

自旋鎖保護的臨界區預設是可以相應中斷的,但是如果在中斷處理程式中請求相同的自旋鎖,那麼會發生死鎖(核心自旋鎖可以關閉中斷)。

自旋鎖 的 初始化與銷燬

c
intpthread_spin_destroy(pthread_spinlock_t *lock);
intpthread_spin_init(pthread_spinlock_t *lock, int pshared);

描述:初始化/銷燬一個自旋鎖物件。

引數解析:

pshared:共享方式

  • PTHREAD_PROCESS_PRIVATE:表示這個自旋鎖是當前程序的區域性自旋鎖
  • PTHREAD_PROCESS_SHARED:這個自旋鎖可以在多個程序之間共享

如果想要使用自旋鎖同步多程序,那麼設定pshared = PTHREAD_PROCESS_SHARED,然後在程序共享記憶體中分配pthread_spinlock_t 物件即可(pthread_mutex_t亦如此,但其需要進行pthread_mutexattr_setpshared())。

自旋鎖操作

c
intpthread_spin_lock(pthread_spinlock_t *lock);
intpthread_spin_trylock(pthread_spinlock_t *lock);
intpthread_spin_unlock(pthread_spinlock_t *lock);

與互斥鎖基本一樣。

POSIX訊號量

POSIX代表 “可移植作業系統介面” Portable Operation System Interface 。只要按照這個API標準寫程式,理論上就可以在各個作業系統和硬體平臺上編譯執行。

POSIX 與System V區別

  • System V IPC存在時間比較老,許多系統都支援,但是介面複雜,並且可能各平臺上實現略有區別(如ftok的實現及限制)
  • posix訊號量只能單個操作,IPC訊號量可以多個訊號量同時操作,IPC訊號量更加強大
  • POSIX是新標準,語法簡單,並且各平臺上實現都一樣

訊號量:一個定義在核心系統中的特殊的變數,有以下定義:
1)P操作:減操作
2)V操作:加操作
3)該變數最小為0,如果等於0的情況下還去進行P操作,就會阻塞

The canonical names V and P come from the initials of Dutch words. V is generally explained as verhogen ("increase"). Several explanations have been offered for P, including proberen ("to test" or "to try"), passeren ("pass"), and pakken ("grab"). Dijkstra's earliest paper on the subject givespassering ("passing") as the meaning for P, and vrijgave ("release") as the meaning for V. It also mentions that the terminology is taken from that used in railroad signals. Dijkstra subsequently wrote that he intended P to stand for the portmanteauprolaag, short for probeer te verlagen, literally "try to reduce," or to parallel the terms used in the other case, "try to decrease."

訊號量(sem)和互斥鎖的區別:

互斥鎖只允許一個執行緒進入臨界區,而訊號量允許多個執行緒進入臨界區

使用時,需要以下標頭檔案:

c
#include<semaphore.h>

POSIX訊號量有兩種:有名訊號量(可以在程序中使用)和無名訊號量。有名訊號量與無名訊號量使用同一套函式(但是在初始化和銷燬的函式有所不同)

我們這裡介紹無名訊號量。

c
初始化並開啟有名訊號量:sem_open();       初始化無名訊號量:sem_init()

操作訊號量:sem_wait()/sem_trywait()/sem_timedwait()/sem_post()/sem_getvalue()

    
關閉有名訊號量:sem_close(); //關閉有名訊號量
銷燬有名訊號量:sem_unlink(); //試圖銷燬訊號量,一旦所有佔用該訊號量的程序都關閉了該訊號量,那麼就會銷燬這個訊號量

銷燬無名訊號量:sem_destroy()

初始化與銷燬訊號量

c
intsem_init(sem_t *sem, int pshared, unsignedint value);
intsem_destory(sem_t *sem);

描述:初始化/銷燬一個訊號量物件

引數解析:

sem:訊號量物件

pshared:共享方式

  • PTHREAD_PROCESS_PRIVATE:表示這個訊號量是當前程序的區域性訊號量
  • PTHREAD_PROCESS_SHARED:這個訊號量可以在多個程序之間共享

value:初始值

返回值:成功符合0,失敗返回-1。

訊號量操作

P操作

c
intsem_wait(sem_t *sem);
intsem_trywait(sem_t *sem); //試圖佔用訊號量,如果訊號量已經為0,立即報錯(errno 設定為 EAGAIN))
intsem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

描述:以原子操作的方式將訊號量的值減去1。

返回值:成功返回0;失敗返回-1,設定errno。

V操作

c
intsem_post(sem_t *sem);

描述::以原子操作的方式將訊號量的值加上1。

返回值:成功返回0;失敗返回-1,設定errno。

取值

c
intsem_getvalue(sem_t *sem, int *sval);

描述:獲得訊號量當前的值,放到sval中。

引數解析:

sval:存放結果的容器。

注意:如果有執行緒正在block這個訊號量,sval可能返回兩個值其中的一個,0或“-正在block的執行緒的數目”,在Linux中返回0。

If one or more processes or threads are blocked waiting to lock the semaphore withsem_wait(), POSIX.1 permits two possibilities for the value returned in #sval: either 0 is returned; or a negative number whose absolute value is the count of the number of processes and threads currently blocked insem_wait(). Linux adopts the former behavior.

例程:訊號量

通過訊號量模擬2個視窗,10個客人進行服務的過程。

c
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<semaphore.h>


int num=10;
sem_t sem;

void *get_service(void *cid){
    int id=*((int*)cid);
    if(sem_wait(&sem)==0)
    {
        printf("customer %d get the service \n", id);
        sleep(2);
        printf("customer %d done \n", id);
        sem_post(&sem);
    }
    return 0;
}

intmain(){
    sem_init(&sem,0,2);
    pthread_t customer[num];
    int flag;

    for(int i=0;i<num;i++)
    {
        int id=i;
        flag=pthread_create(&customer[i],NULL,get_service,&id);
        if(flag)
        {
            return flag;
        }else
        {
        }
        sleep(1);
    }

    //wait all thread done
    for(int j=0;j<num;j++)
    {
        pthread_join(customer[j],NULL);
    }
    sem_destroy(&sem);
    return 0;
}

執行緒柵欄

把先後到達的多個執行緒擋在同一柵欄前,直到所有執行緒(可指定個數)到齊,然後撤下柵欄同時放行。

使用場景

這種“柵欄”機制最大的特點就是最後一個執行wait的動作最為重要,就像賽跑時的起跑槍一樣,它來之前所有人都必須等著。所以實際使用中,pthread_barrier_wait常常用來讓所有執行緒等待“起跑槍”響起後再一起行動。

比如我們可以用pthread_create()生成100個執行緒,每個子執行緒在被create出的瞬間就會自顧自的立刻進入回撥函式執行。但我們可能不希望它們這樣做,因為這時主程序還沒準備好,和它們一起配合的其它執行緒還沒準備好,我們希望它們在回撥函式中申請完執行緒空間、初始化後停下來,一起等待主程序釋放一個“開始”訊號,然後所有執行緒再開始執行業務邏輯程式碼。

為了解決上述場景問題,我們可以在init時指定n+1個等待,其中n是執行緒數。而在每個執行緒執行函式的首部呼叫wait()。這樣100個pthread_create()結束後所有執行緒都停下來等待最後一個wait()函式被呼叫。這個wait()由主程序在它覺得合適的時候呼叫就好。最後這個wait()就是鳴響的起跑槍。

柵欄的初始化與銷燬

c
intpthread_barrier_init(pthread_barrier_t *restrict barrier, constpthread_barrierattr_t *restrict attr, unsigned count);

intpthread_barrier_destroy(pthread_barrier_t *barrier);

描述:初始化/銷燬一個柵欄物件,初始化時要指定等待者個數。

引數解析:

count:等待者個數

在柵欄前等待

c
intpthread_barrier_wait(pthread_barrier_t *barrier);
到柵欄前等待放行,如果該條函式執行的次數等於 #count 時,放行。

描述:讓一個執行緒在柵欄前告訴大家它已經就緒,等待放行。

例程:柵欄的使用

c
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>

pthread_barrier_t barrier;

void *Task1(void *arg);
void *Task2(void *arg);

intmain(void){
    int policy,inher;
    pthread_t tid;
    pthread_attr_t attr;
    structsched_paramparam;

    //初始化執行緒屬性
    pthread_attr_init(&attr);
    pthread_barrier_init(&barrier,NULL,2 + 1);//2+1個等待(2個執行緒,1個自己)

    //建立執行緒1
    pthread_create(&tid, &attr,Task1,NULL);

    //建立執行緒2
    pthread_create(&tid, &attr,Task2,NULL);
    
    printf("main process will sleep 6s.\n");
    sleep(6);/*等待6s後,才讓執行緒執行*/
    pthread_barrier_wait(&barrier);//起跑槍“砰!”

    pthread_join(tid, NULL);
    pthread_barrier_destroy(&barrier);
}

void *Task1(void *arg){
    printf("Task1 will be blocked.\n");
    pthread_barrier_wait(&barrier);//所有執行緒都被阻塞在這裡
    printf("Task1 is running.\n");
    sleep(3);//延時3s
    pthread_exit(NULL);
}

void *Task2(void *arg){
    printf("Task2 will be blocked.\n");
    pthread_barrier_wait(&barrier);//所有執行緒都被阻塞在這裡
    printf("Task2 is running.\n");
    sleep(3);//延時3s
    pthread_exit(NULL);
}

為同步物件設定屬性

https://blog.csdn.net/bytxl/article/details/8822551

在上文的學習中,為了降低學習的理解難度,我們沒有介紹互斥鎖的屬性。現在我們就來介紹有關內容。

執行緒和執行緒的同步物件(互斥鎖,讀寫鎖,條件變數,柵欄)都具有屬性。在修改屬性前都需要對該結構進行初始化。使用後要把該結構回收。

除了互斥鎖還可以設定鎖的型別以外,所有的同步物件只能設定作用域。

互斥鎖屬性

初始化與銷燬

c
#include<pthread.h>

intpthread_mutexattr_destroy(pthread_mutexattr_t *attr);
intpthread_mutexattr_init(pthread_mutexattr_t *attr);

描述:類似於執行緒屬性(但沒有靜態初始化方法),使用時初始化(設定了預設屬性),使用後銷燬(設定了無效屬性)

返回值:成功返回0,失敗返回錯誤號。

設定互斥鎖的共享方式

c
#include<pthread.h>

intpthread_mutexattr_getpshared(constpthread_mutexattr_t
                                 *restrict attr, int *restrict pshared);
intpthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
                                 int pshared);

描述:設定互斥鎖的作用域。

引數解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(預設,由這個屬性物件建立的互斥鎖只能在程序內使用)

  • PTHREAD_PROCESS_SHARED(允許互斥鎖在程序間中使用)

在程序間的用法:設定以後,將一個互斥鎖放置到共享記憶體中即可。

設定互斥鎖的型別

c
#include<pthread.h>

intpthread_mutexattr_gettype(constpthread_mutexattr_t *restrict attr,
                              int *restrict type);
intpthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

描述:設定互斥鎖的型別。

引數解析:

type: 由於 DEFAULT ( NORMAL) 屬性有太多的未定義行為,應該儘可能避免使用。

  • PTHREAD_MUTEX_DEFAULT(預設)
  • PTHREAD_MUTEX_NORMAL
  • PTHREAD_MUTEX_ERRORCHECK
  • PTHREAD_MUTEX_RECURSIVE
PTHREAD_MUTEX_NORMAL

這種型別的互斥鎖不會自動檢測死鎖。如果一個執行緒試圖對一個互斥鎖重複鎖定,將會引起這個執行緒的死鎖。如果試圖解鎖一個由別的執行緒鎖定的互斥鎖會引發不可預料的結果。如果一個執行緒試圖解鎖已經被解鎖的互斥鎖也會引發不可預料的結果。

PTHREAD_MUTEX_ERRORCHECK

這種型別的互斥鎖會自動檢測死鎖。如果一個執行緒試圖對一個互斥鎖重複鎖定,將會返回一個錯誤程式碼。如果試圖解鎖一個由別的執行緒鎖定的互斥鎖將會返回一個錯誤程式碼。如果一個執行緒試圖解鎖已經被解鎖的互斥鎖也將會返回一個錯誤程式碼。

PTHREAD_MUTEX_RECURSIVE

如果一個執行緒對這種型別的互斥鎖重複上鎖,不會引起死鎖,一個執行緒對這類互斥鎖的多次重複上鎖必須由這個執行緒來重複相同數量的解鎖,這樣才能解開這個互斥鎖,別的執行緒才能得到這個互斥鎖。如果試圖解鎖一個由別的執行緒鎖定的互斥鎖將會返回一個錯誤程式碼。如果一個執行緒試圖解鎖已經被解鎖的互斥鎖也將會返回一個錯誤程式碼。這種型別的互斥鎖只能是程序私有的(作用域屬性為PTHREAD_PROCESS_PRIVATE)。

條件變數屬性

初始化與銷燬

c
intpthread_condattr_init(pthread_condattr_t *cattr);
intpthread_condattr_destroy(pthread_condattr_t *cattr);

類似上文,不再描述。

設定共享方式

c
intpthread_condattr_getpshared(constpthread_condattr_t *restrict attr,
                                int *restrict pshared);
intpthread_condattr_setpshared(pthread_condattr_t *attr,
                                int pshared);

引數解析:既然條件變數是搭配互斥鎖用的,那麼同樣地,就可以設定共享方式。

pshared:

  • PTHREAD_PROCESS_PRIVATE(預設,由這個屬性物件建立的條件變數只能在程序內使用)

  • PTHREAD_PROCESS_SHARED(允許條件變數在程序間中使用)

在程序間的用法:設定以後,將條件變數放置到共享記憶體中即可。

讀寫鎖屬性

初始化與銷燬

c
intpthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
intpthread_rwlockattr_init(pthread_rwlockattr_t *attr);

類似上文,不再描述。

設定共享方式

c
intpthread_rwlockattr_getpshared(constpthread_rwlockattr_t
                                  *restrict attr, int *restrict pshared);
intpthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,
                                  int pshared);

引數解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(預設,由這個屬性物件建立的條件變數只能在程序內使用)

  • PTHREAD_PROCESS_SHARED(允許條件變數在程序間中使用)

在程序間的用法:設定以後,將讀寫鎖放置到共享記憶體中即可。

柵欄屬性

初始化與銷燬

c
intpthread_barrierattr_destroy(pthread_barrierattr_t *attr);
intpthread_barrierattr_init(pthread_barrierattr_t *attr);

類似上文,不再描述。

設定共享方式

c
intpthread_barrierattr_getpshared(constpthread_barrierattr_t
                                   *restrict attr, int *restrict pshared);
intpthread_barrierattr_setpshared(pthread_barrierattr_t *attr,
                                   int pshared);

引數解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(預設,由這個屬性物件建立的條件變數只能在程序內使用)

  • PTHREAD_PROCESS_SHARED(允許條件變數在程序間中使用)

在程序間的用法:設定以後,將柵欄放置到共享記憶體中即可。