我們知道,多線程同時修改共享變量時會出現(xiàn)數(shù)據(jù)不一致的問題,比如多個線程同時對一個變量加1,假設(shè)count的初始值為0:
int count;
void add() {
++count;
}
如果只有一個線程調(diào)用add函數(shù),那么什么問題都沒有,但如果多個線程同時調(diào)用上述函數(shù),比如10個線程都調(diào)用一遍,那么count值最后不一定等于0,原因在于對 count加1的操作不是原子的 。
所謂某個操作是原子的是指CPU要么執(zhí)行該操作,要么不執(zhí)行該操作,不存在中間狀態(tài),但上述對count加1的操作經(jīng)過編譯器處理后會生成幾條對應(yīng)的機器指令,所以該操作不是原子的。
那么怎樣才能讓其變成原子的呢?很簡單,加一把鎖。
int count;
mutex mtx; // 鎖
void add() {
mtx.lock();
++count;
mtx.unlock();
};
現(xiàn)在我們用一把鎖將對count的操作保護了起來,此時你可以將mtx.lock()以及mtx.unlock()中間的代碼看成原子的,CPU要完全執(zhí)行完對count的加1要么根本不會操作count,這樣上述程序的運行結(jié)果就是我們想要的了。
這是怎樣做到呢?這就要說到操作系統(tǒng)了,千萬不要小瞧了上面的mutex這把鎖,這把鎖的背后相當(dāng)復(fù)雜,因為這涉及到了操作系統(tǒng)。
假設(shè)現(xiàn)在有三個線程,各自運行在不同的CPU核心上,每個方框代表一個時間片:
T1時間片這三個線程都在調(diào)用add函數(shù),線程A拿到鎖,A可以繼續(xù)向前推進,但B和C就沒這么幸運了,此時操作系統(tǒng)將剝奪線程B和C繼續(xù)持有CPU的權(quán)利,將其分配給其它具備執(zhí)行條件的線程,這就是操作系統(tǒng)中所謂的掛起,注意,這個過程相當(dāng)復(fù)雜,因為這涉及到用戶態(tài)與內(nèi)核態(tài)的切換以及線程的切換等等。
此時來到T2時間片,線程A繼續(xù)向前推進,線程B和C則被按下暫停鍵。
T3時間片,然而就在線程A拿到鎖運行時因為某些原因像高優(yōu)先級線程槍占之類導(dǎo)致操作系統(tǒng)也剝奪了線程A繼續(xù)持有CPU的權(quán)利,糟糕的是,因為線程A此時持有鎖,而線程A又無法繼續(xù)向前推進,這就進一步使得線程B和C也無法繼續(xù)向前推進。
你會發(fā)現(xiàn)在T3時刻,這幾個線程都沒有任何進展,根本原因在于我們?yōu)榻鉀Q多線程問題加互斥鎖驚動了操作系統(tǒng),而這類互斥鎖是操作系統(tǒng)給我們實現(xiàn)的,那么解決線程安全問題一定要經(jīng)過操作系統(tǒng)嗎?
不是的,在硬件層面也可以解決線程安全問題,硬件層面當(dāng)然是指CPU,或者說機器指令。
CPU中有特定的原子指令,實際上操作系統(tǒng)也是基于這些指令實現(xiàn)的互斥鎖,既然操作系統(tǒng)能用這些指令,我們(用戶態(tài))也可以使用這些指令,基于此我們可以將上述代碼進行簡單改造:
int count = 0;
void add() {
int old_value;
do {
old_value = count;
} while (!atomic_compare_exchange(&count, &old_value, old_value + 1));
}
此時add函數(shù)是線程安全的,我們也沒有對其進行加鎖,不管多少線程同時調(diào)用add函數(shù)得到count都是正確的, 而該函數(shù)的執(zhí)行完全不涉及操作系統(tǒng) ,不需要操作系統(tǒng)來維護秩序,利用的就是CPU中的原子指令,CPU在硬件層面一樣可以替我們維護秩序。
上述代碼就是所謂lock-free的, 不管操作系統(tǒng)怎樣調(diào)度這三個線程,我們都能確保這三個線程中總有一個能繼續(xù)向前推進 。
lock-free的系統(tǒng)看起來像這樣:
對于這類系統(tǒng) 不存在某個時間片下線程都無法推進的情況 ,換句話說就是lock-free程序保證至少有一個線程能繼續(xù)向前推進。
可以看到,lock-free給出了比普通鎖更優(yōu)的保障。
但不能簡單從代碼是不是加鎖或不加鎖去判斷代碼是否lock-free ,回旋鎖也是沒有上述互斥鎖的,也不經(jīng)過操作系統(tǒng),但回旋鎖并不是lock-free的,如果你這樣利用CPU中的原子操作修改add函數(shù):
int count = 0;
int lock = 0; // 回旋鎖
void add () {
int expected = 0;
while(!atomic_compare_exchange_weak(&lock, &expected, 1))
expected = 0;
count++;
lock = 0;
}
這就是典型的回旋鎖,然而如果某個線程持有回旋鎖后被操作系統(tǒng)掛起那么其它線程開始無效的執(zhí)行死循環(huán),除了白白消耗CPU之外它們都無法繼續(xù)向前推進,顯而易見,如果此時系統(tǒng)負載較高那么此類程序的性能會變差。
既然現(xiàn)在你已經(jīng)知道了lock-free我們再繼續(xù)優(yōu)化這段代碼:
std::atomic<int> count;
void add() {
++count;
}
這段代碼沒有鎖,也不需要用循環(huán)不斷檢測是否有其它線程修改count,不管操作系統(tǒng)如何調(diào)度這三個線程, 它們都能在有限的操作數(shù)內(nèi)執(zhí)行完成 ,此時我們說該程序是wati-free的,wait-free系統(tǒng)運行起來像這樣:
可以看到在任意時間片內(nèi), 不管操作系統(tǒng)怎樣調(diào)度,所有線程都能向前推進 。
wait-free比lock-free的要求更高更加嚴格,由于wait-free的程序總是能在有限的步驟內(nèi)執(zhí)行完成,因此實時性是最好的,適用于那些對實時性要求較高的場景,當(dāng)然實現(xiàn)難度也要比lock-free更高。
值得注意的是,wait-free以及l(fā)ock-free程序的實現(xiàn)通常不是那么簡單。
好啦,今天就到這里,希望這篇對大家理解多線程有所幫助
-
cpu
+關(guān)注
關(guān)注
68文章
10905瀏覽量
213032 -
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7145瀏覽量
89587 -
多線程
+關(guān)注
關(guān)注
0文章
278瀏覽量
20075 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4346瀏覽量
62978
發(fā)布評論請先 登錄
相關(guān)推薦
在不改變內(nèi)部程序的情況下,只想改變外部元件,如晶振等,怎樣才能讓實時時鐘加快,
labview問題怎樣才能在注冊時信息沒填完整時注冊,
Linux下多線程機制
怎樣才能在SDCARD中運行Android系統(tǒng)呢
在MCU開發(fā)中使用多線程操作一寫一讀是否需要保護?
怎樣才能使本本達到最優(yōu)性能
Linux下的多線程編程
在多線程的情況下如何對一個值進行 a++ 操作
![在<b class='flag-5'>多線程</b>的<b class='flag-5'>情況下</b>如何對一個值進行 a++ 操作](https://file1.elecfans.com/web2/M00/A9/BE/wKgZomUotW-AbU1rAACoAInK2eE359.jpg)
什么情況下避免使用系統(tǒng)調(diào)用
![什么<b class='flag-5'>情況下</b>避免使用系統(tǒng)調(diào)用](https://file1.elecfans.com/web2/M00/AD/E3/wKgaomVRiqaAFfWJAABgFNVWXbE328.jpg)
怎樣才能在有限的容量下發(fā)揮電池的極限續(xù)航能力
![<b class='flag-5'>怎樣才能在</b>有限的容量下發(fā)揮電池的極限續(xù)航能力](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
評論