C11新增多執行緒支援庫-threads.h參考手冊

執行緒管理

int thrd_create(thrd_t *thr,  thrd_start_t func,  void *arg);

thrd_create建立一個新執行緒,該執行緒的工作就是執行func(arg)呼叫,程式設計師需要為執行緒編寫一個函式,函式簽名為:thrd_start_t ,即int (*)(void*)型別的函式。新建立的執行緒的識別符號存放在thr內。

thrd_t thrd_current(void);

thrd_current函式返回撥用執行緒的識別符號。

int thrd_detach(thrd_t thr);

thrd_detach會通知作業系統,當該執行緒結束時由作業系統負責回收該執行緒所佔用的資源。

int thrd_equal(thrd_t thr0, thrd_t thr1);

thrd_equal用於判斷兩個執行緒識別符號是否相等(即標識同一執行緒),thrd_t是標準約定的型別,可能是一個基礎型別,也可能會是結構體,開發人員應該使用thrd_equal來判斷兩者是否相等,不能直接使用==。即便==在某個平臺下表現出來是正確的,但它不是標準的做法,也不可跨平臺。

void thrd_exit(int res)

thrd_exit函式提早結束當前執行緒,res為它的退出狀態碼。這與程序中的exit函式類似。

int thrd_join(thrd_t thr, int *res)

thrd_join將阻塞當前執行緒,直到執行緒thr結束時才返回。如果res非空,那麼res將儲存thr執行緒的結束狀態碼。如果某一執行緒內沒有呼叫thrd_detach函式將自己設定為detach狀態,那麼當它結束時必須由另外一個執行緒呼叫thrd_join函式將它留下的僵死狀態變為結束,並回收它所佔用的系統資源。

void thrd_sleep(const xtime *xt)

thrd_sleep函式讓當前執行緒中途休眠,直到由xt指定的時間過去後才醒過來。

void thrd_yield(void)

thrd_yield函式讓出CPU給其它執行緒或程序。

互斥物件和函式

threads.h中提供了豐富的互斥物件,使用者只需在mtx_init初始化時,指定該互斥物件的型別即可。

int mtx_int(mtx_t  *mtx, int type);

mtx_init函式用於初始化互斥物件,type決定互斥物件的型別,一共有下面6種型別:

  • mtx_plain –簡單的,非遞迴互斥物件
  • mtx_timed –非遞迴的,支援超時的互斥物件
  • mtx_try –非遞迴的,支援鎖檢測的互斥物件
  • mtx_plain | mtx_recursive –簡單的,遞迴互斥物件
  • mtx_timed | mtx_recursive –支援超時的遞迴互斥物件
  • mtx_try | mtx_recursive –支援鎖檢測的遞迴互斥物件
int mtx_lock(mtx_t *mtx)
int mtx_timedlock(mtx_t *mtx, const xtime *xt)
int mtx_trylock(mtx_t *mtx)

mtx_xxxlock函式對mtx互斥物件進行加鎖 , 它們會阻塞,直到獲取鎖,或者xt指定的時間已過去。而trylock版本會進行鎖檢測,如果該鎖已被其它執行緒佔用,那麼它馬上返回thrd_busy

int mtx_unlock(mtx_t *mtx)

mtx_unlock對互斥物件mtx進行解鎖。

條件變數

threads.h透過mtx物件和條件變數來實現wait-notify機制。

int cnd_init(cnd_t *cond)

初始化條件變數,所有條件變數必須初始化後才能使用。

int cnd_wait(cnd_t *cond, mtx_t *mtx)
int cnd_timedwait(cnd_t *cond, mtx_t *mtx, const xtime *xt)

cnd_wait函式自動對mtx互斥物件進行解鎖操作,然後阻塞,直到條件變數condcnd_signalcnd_broadcast呼叫喚醒,當前執行緒變為非阻塞時,它將在返回之前鎖住mtx互斥物件。cnd_timedwait函式與cnd_wait類似,例外之處是當前執行緒在xt時間點上還未能被喚醒時,它將返回,此時返回值為thrd_timeoutcnd_waitcnd_timedwait函式在被呼叫前,當前執行緒必須鎖住mtx互斥物件。

int cnd_signal(cnd_t *cond)
int cnd_broadcast(cnd_t *cond)

cnd_broadcast喚醒那些當前已經阻塞在cond條件變數上的所有執行緒,而cnd_signal只喚醒其中之一。

void cnd_destroy(cnd_t *cond)

銷燬條件變數。

初始化函式

試想一下,如何在一個多執行緒同時執行的環境下來初始化一個變數,即著名的延遲初始化單例模式。你可能會使用DCL技術。但在C11中,你可以直接使用call_once函來實現。

void call_once(once_flag *flag, void (*func)(void))

call_once函式使用flag來保確func只被呼叫一次。第一個執行緒使用flag去呼叫call_once時,函式func會被呼叫,而接下來的使用相同flag來呼叫的call_oncefunc均不會再次被呼叫,以保正func在多執行緒環境只被呼叫一次。

執行緒專有資料(TSD) 和執行緒區域性資料 (TLS)

在多執行緒開發中,並不是所有的同步都需要加鎖的,有時巧妙的資料分解也可減少鎖的碰撞。每個執行緒都擁有自己私有資料,使用它可以減少執行緒間共享資料之間的同步開銷。

如果要將一些遺留程式碼進行執行緒化,很多函式都使用了全域性變數,而在多執行緒環下,最好的方法可能是將這些全域性量變數換成執行緒私有的全域性變數即可。

TSDTLS就是專門用來處理執行緒私有資料的。 它的生存週期是整個執行緒的生存週期,但它在每個執行緒都有一份複製,每個執行緒只能read-write-update屬於自己的那份。如果透過指標方式來read-write-update其它執行緒的備份,它的行為是未定義的。

TSD可認為執行緒私有記憶體下的void *組數,每個資料項的key對應於陣列的下標,用於索引功能。當一個新執行緒建立時,執行緒的TSD區域將所有key關聯的值設定為NULLTSD是透過函式的方式來操作的。C11TSD提供的標準函式如下:

int tss_create(tss_t *key,  tss_dtor_t dtor)
void tss_delete(tss_t key)
void *tss_get(tss_t key)
int tss_set(tss_t key, void *val)

tss_create函式建立一個keydtor為該key將要關聯value的解構函式。當執行緒退出時,會呼叫dtor函式來釋放該key關聯的value所佔用的資源,當然,如果退出時value值為NULLdtor將不被呼叫。tss_delete函式刪除一個keytss_get/tss_set分別獲得或設定該key所關聯的value

透過上述TSD來操作執行緒私有變數的方式,顯得相對繁瑣; C11提供了TLS方法,可以像一般變數的方式去訪問執行緒私有變數。做法很簡單,在宣告和定義執行緒私變數時指定_Thread_local儲存修飾符即可,關於_Thread_local,C11 有如下的描述:

  1. 在宣告式中,_Thread_local只能單獨使用,或者跟staticextern一起使用。
  2. 在某一區快中宣告某一物件,如果宣告儲存修飾符有_Thread_local,那麼必須同時有staticextern
  3. 如果_Thread_local出現在一物件的某個宣告式中,那麼此物件的其餘各處宣告式都應該有_Thread_local儲存修飾符。
  4. 如果某一物件的宣告式中出現_Thread_local儲存修飾符,那麼它有執行緒儲存期。該物件的生命週期為執行緒的整個執行週期,它線上程出生時建立,並線上程啟動時初始化。每個執行緒均有一份該物件,使用宣告時的名字即可引用正在執行當前表示式的執行緒所關聯的那個物件。

TLS方式與傳統的全域性變數或static變數的使用方式完全一致,不同的是,TLS變數在不同的執行緒上均有各自的一份。執行緒訪問TLS時不會產生data race,因為不需要任何加鎖機制。TLS方式需要編譯器的支援,對於任何_Thread_local變數,編譯器要將之編譯並生成放到各個執行緒的private memory區域,並且訪問這些變數時,都要獲得當前執行緒的資訊,從而訪問正確的物理物件,當然這一切都是在連結過程早已安排好的。

以下列出本文參考的資料,在此向原作者致敬。

使用C11新增的多執行緒支援庫-threads.h進行多執行緒程式設計

2019年12月6日更新

首先要感謝評論區的熱心的同學們的提醒,經本人親自驗證:

VS 2019中移除了對threads.h的支援(評論區有同學提到單獨安裝v140工具集可以實現支援,但是測試發現沒有效果),而GCC方面則在最新版中加入了對該標頭檔案的支援。

以下為原文(程式程式碼部分更新了Linux版Dome)。


導語

threads.h是C11標準新增的多執行緒支援庫,在此之前C語言實現多執行緒,除了使用系統API外用的最多的就是pthread.h了,threads.h在語法上和pthread.h非常相似。
當然,對於新出的C語言標準,各大編譯器廠商並不會馬上就支援。就比如說,Linux下主流的C語言編譯器————GCC,直到GCC7.2版本都沒能支援該庫(PS:看到老外網站上說要安裝最新版glibc才能獲得對該庫的支援,然而親測無卵用)。相反,VS在這方面做的就很不錯,VS2017已經可以完美支援該庫了,本文也將基於VS2017社羣版對該庫的使用方法做介紹。

注:本文僅對多執行緒程式設計的概念及threads.h庫檔案的使用方法做簡單介紹,並不會詳盡介紹該庫下的所有函式,如果你需要一個函式功能的參考手冊可以參考此篇文章:C11新增多執行緒支援庫-threads.h參考手冊

以一個小程式為例子

本程式中使用到的庫函式及宏:

  • thrd_t //此宏定義用於存放執行緒識別符號的資料型別
  • thrd_create //此函式用於建立執行緒
  • thrd_detach //此函式用於通知作業系統,當執行緒結束時由作業系統負責釋放資源
  • thrd_exit //此函式用於結束當前執行緒

程式功能:

主執行緒每2秒列印一次“I love ibadboy.net~~~”,共列印10次。子執行緒每1秒列印一次“He love ibadboy.net!!!”,共列印10次。我們知道,在只有一個主執行緒的C程式中該功能是無法實現的,因為後一段程式程式碼必須等待前一段程式碼執行完畢才可執行。但,在多執行緒程式設計中,各個執行緒可以一起執行(這裡涉及到的同步、非同步等等的高階技術就不討論了)。舉個例子:在遊戲開發中,如果程式需要實時監控使用者鍵盤的輸入,就不能把這段程式碼放到主執行緒中,因為這樣的話該段程式碼就會被程式的其他部分阻塞掉而無法做到真正的“實時”,這時就可以利用多執行緒技術來化解尷尬啦!話不多說,直接上程式碼!

程式程式碼Linux版本

#include <stdio.h>
#include <unistd.h>	//包含sleep等函式
#include <stdbool.h>
#include <threads.h>	//包含多執行緒支援庫標頭檔案
#include <stdlib.h>	//包含exit等函式

int thr_fun(void *);

int main(void) {
    thrd_t thr;
    int ret; //儲存thrd_create函式的返回值用於判斷執行緒是否建立成功:0為成功,1為失敗。
    ret = thrd_create(&thr, thr_fun, NULL); //將thr_fun函式放在一個新的執行緒中執行
    if (ret != thrd_success) {
        printf("error!!!\n");
        getchar();
        exit(-1);
    }
    ret = thrd_detach(thr); //通知作業系統,該執行緒結束時由作業系統負責釋放資源。
    if (ret != thrd_success) {
        printf("error!!!\n");
        getchar();
        exit(-1);
    }
    for (int i = 0; i < 10; i++) {
        sleep(2);
        printf("I love ibadboy.net~~~\n");
    }
    getchar();
    return 0;
}

int thr_fun(void *argv) {
    int i = 0;
    while (true) {
        i++;
        sleep(1);
        printf("He love ibadboy.net!!!\n");
        if (i == 10) {
            thrd_exit(0);
        }
    }
}

編譯命令:

gcc a.c -std=c11 -lpthread

程式程式碼Windows版本(經測試在VS 2019中已無法編譯透過):

#include<stdio.h>
#include<stdbool.h>
#include<thr/threads.h>        //包含多執行緒支援庫標頭檔案
#include<Windows.h>
void thr_fun(void);
int main(void) {
    thrd_t thr;
    int ret;    //儲存thrd_create函式的返回值用於判斷執行緒是否建立成功:0為成功,1為失敗。
    ret = thrd_create(&thr, thr_fun, NULL);        //將thr_fun函式放在一個新的執行緒中執行
    if (ret != thrd_success) {
        printf("error!!!\n");
        getchar();
        exit(-1);
    }
    ret = thrd_detach(thr);    //通知作業系統,該執行緒結束時由作業系統負責釋放資源。
    if (ret != thrd_success) {
        printf("error!!!\n");
        getchar();
        exit(-1);
    }
    for (int i = 0; i < 10; i++) {
        Sleep(2000);
        printf("I love ibadboy.net~~~\n");
    }
    getchar();
    return 0;
}
void thr_fun(void) {
    int i = 0;
    while (true) {
        i++;
        Sleep(1000);
        printf("He love ibadboy.net!!!\n");
        if (i == 10) {
            thrd_exit(0);
        }
    }
}

程式輸出:

C語言多執行緒演示小程式的程式碼輸出

程式中用到的庫函式介紹:

thrd_create函式用於建立新執行緒,如果建立成功,該函式會返回thrd_success,否則返回thrd_error

函式原型:

int thrd_create(thrd_t *thr, thrd_start_t func, void *arg);

引數說明:

  • thr:指向放置新執行緒識別符號的記憶體位置的指標。
  • func:要放在子執行緒中執行的函式。
  • arg:傳遞給執行的函式的引數,無引數填NULL。

thrd_detach函式用於通知作業系統,當執行緒結束後由作業系統負責釋放資源,如果成功,則返回thrd_success,否則為thrd_error,如不呼叫該函式,則執行緒所使用的資源將在程式全部執行完後才會釋放。

函式原型:

int thrd_detach( thrd_t thr );

引數說明:

  • thr:要作用的執行緒的識別符號

thrd_exit函式用於結束當前程序,值得一提的是:使用該函式可以在不影響子程序的情況下結束主程序,而使用exit函式的話結束主程序將會連帶結束整個程式。

函式原型:

_Noreturn void thrd_exit( int res );

引數說明:

  • res:要返回的值

結語

看完此篇文章相信你已經對使用threads.h庫進行C語言的多執行緒開發有了初步瞭解,如果還有什麼問題的話歡迎在評論區留言哦~