C1X 係列 : 多線程 (N1494)
1. 關於 C1X 標準
C1X 是 C 語言的下一個標準,用於取代現有的 C99 標準。 C1X 是一個非正式名字,該標準仍在製訂中,最新的工作草案是 N1494 ,發布於 2010 年 6 月。與 C99 相比, C1X 在語言和庫上有顯著的變化,本文重點分析 N1494 草案中的多線程部分。
2. 唿之欲出的多線程
不瞞你說, C99 標準裏麵的內存模型仍然是單線程的,即所有代碼都運行在一個線程(進程)內。也許,你簡直不敢相信這個是真的,因為說不定你每天都與多線程打交道, 使用 _beginthread , CreateThread 或 pthread_create 等不同平台的函數去創建線程。當你擔心每個變量被編譯器優化時,不得不加上 volatile 修飾符時,或者加上內存柵欄 (memory barrier) ,都印證了 C99 為單線程的內存模型。
32 位保護模式的出現,催生了多任務操作係統的誕生,隨之而來的就是多線程環境。隨著多核時代的到來,多線程環境成為程序開發不可逃避的問題。無鎖編程,並行 編程等已經成熟的技術在 C 社區裏,常常給初學者遙不可及的感覺。在迫切需要多線程的時代,不同廠商和平台都紛紛開發了各自的多線程庫,如 POSIX 的 pthreads 和 window 的 winThread 。軟件工程師在特定的平台下,使用各自的線程庫來開發多線程或並發程序,並非很難,但可移植卻成了他們麵臨的問題。如 pthreads 在默認情況下,互斥鎖是非遞歸的,要使用遞歸互斥鎖,程序庫必須支持一些擴展 feature ;而 window 下的鎖默認情況下是遞歸的。 lock-wait-wakup 方式也不盡然相同,這對開發跨平台的多線程程序來說無異於雪上加霜。
放眼 C 語言的後來者,如 Java ,很早就支持了多線程,並且它的內存模型在不斷地修改,以適應發揮多線程的優勢。 Erlang 作為一種天生的並發編程語言,踏上編程語言的行列,成為並發編程的新星。同時,很多腳本語都內置地支持線程類或結構。 C/C++ 作為後來加入到多線程的行列,盡管已經太晚了,但對於 C 語言社區來說,這無疑是最令人興奮的消息了。
3. C1X 多線程接口與語義
如果你有 pthreads 的開發經驗,應該對 C1X threading 不會感到陌生,因為它們在 API ,參數和語義方麵都驚人地一致,以致於你學會了 pthreads ,基本就學會了大半個 C1X threading 了。相反,對於 winthread 的開發人員來說,需要換一個角度來看等多線程,特別是 cnd_wait 和 cnd_signal 之間的 time race 關係。好吧,讓我們一覽目前最新的 C1X threading 編程吧。
3.1 線程管理
int thrd_create(thrd_t *thr, thrd_start_t func, void *arg);
thrd_create 創建一個新線程,該線程的工作就是執行 func(arg) 調用,程序員需要為線程編寫一個函數,函數簽名為: thrd_start_t ,即 int (*)(void*) 類型的函數。新創建的線程的標識符存放在 thr 內。
與 pthread_create 函數相比, thrd_create 函數沒有線程屬性這一參數,並具線程函數的返回值是 int ,而非 pthreads 的 void * 。這一特點與進程的返回值一致,都是使用整數表示一個任務的結束狀態。
thrd_t thrd_current(void);
thrd_current 函數返回調用線程的標識符。類似於 pthreads 下的 pthread_self() 函數。
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 給其它線程或進程。
3.2 互斥對象和函數
C1X threading 中提供了豐富的互斥對象,用戶隻需 mtx_init 初始化時,指定該互斥對象的類型即可,如遞歸的,支持 timeout 和,或者支持鎖檢測。
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 進行解鎖。
3.3 條件變量
C1X 中的條件變量與 pthreads 中的條件變量是一樣的, C1X 通過 mtx 對象和條件變量來實現 wait-notify 機製,這與 Java 語言裏 Object 對象中的 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)
cnd_destroy函數用於銷毀條件變量。
3.4 初始化函數
試想一下,如何在一個多線程同時執行的環境下來初始化一個變量,即著名的延遲初始化單例模式。你可能會使用 DCL 技術。但在 C1X threading 環境下,你可以直接使用 call_once 函來實現。
void call_once(once_flag *flag, void (*func)(void))
call_once 函數使用 flag 來保確 func 隻被調用一次。第一個線程使用 flag 去調用 call_once 時,函數 func 會被調用,而接下來的使用相同 flag 來調用的 call_once , func 均不會再次被調用,以保正 func 在多線程環境隻被調用一次。
3.5 線程專有數據 (thread-specific data, TSD) 和線程局部數據 (thread-local storage, TLS)
在多線程開發中,並不是所有的同步都需要加鎖的,有時巧妙的數據分解也可減少鎖的碰撞。每個線程都擁有自己私有數據,使用它可以減少線程間共享數據之間的同步開銷。
如果要將一些遺留代碼進行線程化,很多函數都使用了全局變量,而在多線程環下,最好的方法可能是將這些全局量變量換成線程私有的全局變量即可。
TSD 和 TLS 就是專門用來處理線程私有數據的。 它的生存周期是整個線程的生存周期,但它在每個線程都有一份拷貝,每個線程隻能 read-write-update 屬於自己的那份。如果通過指針方式來 read-write-update 其它線程的備份,它的行為是未定義的。
C1X 同時提供了 TSD 和 TLS 特性,而 pthreads 隻提供 TSD ,但在 linux 下的 gcc 編譯器提供了 TLS 作為擴展特性。 TSD 可認為線程私有內存下的 void * 組數,每個數據項的 key 對應於數組的下標,用於索引功能。當一個新線程創建時,線程的 TSD 區域將所有 key 關聯的值設置為 NULL 。 TSD 是通過函數的方式來操作的。 C1X 中 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 來操作線程私有變量的方式,顯得相對繁瑣 ; C1X 提供了 TLS 方法,可以像一般變量的方式去訪問線程私有變量。做法很簡單,在聲明和定義線程私變量時指定 _Thread_local 存儲修飾符即可,關於 _Thread_local , C1X 有如下的描述:
在聲明式中,_Thread_local 隻能單獨使用,或者跟 static 或 extern 一起使用。
在某一區塊( block scope) 中聲明某一對象,如果聲明存儲修飾符有 _Thread_local ,那麼必須同時有 static 或 extern 。
如果 _Thread_local 出現在一對象的某個聲明式中,那麼此對象的其餘各處聲明式都應該有 _Thread_local 存儲修飾符。
如果某一對象的聲明式中出現 _Thread_local 存儲修飾符,那麼它有線程儲存期( thread storage duration
)。該對象的生命周期為線程的整個執行周期,它在線程出生時創建,並在線程啟動時初始化。每個線程均有一份該對象,使用聲明時的名字即可引用正在執行當前
表達式的線程所關聯的那個對象。
TLS 方式與傳統的全局變量或 static 變量的使用方式完全一致,不同的是, TLS 變量在不同的線程上均有各自的一份。線程訪問 TLS 時不會產生 data race ,因為不需要任何加鎖機製。 TLS 方式需要編譯器的支持,對於任何 _Thread_local 變量,編譯器要將之編譯並生成放到各個線程的 private memory 區域,並且訪問這些變量時,都要獲得當前線程的信息,從而訪問正確的物理對象,當然這一切都是在鏈接過程早已安排好的。
4. C1X threading 的未來
C1X threading 的整體設計與 pthreads 的驚人地一致,我甚至懷疑它們是出自一人(團隊)之手。我最初接觸多線程編譯中的等待 – 通知原語是從 Java 中 Object 對象裏的 wait 和 notify 函數中獲得感性認識,然後在工作中將 pthreads 中的 pthread_cond_wait 和 pthread_cond_signal 函數應用於實際工作中,並解決了很多實際問題,如編寫線程安全的數據結構。現在最新的 C1Xthreading 標準,線程等待 – 通知的方式與上述兩者如同一轍。
與 pthreads 相比, C1X threading 沒有了信號量操作,讀寫鎖和自旋鎖。顯然開發人員可以借用 C1X threading 中提供的同步機製來實現信號量和讀寫鎖,但要實現自旋鎖是比較難,這需要深入了解所在平台和操作係統提供的原語,在此基礎上再實現自旋鎖。
對於 window 開發者來說, C1X threading 是一個新的標準,裏麵提供的同步原語與 winthread 相比,顯得有點冷清。不過 window 下的開發人員通常都使用 C++ 或 MFC 提供的多線程庫來開發,隻是對於那些完全使用 C語言來開始跨平台多線程程序的同行來說,隻能完全遵循 C1X threading 標準了。
C1X threading 以內存共享模型作為多線程編程模型,提供的同步機製基於鎖來實現。將來是否會提供係統級別的,基於消息傳遞來實現無鎖同步。
5. 總結
多線程和多核開發時代已悄悄降臨在我們的身邊,無論你現在的開發工作是否與並行開發相關, C1X threading 都應該成為你手中的一把利器。 C1X threading 了卻你心中很多疑慮,可移植, TSD/TLS 之抉擇等。
文章轉自 並發編程網-ifeve.com
最後更新:2017-05-22 16:37:11