線程管理
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
互斥對象進行解鎖操作,然後阻塞,直到條件變量cond
被cnd_signal
或cnd_broadcast
調用喚醒,當前線程變為非阻塞時,它將在返回之前鎖住mtx
互斥對象。cnd_timedwait
函數與cnd_wait
類似,例外之處是當前線程在xt
時間點上還未能被喚醒時,它將返回,此時返回值為thrd_timeout
。cnd_wait
和cnd_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_once
,func
均不會再次被調用,以保正func
在多線程環境只被調用一次。
線程專有數據(TSD
) 和線程局部數據 (TLS
)
在多線程開發中,並不是所有的同步都需要加鎖的,有時巧妙的數據分解也可減少鎖的碰撞。每個線程都擁有自己私有數據,使用它可以減少線程間共享數據之間的同步開銷。
如果要將一些遺留代碼進行線程化,很多函數都使用了全局變量,而在多線程環下,最好的方法可能是將這些全局量變量換成線程私有的全局變量即可。
TSD
和TLS
就是專門用來處理線程私有數據的。 它的生存週期是整個線程的生存週期,但它在每個線程都有一份拷貝,每個線程只能read-write-update
屬於自己的那份。如果通過指針方式來read-write-update
其它線程的備份,它的行為是未定義的。
TSD
可認為線程私有內存下的void *
組數,每個數據項的key
對應於數組的下標,用於索引功能。當一個新線程創建時,線程的TSD
區域將所有key
關聯的值設置為NULL
。TSD
是通過函數的方式來操作的。C11
中TSD
提供的標準函數如下:
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
函數創建一個key
,dtor
為該key
將要關聯value
的析構函數。當線程退出時,會調用dtor
函數來釋放該key
關聯的value
所佔用的資源,當然,如果退出時value
值為NULL
,dtor
將不被調用。tss_delete
函數刪除一個key
,tss_get
/tss_set
分別獲得或設置該key
所關聯的value
。
通過上述TSD
來操作線程私有變量的方式,顯得相對繁瑣; C11提供了TLS
方法,可以像一般變量的方式去訪問線程私有變量。做法很簡單,在聲明和定義線程私變量時指定_Thread_local
存儲修飾符即可,關於_Thread_local
,C11 有如下的描述:
- 在聲明式中,
_Thread_local
只能單獨使用,或者跟static
或extern
一起使用。 - 在某一區快中聲明某一對象,如果聲明存儲修飾符有
_Thread_local
,那麼必須同時有static
或extern
。 - 如果
_Thread_local
出現在一對象的某個聲明式中,那麼此對象的其餘各處聲明式都應該有_Thread_local
存儲修飾符。 - 如果某一對象的聲明式中出現
_Thread_local
存儲修飾符,那麼它有線程儲存期。該對象的生命週期為線程的整個執行週期,它在線程出生時創建,並在線程啓動時初始化。每個線程均有一份該對象,使用聲明時的名字即可引用正在執行當前表達式的線程所關聯的那個對象。
TLS
方式與傳統的全局變量或static
變量的使用方式完全一致,不同的是,TLS
變量在不同的線程上均有各自的一份。線程訪問TLS
時不會產生data race
,因為不需要任何加鎖機制。TLS
方式需要編譯器的支持,對於任何_Thread_local
變量,編譯器要將之編譯並生成放到各個線程的private memory
區域,並且訪問這些變量時,都要獲得當前線程的信息,從而訪問正確的物理對象,當然這一切都是在鏈接過程早已安排好的。