執行緒

fowind發表於2024-04-14

什麼是執行緒:程序裡面的一條執行流程

img

為什麼要引入執行緒

這就不得說說程序的缺點了:

  • 程序間的切換,會導致TLB、CPU的Cache失效
  • 程序之間是隔離的,程序間的通訊需要打破隔離的壁障

而相較於程序而言,

  • 執行緒的建立和銷燬是輕量級的。

  • 同一程序的執行緒之間的切換,不會導致TLB失效、也不會導致CPUcache失效.

  • 執行緒之間共享程序的所有資源,所以執行緒之間通訊的代價小

獲取程序的標識

程序:getpid(), getppid()

執行緒:pthread_self()

NAME
       pthread_self  -  obtain  ID  of the calling thread

SYNOPSIS
       #include <pthread.h>

       pthread_t pthread_self(void);

       Compile and link with -pthread.

下面透過一個簡單的例子來了解一下pthread_self()怎麼使用。

在使用之前,我們需要先知道pthread_t是什麼型別,可以透過下面的命令獲取

gcc -E pthread_self.c | grep -nE "pthread_t"

1305:typedef unsigned long int pthread_t;

可以看到pthread_tunsigned long型別

int main(int argc, char* argv[])
{
    printf("pid = %d, ppid = %d\n", getpid(), getppid());
    pthread_t tid = pthread_self();
    printf("tid = %lu\n", tid);
    return 0;
}

需要注意的是,為保證可移植性在編譯時需要再Makefile檔案中加入-pthread

img

執行緒的基本操作

  • 執行緒的建立
  • 執行緒的終止
  • 執行緒的等待
  • 執行緒的清理
  • 執行緒的遊離

在正式介紹pthead系列函式時,需要了解一個pthread的設計原則:

  • 成功:返回0
  • 失敗:返回錯誤碼,不會設定errno

執行緒的建立

在建立執行緒時,使用到pthread_creat()函式。

NAME
       pthread_create - create a new thread

SYNOPSIS
       #include <pthread.h>

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

       Compile and link with -pthread.

thread: 作為傳出引數,用於傳出建立新執行緒的id,注意是指標型別。
attr: 執行緒的屬性,傳入引數,一般填NULL,表示採用預設屬性
start_routine: 執行緒的入口函式,型別和引數都是void*型別,在C語言中指通用指標,可以傳遞或返回任意型別的值。
arg:入口函式的引數,沒有引數傳NULL

透過一個簡單的例子來了解一下thread_create()函式的使用

#include <func.h>

void printf_ids(char* prefix) {
    printf("%s pid = %d, ppid = %d, thread_id = %lu\n", 
           prefix, getpid(), getppid(), pthread_self());
}

void* start_routine(void* arg){
    printf_ids("new thread");
    return NULL;
}
int main(int argc, char* argv[])
{
    // 建立執行緒
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if (err) {
        error(1, err, "pthread_creat");
    }
    
    // 主執行緒
    printf("main pthread\n");
    return 0;
}

img

可以看到,主執行緒和子執行緒的執行順序是不確定的。

在第一種情況只列印了主執行緒的資訊,這是因為主執行緒在結束時,程序就會終止(所有子執行緒都會終止)

這就不得不提一個線上程程式設計中的慣用法:主執行緒通常用於接收任務(或請求),然後將這些任務分配給其他子執行緒執行。主執行緒會等待所有子執行緒執行完畢後再結束,從而實現有序的退出。在實現執行緒的等待需要使用pthread_join()函式,我們在後面介紹。

需要注意的是:主執行緒的執行流程是從main函式開始,而子執行緒的執行流程從入口函式開始

在這,提供一個技巧,在64位計算機中,如果傳遞的引數不超過8個位元組,可以將其分裝到一個指標中傳遞。下面是一個例項:

// pthread_create2.c
void printf_ids(char* prefix) {
    printf("%s pid = %d, ppid = %d, thread_id = %lu\n", 
           prefix, getpid(), getppid(), pthread_self());
}

void* start_routine(void* arg){
    int num = (int)arg;
    printf_ids("new thread");
    printf("num = %d\n", num);
    return NULL;
}
int main(int argc, char* argv[])
{
    // 建立執行緒
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, (void*)4096);
    if (err) {
        error(1, err, "pthread_creat");
    }
    
    // 主執行緒
    printf("main pthread\n");
    sleep(2);
    return 0;
}
img

當傳遞多個引數時,需要封裝到一個陣列(同型別)或結構體中。下面是一個簡單的例項,但會出現一些問題,我們先執行一下。

typedef struct {
    int a;
    double b;
    char* message;
} Paras;

void printf_ids(char* prefix) {
    printf("%s pid = %d, ppid = %d, thread_id = %lu\n", 
           prefix, getpid(), getppid(), pthread_self());
}

void* start_routine(void* arg){
    Paras* arguments = (Paras*)arg; 
    printf_ids("new thread");
    printf("a = %d, b = %lf, message = %s\n",
           arguments->a, arguments->b, arguments->message);
    return NULL;
}
int main(int argc, char* argv[])
{
    // 建立執行緒
    pthread_t tid;
    Paras arguments = {1, 3.14, "Hello"};
    int err = pthread_create(&tid, NULL, start_routine, &arguments);
    if (err) {
        error(1, err, "pthread_creat");
    }
    
    // 主執行緒
    printf("main pthread\n");
    sleep(2);
    return 0;
}
img

可以看到,子程序可以正常獲取arguments的值。這是因為arguments儲存在主執行緒的棧上,由於執行緒之間資源共享,因此子執行緒可以成功獲取到arguments的值

但需要注意的是,不要輕易訪問其他執行緒的棧空間。因為當訪問的執行緒終止時,其對應的堆空間也會被釋放。

若要線上程之間共享資料,可以放到程序的堆空間或程序的程式碼段和資料段。

若要存放到堆空間,需確保堆空間有且只能被其中一個程序free.

執行緒的等待

要實現等待某執行緒完成,需要呼叫pthread_join()函式

SYNOPSIS
       #include <pthread.h>

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

       Compile and link with -pthread.

第一個引數:thread指定要等待執行緒的tid

第二個引數retval:是void**型別,是傳出引數,接收返回值(void*,任意型別),不想接收返回值置為NULL。

下面是一個簡單的例子

#include <func.h>

void* start_routine(void* arg) {
    printf("new thread start\n");
    
    sleep(3);   // 讓子執行緒sleep(3),看主執行緒是否提前結束

    printf("new thread end\n");
    return NULL;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, start_routine, NULL);
    
    // 等待tid終止,如果tid沒有終止,主執行緒就會一直阻塞
    pthread_join(tid, NULL);
    return 0;
}

執行結果

new thread start
new thread end

可以發現,當強制讓子執行緒sleep(3)時,主執行緒會一直等待子執行緒結束,否則會一直阻塞。

執行緒的終止

引起程序終止的事件有:從main返回、呼叫exit()、使用訊號量機制kill -SIGKILL pid

引起執行緒終止的事件有:

  • start_routine返回

  • 呼叫pthread_exit()

  • 呼叫pthread_cancel(),一個執行緒給另一個執行緒傳送取消請求,若響應則終止

pthread_exit

NAME
       pthread_exit - terminate calling thread

SYNOPSIS
       #include <pthread.h>

       void pthread_exit(void *retval);

       Compile and link with -pthread.


可以看到pthread_exit()的引數是void*型別,是一個傳出引數。通常是執行緒的退出狀態或其他一些有用的結果。在同一程序中的其他執行緒,可以使用pthread_join()接收。

下面是一個分別是從start_routine返回以及使用pthread_exit()退出的簡單示例。

// 從start_routine返回

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

    printf("new thread end\n");
    return NULL;

}
int main(int argc, char* argv[])
{
    // 建立執行緒
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if (err) {
        error(1, err, "pthread_creat");
    }
    
    // 主執行緒
    printf("main pthread: create new ptherad\n");

    sleep(3);   // 等待子執行緒結束
    return 0;
}

// 呼叫pthread_exit()

void* start_routine(void* arg){
    printf("new thread start\n");
    pthread_exit(1);
    printf("new thread end\n");
    //return NULL;
}
int main(int argc, char* argv[])
{
    // 建立執行緒
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if (err) {
        error(1, err, "pthread_creat");
    }
    
    // 主執行緒
    printf("main pthread: create new ptherad\n");

    sleep(3);   // 等待子執行緒結束
    return 0;
}

imgimg

透過對比可以發現當使用pthread_eixt()顯示地退出執行緒時,呼叫即退出。而使用start_routine返回,執行緒可以透過其他執行緒退出。

pthread_cancel(瞭解)

SYNOPSIS
       #include <pthread.h>

       int pthread_cancel(pthread_t thread);

       Compile and link with -pthread.

透過下面的例子來簡單瞭解一下這個函式

#include <func.h>

void* start_routine(void* arg) {
    for(;;) {

    }
    return NULL;
}

int main(int argc, char* argv[])
{
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if (err) {
        error(1, err, "pthread_create");
    }

    // 主執行緒傳送取消請求給子執行緒
    err = pthread_cancel(tid);
    if (err) {
        error(1, err, "pthread_cancel");
    }
    // 等待子執行緒終止
    pthread_join(tid, NULL);
    return 0;
}

在上面的例子中,由於子執行緒的入口函式沒有響應主執行緒的終止訊號,因此主執行緒會在pthread_join(tid, NULL)處阻塞等待子執行緒結束。

但如果將子執行緒的入口函式修改成下面的程式碼,會結束子執行緒的執行。

void* start_routine(void* arg) {
    for(;;) {
		sleep(1);
    }
    return NULL;
}

這是由於sleep(1)是一個取消點。透過閱讀pthread_cancel的man手冊,可以看到是否響應,以及何時響應取決於執行緒的屬性。

The  pthread_cancel()  function  sends  a  cancellation  request to the thread thread.
Whether and when the target thread reacts to the cancellation request depends  on  two
attributes  that  are  under  the  control of that thread: its cancelability state and
type.
  • 是否響應:取消state
  • 何時響應:取消type

修改者兩個屬性可以透過下面兩個函式進行。

#include <pthread.h>

int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);

其中oldstateoldtype是傳出引數,用於返回舊的狀態和舊的取消型別。

取消stste:的取值有下面兩個

PTHREAD_CANCEL_ENABLE (預設)	響應取消狀態
PTHREAD_CANCEL_DISABLE		 不響應取消狀態	

取消type: 的取值有:

PTHREAD_CANCEL_DEFERRED (預設)	在取消點響應
PTHREAD_CANCEL_ASYNCHRONOUS		 可以在任意點響應

取消點可以檢視man手冊man 7 pthread

執行緒的清理

要實現執行緒的清理,需要先透過下面兩個函式註冊執行緒清理函式

#include <pthread.h>

       void pthread_cleanup_push(void (*routine)(void *), void *arg);
			第一個引數:要執行的清理函式
            第二個引數:傳遞給清理函式的引數
       void pthread_cleanup_pop(int execute);	移除最近新增的清理處理程式。如果它的引數是非零值,則它還會執行清理處理程式
			execute:是一個標誌,用於指示是否執行清理函式。
            0: 不執行
            1: 執行

透過一個簡單例子,來了解一些執行緒的清理

void cleanup(void* arg){
    char* msg = (char*)arg;
    puts(msg);
}

void* start_routine(void* arg) {
    // 註冊執行緒清理函式
    pthread_cleanup_push(cleanup, "111");
    pthread_cleanup_push(cleanup, "222");

    // 2. 執行執行緒邏輯
    printf("new thread push\n");
    sleep(1);
    printf("new thread pop\n");

    // 3. 執行緒退出
    //pthread_exit(NULL);
    //return NULL;

    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
}

int main(int argc, char* argv[])
{
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);

    // 主執行緒傳送取消請求給子執行緒
    err = pthread_cancel(tid);

    // 等待子執行緒終止
    pthread_join(tid, NULL);
    return 0;
}

使用pthread_exit()退出子執行緒的執行結果

new thread push
new thread pop
222
111

使用return NULL退出子執行緒的執行結果

new thread push
new thread pop

使用pthread_cancel()在退出點退出子執行緒的執行結果

new thread push
222
111

什麼時候執行執行緒清理函式呢?透過上面的例子不難發現:

  1. 使用pthread_exit退出
  2. 響應取消請求時,都會執行執行緒清理函式

注意:

  1. clear_push 和 clearup_pop一定要成對出現
  2. 從start_routine 返回不會執行執行緒清理函式
  3. 呼叫pthread_clearnup_pop(1)時,不會造成執行緒終止

執行緒的遊離

執行緒的遊離是指:斷開執行緒之間的attached,使執行緒處於遊離狀態。

	   #include <pthread.h>

       int pthread_detach(pthread_t thread);
typedef struct{
    int id;
    char name[20];
    char gender;
} Student;

void print_stu_info(Student* s){
    printf("%d %s %c",
           s->id,
           s->name,
           s->gender);
}
void* start_routine(void* arg) {
    printf("new thread start\n");
    printf("new thread end\n");

    Student* s = (Student*)malloc(sizeof(Student));
    s->id = 1;
    strcpy(s->name, "hello");
    s->gender = 'f';

    pthread_exit(s);
}

int main(int argc, char* argv[])
{
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if (err) {
        error(1, err, "pthread_create");
    }

    // detach 執行緒
    err = pthread_detach(tid);
    if (err) {
        error(1, err, "pthread_detach");
    }
    Student* retval;
    // 等待子執行緒終止
    err = pthread_join(tid, (void**)&retval);
    if (err) {
        error(1, err, "pthread_join");
    }

    // 列印retval
    print_stu_info(retval);
    free(retval);
    return 0;
}

在列印之前已經遊離了執行緒,因此會join失敗。

./pthread_detach: pthread_join: Invalid argument

同步、非同步、併發、並行

程式的執行方式:非同步、同步、併發、並行

程式設計正規化:貫通式,物件導向、函式式、範型

非同步:任務之間相互獨立,不需要等待前一個任務完成就可以開始執行下一個任務,非同步模式下事件的執行順序一般是隨機的,一個任務的執行不會阻塞其他任務的進行。

同步:時間之間的執行順序是確定的,每一個任務需要等待前一個任務完成才可以開始執行。可以看作它們共同遵循一定的規則,可以讓程式有秩序的執行。同步的基礎是通訊,通訊的基礎是共享資源。

併發:併發是一種現象。兩個執行流程在一段時間內可以交替執行。

並行:是一種技術。指的是在同一個時間點可以執行多個任務。

由於執行緒間的非同步執行,從而導致竟態條件的產生。

竟態條件

竟態條件是指:有多個執行流程同時訪問共享資源,從而導致執行的結果由執行流程訪問共享資源的先後順序決定。

臨界區

為避免竟態條件的額產生,提出了臨界區的概念。

臨界區:訪問共享資源的一段程式碼,資源通常是一個變數或資料結構

為了實現併發程式設計,我們希望原子式執行一系列指令,但由於單處理器上的中斷(或多個執行緒在多處理器上併發執行),很難實現。

因此,鎖(lock)直接解決這個問題。

什麼是鎖

鎖是一個變數,因此需要宣告一個某種型別的鎖變數(lock variable)才能使用。

鎖變數儲存了鎖在某一時刻的狀態。

透過給臨界區加鎖,可以保證臨界區內只有一個執行緒活躍,從而保證對臨界區的訪問是原子的。鎖將原本由作業系統排程的混亂狀態變為可控。

互斥鎖

SYNOPSIS
       #include <pthread.h>

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

lock: 上鎖。先嚐試獲取鎖,若鎖被佔有,會一直阻塞,直到獲取鎖

unlock: 釋放鎖

trylock: 嘗試上鎖。嘗試獲取鎖,獲取不成功立即返回。

初始化鎖

在使用鎖之前,需要正確初始化鎖。

SYNOPSIS
       #include <pthread.h>
	   
       // 銷燬鎖
       int pthread_mutex_destroy(pthread_mutex_t *mutex);

	   // 動態初始化鎖	
       int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);

	   // 靜態初始化鎖
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

動態初始化鎖

  • mutex: 指向將要被初始化的互斥鎖變數的指標
  • attr: 可選屬性。如果設定為NULL,初始化為預設屬性

需要注意的是,若採用動態初始化,需要使用pthread_mutex_destory()銷燬鎖

銷燬鎖

int pthread_mutex_destroy(pthread_mutex_t *mutex);

怎樣上鎖

一個上鎖的簡單步驟就是:

  1. 需要先判斷出臨界區
  2. 在臨界區前上鎖
  3. 臨界區後釋放鎖

為保證程式的邏輯,

下面是一個使用使用靜態初始化方式上鎖的例子:


請完善下面程式:

int main(void) {
    long long* value = (long long*) calloc(1, sizeof(long long));
    // 建立兩個執行緒
    // 第一個執行緒執行 (*value)++ 10000000次
    // 第二個執行緒葉執行 (*value)++ 10000000次


    // 主線乘等待兩個子執行緒結束。並列印 *value 的值。

}
// 靜態初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 執行緒函式
void* start_routine(void* arg) {
    long long* value = (long long*)arg;
    for (int i = 0; i < 10000000; i++) {
        // 上鎖
        pthread_mutex_lock(&mutex);
        (*value)++;
        // 釋放鎖
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main(void) {
    long long* value = (long long*) calloc(1, sizeof(long long));
    // 建立兩個執行緒
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, start_routine, value);
    pthread_create(&tid2, NULL, start_routine, value);


    // 主線乘等待兩個子執行緒結束。並列印 *value 的值。
    err = pthread_join(tid1, NULL);
    err = pthread_join(tid2, NULL);
    // 輸出最終結果。

    printf("*value = %lld\n", *value);

    free(value);
}

執行結果如下:

*value=2000000

如果不上鎖呢?

多次執行:
*value = 11256461
*value = 11863935
*value = 11400809

可以發現,每次執行結果都不同,發生的原因是什麼呢?

檢視一下執行+1操作的彙編程式碼:

mov 0x8049a1c, %eax 
add $0x1, %eax 
mov %eax, 0x8049a1c

假設兩個執行緒在執行時出現下面這種情況,導致競爭狀態的產生,因此需要上鎖。

img

下面,我們以銀行的為例,來詳細介紹一下鎖的使用

鎖的使用

實現銀行取錢功能

typedef struct {
    int id;
    int balance;
} Account;

Account acct1 = {1, 100};
pthread_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 取錢
int withdraw(Account* acct, int money) {
    pthread_mutex_lock(&mutex);
    // 檢驗
    if (acct->balance < money) {
        return 0;
    } 
    // 取錢
    acct->balance -= money;
    pthread_mutex_unlock(&mutex);
    return money;
}

void* start_routine(void* arg) {
    int money = (int)arg;
    int n = withdraw(&acct1, money);
    printf("%lu withdraw $%d\n", pthread_self(), n);
    return NULL;
}

void* start_routine1(void* arg) {
    int money = (int)arg;
    int n = withdraw(&acct1, money);
    printf("%lu withdraw $%d\n", pthread_self(), n);
    return NULL;
}

int main(int argc, char* argv[])
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, start_routine, (void*)100);
  	pthread_create(&tid2, NULL, start_routine1, (void*)100);

    // 主執行緒等待子執行緒
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

這樣就可以使取錢操作正確進行。

但存在一個問題:只有一把鎖,因此在同一時刻只能有一個人可以取錢,導致併發量很低,在實際環境中很不適用。

在實際環境中,應該是每個使用者都有自己的鎖,自己取錢時不影響其他人。可以修改程式碼

typedef struct {
    int id;
    int balance;
    pthread_mutex_t mutex;
} Account;

Account acct1 = {1, 100};
//pthread_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 取錢
int withdraw(Account* acct, int money) {
    pthread_mutex_lock(&acct->mutex);
    // 檢驗
    if (acct->balance < money) {
        pthread_mutex_unlock(&acct->mutex);
        return 0;
    } 
    // 取錢
    acct->balance -= money;
    pthread_mutex_unlock(&acct->mutex);
    return money;
}

下面實現一個簡單的轉賬功能。

// 轉賬
int transfer(Account* acctA, Account* acctB, int money) {
    pthread_mutex_lock(&acctA->mutex);
    pthread_mutex_lock(&acctB->mutex);

    if (acctA->balance < money) {
        pthread_mutex_unlock(&acctA->mutex);
        pthread_mutex_unlock(&acctB->mutex);
        return 0;
    }
    
    acctA->balance -= money;
    acctB->balance += money;

    pthread_mutex_unlock(&acctA->mutex);
    pthread_mutex_unlock(&acctB->mutex);

    return money;
}

但如果線上程A執行期間強行sleep(1)則可能發生死鎖,

img

程式的執行流程如下,由於tid1會一直等待tid2釋放鎖,tid2也會一直等待tid1釋放鎖,所以程式處於死鎖狀態。而主執行緒則在join處等待子執行緒結束。

img

死鎖

死鎖(deadlock)是指多個程序或執行緒在執行過程中造成的一種相互等待的現象,若無外力干涉,將無法向前推進。

死鎖出現的原因:

  1. 互斥:至少有一個資源處於非共享模式。即,在一段時間內只有一個程序可以使用資源。如果另外一個程序請求該資源,請求者只能等待,直到資源被釋放。
  2. 持有並等待:一個程序至少佔有一個資源,並正在等待獲取被其他程序持有的資源。
  3. 不能搶佔:資源不能被搶佔。一旦資源被佔有,在它被使用完成並資源釋放之前,不能被強行奪取
  4. 迴圈等待:存在一條程序資源的迴圈鏈,鏈中的每一個程序至少佔有一個資源,該資源被鏈中的下一個程序鎖請求。
img

以上4個條件缺一不可。因此破除死鎖只需破壞其中一個條件既可。

破壞迴圈等待

如何破壞迴圈等待,最常用的方式是按照一定順序上鎖。修改轉賬程式碼如下。

// 轉賬
int transfer(Account* acctA, Account* acctB, int money) {
    if (acctA->id < acctB->id) {
        pthread_mutex_lock(&acctA->mutex);
        sleep(1);   // 切換
        pthread_mutex_lock(&acctB->mutex);
    } else {
        pthread_mutex_lock(&acctB->mutex);
        sleep(2);    // 切換
        pthread_mutex_lock(&acctA->mutex);
    }

    if (acctA->balance < money) {
        pthread_mutex_unlock(&acctA->mutex);
        pthread_mutex_unlock(&acctB->mutex);
        return 0;
    }
    
    acctA->balance -= money;
    acctB->balance += money;

    pthread_mutex_unlock(&acctA->mutex);
    pthread_mutex_unlock(&acctB->mutex);

    return money;
}

破壞不能搶佔

// 破壞不能搶佔
start:
    pthread_mutex_lock(&acctA->mutex);
    sleep(1);
    int err = pthread_mutex_trylock(&acctB->mutex);
    if (err) {
        pthread_mutex_unlock(&acctA->mutex);
        // 停留一個隨機時間
        int nsec = rand() % 5;
        sleep(nsec);
        goto start;
    }

破壞持有並等待

破壞持有並等待,可以透過要麼一次獲得所有鎖,要麼一次也不獲取。

因此,可以定義一個全域性鎖,將所有獲取鎖的操作變為原子操作

// 2. 持有並等待
    pthread_mutex_lock(&protection);
    pthread_mutex_lock(&acctA->mutex);
    sleep(1);   // 切換
    pthread_mutex_lock(&acctB->mutex);
    pthread_mutex_unlock(&protection);

破壞互斥

破壞互斥,需要硬體的支援。

條件變數

鎖並不是併發程式設計所需的唯一原語。詳細來說,在很多情況下,執行緒需要檢查某一條件滿足之後,才會繼續執行。

如何等待一個條件滿足呢?

簡單的方法是自旋直到條件滿足,下面是一個簡單例子。

volatile int done = 0;

void* start_continue(void* arg) {
    printf("child\n");
    done = 1;
    return NULL;
}

int main(int argc, char* argv[]) {
    printf("parent begin\n");
    pthread_t tid;
    pthread_create(&tid, NULL, child, NULL); // create child
    while (done == 0) 
        ;	// 自旋
    printf("parent end\n");
    return 0;
}

執行緒也可以使用條件變數來等待一個條件變為真。使用條件變數,需要等待和喚醒機制。

int pthread_cond_broadcast(pthread_cond_t *cond);	// 等待
int pthread_cond_signal(pthread_cond_t *cond);		// 喚醒

在使用條件變數時,必須有另外一個與此條件相關的鎖,在使用pthread_cond_wait()pthread_cond_signal()函式時,必須擁有該鎖。

典型的用法如下:

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; 
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_mutex_lock(&lock);
while (ready == 0) {
    pthread_cond_wait(&cond, &lock);
}
pthread_mutex_unlock(&lock);

在初始化相關的鎖和條件之後,一個執行緒檢查變數 ready 是否準備好。如果沒有,那麼執行緒只是簡單地呼叫等待函式以便休眠,直到其他執行緒喚醒它。

喚醒執行緒的程式碼可以執行在另外某個執行緒中

Pthread_mutex_lock(&lock); 
ready = 1; 
Pthread_cond_signal(&cond); 
Pthread_mutex_unlock(&lock);

等待呼叫將鎖(互斥鎖)作為其第二個引數,而訊號呼叫僅需要一個條件。

這是因為,等待呼叫除了使呼叫執行緒進入休眠狀態外,還會讓呼叫者在睡眠時釋放鎖。如果不這樣,其他執行緒就不會獲得鎖將其喚醒。

但是,在被喚醒之後返回之前,pthread_cond_wait()會重新獲取該鎖

條件變數的初始化和銷燬

在使用條件變數之前需要對其進行初始化

SYNOPSIS
       #include <pthread.h>
	   
       int pthread_cond_destroy(pthread_cond_t *cond);

	   // 動態初始化
       int pthread_cond_init(pthread_cond_t *restrict cond,
           const pthread_condattr_t *restrict attr);

	   // 靜態初始胡
       pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

通知機制(條件滿足)

當條件滿足時,通知等待該條件成立的執行緒

int pthread_cond_signal(pthread_cond_t *cond);

cond滿足時,會喚醒一個等待該條件的執行緒。

主要注意的是:核心在實現時,為了效能考慮,可能會喚醒多個等待的執行緒

int pthread_cond_broadcast(pthread_cond_t *cond);

會喚醒所有等待該條件的執行緒。

等待機制(條件不滿足)

條件一直不成立,就會一直等待

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

pthread_cond_t *restrict cond: 指向條件變數的指標。

pthread_mutex_t *restrict mutex: 互斥鎖

生產者/消費者(有界緩衝區)問題

假設一個或多個生產者執行緒和一個或多個消費者執行緒。生產者將生產的資料項放入緩衝區;消費者從緩衝區中取走資料項,以某種方式消費。

因為緩衝區是共享資源,所以必須透過同步機制來進行訪問。以免產生竟態條件。

cond_t cond; 
mutex_t mutex; 

void *producer(void *arg) { 
    int i; 
    for (i = 0; i < loops; i++) { 
        Pthread_mutex_lock(&mutex);             // p1 
        if (count == 1)                         // p2 
            Pthread_cond_wait(&cond, &mutex);   // p3 
        put(i);                                 // p4 
        Pthread_cond_signal(&cond);             // p5 
        Pthread_mutex_unlock(&mutex);           // p6 
    } 
} 

void *consumer(void *arg) { 
    int i; 
    for (i = 0; i < loops; i++) { 
        Pthread_mutex_lock(&mutex);             // c1 
        if (count == 0)                         // c2 
            Pthread_cond_wait(&cond, &mutex);   // c3 
        int tmp = get();                        // c4 
        Pthread_cond_signal(&cond);             // c5 
        Pthread_mutex_unlock(&mutex);           // c6
        printf("%d\n", tmp); 
    } 
}

相關文章