進程和線程這兩個話題是程序員繞不開的,操做系統提供的這兩個抽象概念實在是過重要了。程序員
關於進程和線程有一個極其經典的問題,那就是進程和線程的區別是什麼?相信不少同窗對答案似懂非懂。多線程
有的同窗可能已經「背得」倒背如流了:「進程是操做系統分配資源的單位,線程是調度的基本單位,線程之間共享進程資源」。app
但是你真的理解了上面這句話嗎?到底線程之間共享了哪些進程資源,共享資源意味着什麼?共享資源這種機制是如何實現的?對此若是你沒有答案的話,那麼這意味着你幾乎很難寫出能正確工做的多線程程序,同時也意味着這篇文章就是爲你準備的。函數
查理芒格常常說這樣一句話:「反過來想,老是反過來想」,若是你對線程之間共享了哪些進程資源這個問題想不清楚的話那麼也能夠反過來思考,那就是有哪些資源是線程私有的。工具
線程運行的本質其實就是函數的執行,函數的執行總會有一個源頭,這個源頭就是所謂的入口函數,CPU從入口函數開始執行從而造成一個執行流,只不過咱們人爲的給執行流起一個名字,這個名字就叫線程。ui
既然線程運行的本質就是函數的執行,那麼函數執行都有哪些信息呢?spa
在《函數運行時在內存中是什麼樣子?》這篇文章中咱們說過,函數運行時的信息保存在棧幀中,棧幀中保存了函數的返回值、調用其它函數的參數、該函數使用的局部變量以及該函數使用的寄存器信息,如圖所示,假設函數A調用函數B:操作系統
此外,CPU執行指令的信息保存在一個叫作程序計數器的寄存器中,經過這個寄存器咱們就知道接下來要執行哪一條指令。因爲操做系統隨時能夠暫停線程的運行,所以咱們保存以及恢復程序計數器中的值就能知道線程是從哪裏暫停的以及該從哪裏繼續運行了。.net
因爲線程運行的本質就是函數運行,函數運行時信息是保存在棧幀中的,所以每一個線程都有本身獨立的、私有的棧區。線程
同時函數運行時須要額外的寄存器來保存一些信息,像部分局部變量之類,這些寄存器也是線程私有的,一個線程不可能訪問到另外一個線程的這類寄存器信息。
從上面的討論中咱們知道,到目前爲止,所屬線程的棧區、程序計數器、棧指針以及函數運行使用的寄存器是線程私有的。
以上這些信息有一個統一的名字,就是線程上下文,thread context。
咱們也說過操做系統調度線程須要隨時中斷線程的運行而且須要線程被暫停後能夠繼續運行,操做系統之因此能實現這一點,依靠的就是線程上下文信息。
如今你應該知道哪些是線程私有的了吧。
除此以外,剩下的都是線程間共享資源。
那麼剩下的還有什麼呢?還有圖中的這些。
這其實就是進程地址空間的樣子,也就是說線程共享進程地址空間中除線程上下文信息中的全部內容,意思就是說線程能夠直接讀取這些內容。
接下來咱們分別來看一下這些區域。
進程地址空間中的代碼區,這裏保存的是什麼呢?從名字中有的同窗可能已經猜到了,沒錯,這裏保存的就是咱們寫的代碼,更準確的是編譯後的可執行機器指令。
那麼這些機器指令又是從哪裏來的呢?答案是從可執行文件中加載到內存的,可執行程序中的代碼區就是用來初始化進程地址空間中的代碼區的。
線程之間共享代碼區,這就意味着程序中的任何一個函數均可以放到線程中去執行,不存在某個函數只能被特定線程執行的狀況。
堆區是程序員比較熟悉的,咱們在C/C++中用malloc或者new出來的數據就存放在這個區域,很顯然,只要知道變量的地址,也就是指針,任何一個線程均可以訪問指針指向的數據,所以堆區也是線程共享的屬於進程的資源。
唉,等等!剛不是說棧區是線程私有資源嗎,怎麼這會兒又提及棧區了?
確實,從線程這個抽象的概念上來講,棧區是線程私有的,然而從實際的實現上看,棧區屬於線程私有這一規則並無嚴格遵照,這句話是什麼意思?
一般來講,注意這裏的用詞是一般,一般來講棧區是線程私有,既然有一般就有不一般的時候。
不一般是由於不像進程地址空間之間的嚴格隔離,線程的棧區沒有嚴格的隔離機制來保護,所以若是一個線程能拿到來自另外一個線程棧幀上的指針,那麼該線程就能夠改變另外一個線程的棧區,也就是說這些線程能夠任意修改本屬於另外一個線程棧區中的變量。
這從某種程度上給了程序員極大的便利,但同時,這也會致使極其難以排查到的bug。
試想一下你的程序運行的好好的,結果某個時刻忽然出問題,定位到出問題代碼行後根本就排查不到緣由,你固然是排查不到問題緣由的,由於你的程序原本就沒有任何問題,是別人的問題致使你的函數棧幀數據被寫壞從而產生bug,這樣的問題一般很難排查到緣由,須要對總體的項目代碼很是熟悉,經常使用的一些debug工具這時可能已經沒有多大做用了。
說了這麼多,那麼同窗可能會問,一個線程是怎樣修改本屬於其它線程的數據呢?
接下來咱們用一個代碼示例講解一下。
最後,若是程序在運行過程當中打開了一些文件,那麼進程地址空間中還保存有打開的文件信息,進程打開的文件也能夠被全部的線程使用,這也屬於線程間的共享資源。關於文件IO操做,你能夠參考《讀取文件時,程序經歷了什麼?》
本文就這些了嗎?
實際上本篇開頭關於線程私有數據還有一個項沒有詳細講解,由於再講下去本篇就撐爆了,實際上本篇講解的已經足夠用了,剩下的這一點僅僅做爲補充。
關於線程私有數據還有一項技術,那就是線程局部存儲,Thread Local Storage,TLS。
這是什麼意思呢?
其實從名字上也能夠看出,所謂線程局部存儲,是指存放在該區域中的變量有兩個含義:
說了這麼多仍是沒懂有沒有?不要緊,接下來看完這兩段代碼還不懂你來打我。
咱們先來看第一段代碼,不用擔憂,這段代碼很是很是的簡單:
int a = 1; // 全局變量
void print_a() {
cout<<a<<endl;
}
void run() {
++a;
print_a();
}
void main() {
thread t1(run);
t1.join();
thread t2(run);
t2.join();
}
怎麼樣,這段代碼足夠簡單吧,上述代碼是用C++11寫的,我來說解下這段代碼是什麼意思。
那麼這段代碼的運行起來會打印什麼呢?
全局變量a的初始值爲1,第一個線程加1後a變爲2,所以會打印2;第二個線程再次加1後a變爲3,所以會打印3,讓咱們來看一下運行結果:
2
3
看來咱們分析的沒錯,全局變量在兩個線程分別加1後最終變爲3。
接下來咱們對變量a的定義稍做修改,其它代碼不作改動:
__thread int a = 1; // 線程局部存儲
咱們看到全局變量a前面加了一個__thread關鍵詞用來修飾,也就是說咱們告訴編譯器把變量a放在線程局部存儲中,那這會對程序帶來哪些改變呢?
簡單運行一下就知道了:
2
2
和你想的同樣嗎,有的同窗可能會大吃一驚,爲何咱們明明對變量a加了兩次,但第二次運行爲何仍是打印2而不是3呢?
想想這是爲何。
原來,這就是線程局部存儲的做用所在,線程t1對變量a的修改不會影響到線程t2,線程t1在將變量a加到1後變爲2,但對於線程t2來講此時變量a依然是1,所以加1後依然是2。
所以,線程局部存儲可讓你使用一個獨屬於線程的全局變量。也就是說,雖然該變量能夠被全部線程訪問,但該變量在每一個線程中都有一個副本,一個線程對改變量的修改不會影響到其它線程。
怎麼樣,沒想到教科書上一句簡單的「線程共享進程資源」背後居然會有這麼多的知識點吧,教科書上的知識確實枯燥,但,並不簡單。
但願本篇能對你們理解進程、線程能有多幫助。