導讀:C語言國際標準新的新草案之前已經公布,新標準提高了對C++的兼容性,并將新的特性增加到C語言中。此外支持多線程的功能也受到了開發者的關注,基于ISO/IEC TR 19769:2004規范下支持Unicode,提供更多用于查詢浮點數類型特性的宏定義和靜態聲明功能。根據草案規定,最新發布的標準草案修訂了許多特性,支持當前的編譯器。(背景:C編程語言的標準化委員會(ISO/IEC JTC1/SC22/WG14)已完成了C標準的主要修訂,該標準的早期版本于1999年完成,俗稱為“C99”。新標準在去年年底完成,也被稱為“C11”。)
本文作者Tom Plum是Plum Hall Inc.的技術工程副總監,也是C11和C++11標準委員會的成員,他在這篇文章里從語言集的atomic操作和線程原語開始探討了C語言的新特性,在文章末尾還討論了C11與C++兼容性問題。此外,在本文和它的姊妹篇里,作者還描述了C11的新功能、并發性、安全性和易用性等話題。
并發
C11的標準化了可能運行在多核平臺上的多線程程序的語義,使用atomic變量讓線程間通信更輕量。
頭文件<threads.h>提供宏、類型以及支持多線程的函數。下面是宏、類型和枚舉常量的摘要總結:
??? 宏:
- thread_local,?ONCE_FLAG,?TSS_DTOR_ITERATIONS?cnd_t?thrd_t,?tss_t,?mtx_t,?tss_dtor_t,?thrd_start_t,?once_flag。?
??? 通過枚舉常量:
- mtx_init:?mtx_plain,?mtx_recursive,?mtx_timed。?
??? 線程枚舉常量:
- thrd_timedout,?thrd_success,?thrd_busy,?thrd_error,?thrd_nomem。?
??? 條件變量的函數:
- call_once(once_flag?*flag,?void?(*func)(void));?
- cnd_broadcast(cnd_t?*cond);?
- cnd_destroy(cnd_t?*cond);?
- cnd_init(cnd_t?*cond);?
- cnd_signal(cnd_t?*cond);?
- cnd_timedwait(cnd_t?*restrict?cond,?mtx_t?*restrict?mtx,?const?struct?timespec?*restrict?ts);?
- cnd_wait(cnd_t?*cond,?mtx_t?*mtx);?
??? 互斥函數:
- void?mtx_destroy(mtx_t?*mtx);?
- int?mtx_init(mtx_t?*mtx,?int?type);?
- int?mtx_lock(mtx_t?*mtx);?
- int?mtx_timedlock(mtx_t?*restrict?mtx;?
- const?struct?timespec?*restrict?ts);?
- int?mtx_trylock(mtx_t?*mtx);?
- int?mtx_unlock(mtx_t?*mtx);?
??? 線程函數:
- int?thrd_create(thrd_t?*thr,?thrd_start_t?func,?void?*arg);?
- thrd_t?thrd_current(void);?
- int?thrd_detach(thrd_t?thr);?
- int?thrd_equal(thrd_t?thr0,?thrd_t?thr1);?
- noreturn?void?thrd_exit(int?res);?
- int?thrd_join(thrd_t?thr,?int?*res);?
- int?thrd_sleep(const?struct?timespec?*duration,?struct?timespec?*remaining);?
- void?thrd_yield(void);?
??? 特定于線程的存儲功能:
- 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);?
這些標準庫函數可能更適合作為易用的API的基礎而不是開發平臺來使用。例如,使用這些低級庫的函數時,很容易造成數據競爭,多個進程會不同步地對同一個位置的數據進行操作。C(和C++)標準允許任何行為,即使會發生爭用同一個變量x,哪怕會導致嚴重的后果。例如,多字節值x可能被一個線程修改部分字節,而另一個線程又會修改別的部分(值撕裂),或者產生一些其它的副作用。
下面一個簡單的示例程序,它包含一個數據競爭,其中64位的整數x會同時被兩個線程改動。
- #include?<threads.h>?
- #include?<stdio.h>?
- #define?N?100000?
- char?buf1[N][99]={0},?buf2[N][99]={0};?
- long?long?old1,?old2,?limit=N;?
- long?long?x?=?0;?
- ??
- static?void?do1()??{?
- ???long?long?o1,?o2,?n1;?
- ???for?(long?long?i1?=?1;?i1?<?limit;?++i1)?{?
- ??????old1?=?x,?x?=?i1;?
- ??????o1?=?old1;??o2?=?old2;?
- ??????if?(o1?>?0)?{?//?x?was?set?by?this?thread?
- ?????????if?(o1?!=?i1-1)?
- ????????????sprintf(buf1[i1],?"thread?1:?o1=%7lld,?i1=%7lld,?o2=%7lld",?
- ?????????????????????o1,?i1,?o2);?
- ??????}?else?{??????//?x?was?set?by?the?other?thread?
- ?????????n1?=?x,?x?=?i1;?
- ?????????if?(n1?<?0?&&?n1?>?o1)?
- ????????????sprintf(buf1[i1],?"thread?1:?o1=%7lld,?i1=%7lld,?n1=%7lld",?
- ?????????????????????o1,?i1,?n1);?
- ??????}?????????
- ???}?
- }?
- ??
- static?void?do2()??{?
- ???long?long?o1,?o2,?n2;?
- ???for?(long?long?i2?=?-1;?i2?>?-limit;?--i2)?{?
- ??????old2?=?x,?x?=?i2;?
- ??????o1?=?old1;?o2?=?old2;?
- ??????if?(o2?<?0)?{?//?x?was?set?by?this?thread?
- ?????????if?(o2?!=?i2+1)?
- ????????????sprintf(buf2[-i2],?"thread?2:?o2=%7lld,?i2=%7lld,?o1=%7lld",?
- ?????????????????????o2,?i2,?o1);?
- ??????}?else?{??????//?x?was?set?by?the?other?thread?
- ?????????n2?=?x,?x?=?i2;?
- ?????????if?(n2?>?0?&&?n2?<?o2)?
- ????????????sprintf(buf2[-i2],?"thread?2:?o2=%7lld,?i2=%7lld,?n2=%7lld",?
- ?????????????????????o2,?i2,?n2);?
- ??????}?
- ???}?
- }?
- ??
- int?main(int?argc,?char?*argv[])?
- {?
- ???thrd_t?thr1;?
- ???thrd_t?thr2;?
- ???thrd_create(&thr1,?do1,?0);?
- ???thrd_create(&thr2,?do2,?0);?
- ???thrd_join(&thr2,?0);?
- ???thrd_join(&thr1,?0);?
- ???for?(long?long?i?=?0;?i?<?limit;?++i)?{?
- ??????if?(buf1[i][0]?!=?'\0')?
- ?????????printf("%s\n",?buf1[i]);?
- ??????if?(buf2[i][0]?!=?'\0')?
- ?????????printf("%s\n",?buf2[i]);?
- ???}?
- ???return?0;?
- }??
如果你的應用已經符合C11的標準,并且將它在一個32位的機器編譯過了(在64位機器里long long類型會占用兩倍以上的存儲周期),你將會看到數據競爭的結果,得到像下面亂碼一樣的輸出:
- thread?2:?o2=-4294947504,?i2=????-21,?o1=??19792?
傳統的解決方案是通過創建一個鎖來解決數據競爭。然而,用atomic數據有時會更高效。加載和存儲atomic類型循序漸進、始終如一。特別是如果線程1存儲了一個值名為x的atomic類型變量,線程2讀取該值時則可以看到之前在線程1中運行的所有其它存儲(即使是非atomic對象)。(C11和C++11標準還提供其他內存一致性的模型,雖然專家提醒要遠離它們。)
新的頭文件<stdatomic.h>提供了很多命名類和函數來操作atomic數據的大集。例如,atomic_llong就是一個為操作atomic long long整數的類型。所有的整數類都提供了相似的命名。該標準包括一個ATOMIC_VAR_INIT(n)宏,用來初始化atomic整數,如下:
之前的數據競爭的例子可以通過定義x為一個atomic_llong類型的變量解決。簡單地改變一下上述例子中聲明x的那行語句:
- #include?<stdatomic.h>?
- atomic_llong?x?=?ATOMIC_VAR_INIT(0);?
通過使用這樣的atomic變量,代碼在運行中不會出現任何數據競爭的問題。
注意關鍵字
C委員會并不希望在用戶命名空間里創建新的關鍵字,每個新的C版本都一直在避免出現不兼容之前版本C程序的問題。相比之下,C++委員會(WG21)更喜歡創造新的關鍵字,例如:C++11定義一個新的thread_local關鍵字來指定一個線程的本地靜態存儲,C11使用_Thread_local來代替,在C11新的頭文件<threads.h>中有一個宏定義來提供normal-looking name:
- #define???thread_local????_Thread_local?
下面我將假設你已經引入例如適當的頭文件,所以我會顯示normal-looking name。
thread_local存儲類
新thread_local存儲類為每個線程提供一個單獨的靜態存儲,并且在線程運行之前初始化。但有沒有保障措施來防止你捕獲一個thread_local變量的地址,并把它傳遞給其他線程,然后明確實現(即,不便攜/不可移植),每個線程在thread_local都有它自己的errno存儲副本。
線程可選
C11已指定為多種特色為可選功能,例如:如果它明確實現了一個名為 _ _STDC_NO_THREADS_ _ 的宏,就不會提供一個頭名為<threads.h>的頭文件,也不可能在其中定義任何函數。
設計準則
作為通則,WG21委托Bjarne Stroustrup整體設計和進化的責任,不明白的話可以在網上搜索“camel is a horse designed by committee”。然而,有一個WG14和WG21同樣遵循的設計原則:不要給任何系統編程語言比我們(C/C++)更高效的機會!
一些與會者(稱他們為“A組”)認為atomic數據仍將是一個很少使用的專業性功能,但其他人(稱他們為“B組”)認為,atomic數據將成為一個重要的功能,至少會在一種系統編程語言中被應用。
在過去的幾十年里,很多更高級別的語言都是基于C語言創建的(Java,C#,ObjectiveC,當然,也包括C++)和C++子集或超集(如D和嵌入式C++)。許多參與WG14和WG21的公司已經決定使用這些語言作為自己的應用的編程語言(稱他們為“C組”)。這些公司之所以選擇C++作為他們的上層應用程序的語言,通常是因為C足夠穩定(或者說是因為WG21控制其標準化),而選擇其他語言的公司(稱他們為“D組”)通常認為C是他們所使用的高級語言非常重要的基石。
有了這些背景,我可以得出一個C11中atomic進化的理由。C++11中對atomic的設計充分運用了模板。比如atomic<T>是獲得任何一種類型T的簡單且常用的方法。即使T是一個類或結構,那么無論T*指向哪兒,atomic<T*>都會保存類型信息,但是,幾年來,C語言的設計依然只用了幾十種命名類型(如上所示的atomic_llong)。這樣做的主要優點在于:它不需要編譯器做任何改變。它能夠實現一個僅庫的解決方案,會在最低水平調用系統依賴的原生函數。然而命名類型的解決方案干撓了為C結構或者T*指針創建atomic(無論多么小)。主要因為B組和D組的意見,WG14決定要求C11編譯器為任何類型的T識別atomicT。
后來也有一個WG14內部的關于編譯器識別atomicT語法的爭論。一種使用atomic-parenthesis的解決方案因為和C++良好的兼容性而被推動。Let _Atomic(T)成為了指定atomicT的語法。同樣的源程序能夠簡單地在C++定義宏中編譯。
- #define?_Atomic(T)????atomic<T>?
另一種相反的觀點認為應該創建新的類限制符(類似于C99的_Complex的解決方案);使用"atomic-space"語法,atomic T類型應該被寫為“_Atomic T”。使用這種語法的程序型的程序無法立刻被當作C++來編譯。(無兼容性宏,本質上看起來像atomic-parenthesis的方法)。
獲取C11標準
新標準可以在webstore.ansi.org查看(或者搜索"ISO/IEC 9899:2011")。現在已經有了PDF文檔,但是需要支付$285(和C++2011一樣)才可以使用。一旦這些標準被ANSI采納為美國國家標準,價格將下降至$30左右。
你可以定期檢查此頁面的部分,我及時檢查草案的狀態,如果您填寫了這個Web表單,將會在C11和C++11被ANSI采用為標準時獲得電子郵件通知。也可以通過tplum@plumhall.com聯系我,非常感謝來自Pete Becker(C++2011標準的項目編輯)的建設性建議。
原文鏈接:drdobbs.com
更多關于C11的變更可以參考維基百科
相關文章:
ISO發布C語言標準新版本
C語言中史上最愚蠢的Bug
如何創建比C語言更快的編程語言?