相信有很多同學(xué)在面對(duì)多線程代碼時(shí)都會(huì)望而生畏,認(rèn)為多線程代碼就像一頭難以馴服的怪獸,你制服不了這頭怪獸它就會(huì)反過來吞噬你。
夸張了哈,總之,多線程程序有時(shí)就像一潭淤泥,走不進(jìn)去退不出來。可這是為什么呢?為什么多線程代碼如此難以正確編寫呢?
從根源上思考
關(guān)于這個(gè)問題,本質(zhì)上是有一個(gè)詞語你沒有透徹理解,這個(gè)詞就是所謂的線程安全,thread safe。如果你不能理解線程安全,那么給你再多的方案也是無用武之地。接下來我們了解一下什么是線程安全,怎樣才能做到線程安全。這些問題解答后,多線程這頭大怪獸自然就會(huì)變成溫順的小貓咪。
可上圖關(guān)小貓咪屁事!
關(guān)你什么屁事
生活中我們口頭上經(jīng)常說的一句話就是“關(guān)你屁事”,大家想一想,為什么我們的屁事不關(guān)別人?原因很簡(jiǎn)單,這是我的私事??!我的衣服、我的電腦,我的手機(jī)、我的車子、我的別墅以及私人泳池(可以沒有,但不妨礙想象),我想怎么處理就怎么處理,妨礙不到別人,只屬于我一個(gè)人的東西以及事情當(dāng)然不關(guān)別人,即使是屁事也不關(guān)別人。 我們?cè)谧约杭依锵氤允裁闯允裁?,想去廁所就去廁所!因?yàn)檫@些都是我私有的,只有我自己使用。那么,什么時(shí)候會(huì)和其它人有交集呢?答案就是公共場(chǎng)所。在公共場(chǎng)所下你不能像在自己家里一樣想去哪就去哪,想什么時(shí)候去廁所就去廁所,為什么呢?原因很簡(jiǎn)單,因?yàn)楣矆?chǎng)所下的飯館、衛(wèi)生間不是你家的,這是公共資源,大家都可以使用的公共資源。如果你想去飯館、去公共衛(wèi)生間那么就必須遵守規(guī)則,這個(gè)規(guī)則就是排隊(duì),只有前一個(gè)人用完公共資源后下一個(gè)人才可以使用,而且不能同時(shí)使用,想使用就必須排隊(duì)等待。上面這段話道理足夠簡(jiǎn)單吧。
如果你能理解這段話,那么馴服多線程這頭小怪獸就不在話下。
維護(hù)公共場(chǎng)所秩序
如果把你自己理解為線程的話,那么在你自己家里使用私有資源就是所謂的線程安全,原因很簡(jiǎn)單,因?yàn)?strong>你隨便怎么折騰自己的東西(資源)都不會(huì)妨礙到別人;但到公共場(chǎng)所浪的話就不一樣了,在公共場(chǎng)所使用的是公共資源,這時(shí)你就不能像在自己家里一樣想怎么用就怎么用想什么時(shí)候用就什么時(shí)候用,公共場(chǎng)所必須有相應(yīng)規(guī)則,這里的規(guī)則通常是排隊(duì),只有這樣公共場(chǎng)所的秩序才不會(huì)被破壞,線程以某種不妨礙到其它線程的秩序使用共享資源就能實(shí)現(xiàn)線程安全。 因此我們可以看到,這里有兩種情況:
-
線程私有資源,沒有線程安全問題
-
共享資源,線程間以某種秩序使用共享資源也能實(shí)現(xiàn)線程安全。
本文都是圍繞著上述兩個(gè)核心點(diǎn)來講解的,現(xiàn)在我們就可以正式的聊聊編程中的線程安全了。
什么是線程安全
我們說一段代碼是線程安全的,當(dāng)且僅當(dāng)我們?cè)诙鄠€(gè)線程中同時(shí)且多次調(diào)用的這段代碼都能給出正確的結(jié)果,這樣的代碼我們才說是線程安全代碼,Thread Safety,否則就不是線程安全代碼,thread-unsafe.。非線程安全的代碼其運(yùn)行結(jié)果是由擲骰子決定的。
怎么樣,線程安全的定義很簡(jiǎn)單吧,也就是說你的代碼不管是在單個(gè)線程還是多個(gè)線程中被執(zhí)行都應(yīng)該能給出正確的運(yùn)行結(jié)果,這樣的代碼是不會(huì)出現(xiàn)多線程問題的,就像下面這段代碼:
int func() {
int a = 1;
int b = 1;
return a + b;
}
對(duì)于這樣段代碼,無論你用多少線程同時(shí)調(diào)用、怎么調(diào)用、什么時(shí)候調(diào)用都會(huì)返回2,這段代碼就是線程安全的。
那么,我們?cè)撛鯓訉懗鼍€程安全的代碼呢?要回答這個(gè)問題,我們需要知道我們的代碼什么時(shí)候呆在自己家里使用私有資源,什么時(shí)候去公共場(chǎng)所浪使用公共資源,也就是說你需要識(shí)別線程的私有資源和共享資源都有哪些,這是解決線程安全問題的核心所在。
線程私有資源
線程都有哪些私有資源呢?啊哈,我們?cè)谏弦黄?a href="http://wenjunhu.com/outside?redirect=http://mp.weixin.qq.com/s?__biz=MzU2NTYyOTQ4OQ==&mid=2247485059&idx=1&sn=c798550fb3eda73c6f66440659bdcd8d&chksm=fcb981ddcbce08cb74b6ea9412720ce827231c2e67e57811dbfcaac78c0e7af55e0596b72a7f&scene=21#wechat_redirect" target="_blank">線程到底共享了哪些進(jìn)程資源》中詳細(xì)講解了這個(gè)問題。線程運(yùn)行的本質(zhì)其實(shí)就是函數(shù)的執(zhí)行,函數(shù)的執(zhí)行總會(huì)有一個(gè)源頭,這個(gè)源頭就是所謂的入口函數(shù),CPU從入口函數(shù)開始執(zhí)行從而形成一個(gè)執(zhí)行流,只不過我們?nèi)藶榈慕o執(zhí)行流起一個(gè)名字,這個(gè)名字就叫線程。既然線程運(yùn)行的本質(zhì)就是函數(shù)的執(zhí)行,那么函數(shù)運(yùn)行時(shí)信息都保存在哪里呢?答案就是棧區(qū),每個(gè)線程都有一個(gè)私有的棧區(qū),因此在棧上分配的局部變量就是線程私有的,無論我們?cè)鯓邮褂眠@些局部變量都不管其它線程屁事。
線程私有的棧區(qū)就是線程自己家。
線程間共享數(shù)據(jù)
除了上一節(jié)提到的剩下的區(qū)域就是公共場(chǎng)合了,這包括:
-
用于動(dòng)態(tài)分配內(nèi)存的堆區(qū),我們用C/C++中的malloc或者new就是在堆區(qū)上申請(qǐng)的內(nèi)存
-
全局區(qū),這里存放的就是全局變量
-
文件,我們知道線程是共享進(jìn)程打開的文件
有的同學(xué)可能說,等等,在上一篇文章不是說還有代碼區(qū)和動(dòng)態(tài)鏈接庫嗎?要知道這兩個(gè)區(qū)域是不能被修改的,也就是說這兩個(gè)區(qū)域是只讀的,因此多個(gè)線程使用是沒有問題的。在剛才我們提到的堆區(qū)、數(shù)據(jù)區(qū)以及文件,這些就是所有的線程都可以共享的資源,也就是公共場(chǎng)所,線程在這些公共場(chǎng)所就不能隨便浪了。線程使用這些共享資源必須要遵守秩序,這個(gè)秩序的核心就是對(duì)共享資源的使用不能妨礙到其它線程,無論你使用各種鎖也好、信號(hào)量也罷,其目的都是在維護(hù)公共場(chǎng)所的秩序。知道了哪些是線程私有的,哪些是線程間共享的,接下來就簡(jiǎn)單了。值得注意的是,關(guān)于線程安全的一切問題全部圍繞著線程私有數(shù)據(jù)與線程共享數(shù)據(jù)來處理,抓住了線程私有資源和共享資源這個(gè)主要矛盾也就抓住了解決線程安全問題的核心。
接下來,我們看一下在各種情況下該怎樣實(shí)現(xiàn)線程安全。這里依然以C/C++代碼為例,但是這里講解的方法適用于任何語言,請(qǐng)放心,這些代碼足夠簡(jiǎn)單。
只使用線程私有資源
我們來看這段代碼:
這段代碼在前面提到過,無論你在多少個(gè)線程中怎么調(diào)用什么時(shí)候調(diào)用,func函數(shù)都會(huì)確定的返回2,該函數(shù)不依賴任何全局變量,不依賴任何函數(shù)參數(shù),且使用的局部變量都是線程私有資源,這樣的代碼也被稱為無狀態(tài)函數(shù),stateless,很顯然這樣的代碼是線程安全的。int func() {
int a = 1;
int b = 1;
return a + b;
}
這樣的代碼請(qǐng)放心大膽的在多線程中使用,不會(huì)有任何問題。
有的同學(xué)可能會(huì)說,那如果我們還是使用線程私有資源,但是傳入函數(shù)參數(shù)呢?
線程私有資源+函數(shù)參數(shù)
這樣的代碼是線程安全的嗎?自己先想一想這個(gè)問題。答案是it depends,也就是要看情況。看什么情況呢?1、按值傳參如果你傳入的參數(shù)的方式是按值傳入,那么沒有問題,代碼依然是線程安全的:
這這段代碼無論在多少個(gè)線程中調(diào)用怎么調(diào)用什么時(shí)候調(diào)用都會(huì)正確返回參數(shù)加1后的值。原因很簡(jiǎn)單,按值傳入的這些參數(shù)是線程私有資源。int func(int num) {
num++;
return num;
}
2、按引用傳參但如果是按引用傳入?yún)?shù),那么情況就不一樣了:
如果調(diào)用該函數(shù)的線程傳入的參數(shù)是線程私有資源,那么該函數(shù)依然是線程安全的,能正確的返回參數(shù)加1后的值。但如果傳入的參數(shù)是全局變量,就像這樣:int func(int* num) {
++(*num);
return *num;
}
那此時(shí)func函數(shù)將不再是線程安全代碼,因?yàn)閭魅氲膮?shù)指向了全局變量,這個(gè)全局變量是所有線程可共享資源,這種情況下如果不改變?nèi)肿兞康氖褂梅绞?,那么?duì)該全局變量的加1操作必須施加某種秩序,比如加鎖。int global_num = 1;
int func(int* num) {
++(*num);
return *num;
}
// 線程1
void thread1() {
func(&global_num);
}
// 線程2
void thread1() {
func(&global_num);
}
有的同學(xué)可能會(huì)說如果我傳入的不是全局變量的指針(引用)是不是就不會(huì)有問題了?答案依然是it depends,要看情況。即便我們傳入的參數(shù)是在堆上(heap)用malloc或new出來的,依然可能會(huì)有問題,為什么?答案很簡(jiǎn)單,因?yàn)?strong>堆上的資源也是所有線程可共享的。
假如有兩個(gè)線程調(diào)用func函數(shù)時(shí)傳入的指針(引用)指向了同一個(gè)堆上的變量,那么該變量就變成了這兩個(gè)線程的共享資源,在這種情況下func函數(shù)依然不是線程安全的。改進(jìn)也很簡(jiǎn)單,那就是每個(gè)線程調(diào)用func函數(shù)傳入一個(gè)獨(dú)屬于該線程的資源地址,這樣各個(gè)線程就不會(huì)妨礙到對(duì)方了,因此,寫出線程安全代碼的一大原則就是能用線程私有的資源就用私有資源,線程之間盡最大可能不去使用共享資源。
如果線程不得已要使用全局資源呢?
使用全局資源
使用全局資源就一定不是線程安全代碼嗎?答案還是。。有的同學(xué)可能已經(jīng)猜到了,答案依然是要看情況。如果使用的全局資源只在程序運(yùn)行時(shí)初始化一次,此后所有代碼對(duì)其使用都是只讀的,那么沒有問題,就像這樣:
我們看到,即使func函數(shù)使用了全局變量,但該全局變量只在運(yùn)行前初始化一次,此后的代碼都不會(huì)對(duì)其進(jìn)行修改,那么func函數(shù)依然是線程安全的。int global_num = 100; //初始化一次,此后沒有其它代碼修改其值
int func() {
return global_num;
}
但是,如果我們簡(jiǎn)單修改一下func:
int global_num = 100;
int func() {
++global_num;
return global_num;
}
這時(shí),func函數(shù)就不再是線程安全的了,對(duì)全局變量的修改必須加鎖保護(hù)。
線程局部存儲(chǔ)
接下來我們?cè)賹?duì)上述func函數(shù)簡(jiǎn)單修改:
我們看到全局變量global_num前加了關(guān)鍵詞__thread修飾,這時(shí),func代碼就是又是線程安全的了。為什么呢?其實(shí)在上一篇文章中我們講過,被__thread關(guān)鍵詞修飾過的變量放在了線程私有存儲(chǔ)中,Thread Local Storage,什么意思呢?意思是說這個(gè)變量是線程私有的全局變量:__thread int global_num = 100;
int func() {
++global_num;
return global_num;
}
-
global_num是全局變量
-
global_num是線程私有的
各個(gè)線程對(duì)global_num的修改不會(huì)影響到其它線程,因?yàn)槭?strong>線程私有資源,因此func函數(shù)是線程安全的。
說完了局部變量、全局變量、函數(shù)參數(shù),那么接下來就到函數(shù)返回值了。
函數(shù)返回值
這里也有兩種情況,一種是函數(shù)返回的是值;另一種返回對(duì)變量的引用。1、返回的是值我們來看這樣一段代碼:
毫無疑問,這段代碼是線程安全的,無論我們?cè)鯓诱{(diào)用該函數(shù)都會(huì)返回確定的值100。2、返回的是引用我們把上述代碼簡(jiǎn)單的改一改:int func() {
int a = 100;
return a;
}
如果我們?cè)诙嗑€程中調(diào)用這樣的函數(shù),那么接下來等著你的可能就是難以調(diào)試的bug以及漫漫的加班長(zhǎng)夜。int* func() {
static int a = 100;
return &a;
}
很顯然,這不是線程安全代碼,產(chǎn)生bug的原因也很簡(jiǎn)單,你在使用該變量前其值可能已經(jīng)被其它線程修改了。因?yàn)樵摵瘮?shù)使用了一個(gè)靜態(tài)全局變量,只要能拿到該變量的地址那么所有線程都可以修改該變量的值,因?yàn)檫@是線程間的共享資源,不到萬不得已不要寫出上述代碼,除非老板拿刀架在你脖子上。但是,請(qǐng)注意,有一個(gè)特例,這種使用方法可以用來實(shí)現(xiàn)設(shè)計(jì)模式中的單例模式,就像這樣:
為什么呢?因?yàn)闊o論我們調(diào)用多少次func函數(shù),static局部變量都只會(huì)被初始化一次,這種特性可以很方便的讓我們實(shí)現(xiàn)單例模式。class S {
public:
static S& getInstance() {
static S instance;
return instance;
}
private:
S() {}
// 其它省略
}
最后讓我們來看下這種情況,那就是如果我們調(diào)用一個(gè)非線程安全的函數(shù),那么我們的函數(shù)是線程安全的嗎?
調(diào)用非線程安全代碼
假如一個(gè)函數(shù)A調(diào)用另一個(gè)函數(shù)B,但B不是線程安全,那么函數(shù)A是線程安全的嗎?答案依然是,要看情況。我們看下這樣一段代碼,這段代碼在之前講解過:
我們認(rèn)為func函數(shù)是非線程安全的,因?yàn)閒unc函數(shù)使用了全局變量并對(duì)其進(jìn)行了修改,但如果我們這樣調(diào)用func函數(shù):int global_num = 0;
int func() {
++global_num;
return global_num;
}
雖然func函數(shù)是非線程安全的,但是我們?cè)谡{(diào)用該函數(shù)前加了一把鎖進(jìn)行保護(hù),那么這時(shí)funcA函數(shù)就是線程安全的了,其本質(zhì)就是我們用一把鎖間接的保護(hù)了全局變量。再看這樣一段代碼:int funcA() {
mutex l;
l.lock();
func();
l.unlock();
}
一般我們認(rèn)為func函數(shù)是非線程安全的,因?yàn)槲覀儾恢纻魅氲闹羔樖遣皇侵赶蛄艘粋€(gè)全局變量,但如果調(diào)用func函數(shù)的代碼是這樣的:int func(int *num) {
++(*num);
return *num;
}
那么這時(shí)funcA函數(shù)依然是線程安全的,因?yàn)閭魅氲膮?shù)是線程私有的局部變量,無論多少線程調(diào)用funcA都不會(huì)干擾到其它線程。void funcA() {
int a = 100;
func(&a);
}
看了各種情況下的線程安全問題,最后讓我們來總結(jié)一下實(shí)現(xiàn)線程安全代碼都有哪些措施。
如何實(shí)現(xiàn)線程安全
從上面各種情況的分析來看,實(shí)現(xiàn)線程安全無外乎圍繞線程私有資源和線程共享資源這兩點(diǎn),你需要識(shí)別出哪些是線程私有,哪些是共享的,這是核心,然后對(duì)癥下藥就可以了。
- 不使用任何全局資源,只使用線程私有資源,這種通常被稱為無狀態(tài)代碼
- 線程局部存儲(chǔ),如果要使用全局資源,是否可以聲明為線程局部存儲(chǔ),因?yàn)檫@種變量雖然是全局的,但每個(gè)線程都有一個(gè)屬于自己的副本,對(duì)其修改不會(huì)影響到其它線程
- 只讀,如果必須使用全局資源,那么全局資源是否可以是只讀的,多線程使用只讀的全局資源不會(huì)有線程安全問題。
- 原子操作,原子操作是說其在執(zhí)行過程中是不可能被其它線程打斷的,像C++中的std::atomic修飾過的變量,對(duì)這類變量的操作無需傳統(tǒng)的加鎖保護(hù),因?yàn)镃++會(huì)確保在變量的修改過程中不會(huì)被打斷。我們常說的各種無鎖數(shù)據(jù)結(jié)構(gòu)通常是在這類原子操作的基礎(chǔ)上構(gòu)建的 。
- 同步互斥,到這里也就確定了你必須要以某種形式使用全局資源,那么在這種情況下公共場(chǎng)所的秩序必須得到維護(hù),那么怎么維護(hù)呢?通過同步或者互斥的方式,這是一大類問題,我們將在《深入理解操作系統(tǒng)》系列文章中詳細(xì)闡述這一問題。
總 結(jié)
怎么樣,想寫出線程安全的還是不簡(jiǎn)單的吧,如果本文你只能記住一句話的話,那么我希望是這句,這也是本文的核心:實(shí)現(xiàn)線程安全無外乎圍繞線程私有資源和線程共享資源來進(jìn)行,你需要識(shí)別出哪些是線程私有,哪些是共享的,然后對(duì)癥下藥就可以了。希望本文對(duì)大家編寫多線程程序有幫助。
-
代碼
+關(guān)注
關(guān)注
30文章
4790瀏覽量
68653 -
線程安全
+關(guān)注
關(guān)注
0文章
13瀏覽量
2461
原文標(biāo)題:線程安全代碼到底是怎么編寫的?
文章出處:【微信號(hào):gh_c472c2199c88,微信公眾號(hào):嵌入式微處理器】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論