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語言的多線程開發有了初步瞭解,如果還有什麼問題的話歡迎在評論區留言哦~