線程間到底共享了哪些進程資源?
時間:2025-12-02 22:24:14
手機看文章
掃描二維碼
隨時隨地手機看文章
進程和線程這兩個話題是程序員繞不開的,操作系統(tǒng)提供的這兩個抽象概念實在是太重要了。關(guān)于進程和線程有一個極其經(jīng)典的問題,那就是進程和線程的區(qū)別是什么?相信很多同學對答案似懂非懂。
這樣的變量對每個線程來說也是可見的,也就是說每個線程都可以訪問到該變量。
首先我們在主線程的棧區(qū)定義了一個局部變量,也就是 int a= 1這行代碼,現(xiàn)在我們已經(jīng)知道了,局部變量a屬于主線程私有數(shù)據(jù),但是,接下來我們創(chuàng)建了另外一個線程。在新創(chuàng)建的這個線程中,我們將變量a的地址以參數(shù)的形式傳給了新創(chuàng)建的線程,然后我來看一下thread函數(shù)。在新創(chuàng)建的線程中,我們獲取到了變量a的指針,然后將其修改為了2,也就是這行代碼,我們在新創(chuàng)建的線程中修改了本屬于主線程的私有數(shù)據(jù)。
。關(guān)于線程私有數(shù)據(jù)還有一項技術(shù),那就是線程局部存儲,Thread Local Storage,TLS。這是什么意思呢?其實從名字上也可以看出,所謂線程局部存儲,是指存放在該區(qū)域中的變量有兩個含義:
記住了不一定真懂
關(guān)于這個問題有的同學可能已經(jīng)“背得”滾瓜爛熟了:“進程是操作系統(tǒng)分配資源的單位,線程是調(diào)度的基本單位,線程之間共享進程資源”。可是你真的理解了上面最后一句話嗎?到底線程之間共享了哪些進程資源,共享資源意味著什么?共享資源這種機制是如何實現(xiàn)的?對此如果你沒有答案的話,那么這意味著你幾乎很難寫出能正確工作的多線程程序,同時也意味著這篇文章就是為你準備的。逆向思考
查理芒格經(jīng)常說這樣一句話:“反過來想,總是反過來想”,如果你對線程之間共享了哪些進程資源這個問題想不清楚的話那么也可以反過來思考,那就是有哪些資源是線程私有的。線程私有資源
線程運行的本質(zhì)其實就是函數(shù)的執(zhí)行,函數(shù)的執(zhí)行總會有一個源頭,這個源頭就是所謂的入口函數(shù),CPU從入口函數(shù)開始執(zhí)行從而形成一個執(zhí)行流,只不過我們?nèi)藶榈慕o執(zhí)行流起一個名字,這個名字就叫線程。既然線程運行的本質(zhì)就是函數(shù)的執(zhí)行,那么函數(shù)執(zhí)行都有哪些信息呢?在《函數(shù)運行時在內(nèi)存中是什么樣子》這篇文章中我們說過,函數(shù)運行時的信息保存在棧幀中,棧幀中保存了函數(shù)的返回值、調(diào)用其它函數(shù)的參數(shù)、該函數(shù)使用的局部變量以及該函數(shù)使用的寄存器信息,如圖所示,假設函數(shù)A調(diào)用函數(shù)B:
代碼區(qū)
進程地址空間中的代碼區(qū),這里保存的是什么呢?從名字中有的同學可能已經(jīng)猜到了,沒錯,這里保存的就是我們寫的代碼,更準確的是編譯后的可執(zhí)行機器指令。那么這些機器指令又是從哪里來的呢?答案是從可執(zhí)行文件中加載到內(nèi)存的,可執(zhí)行程序中的代碼區(qū)就是用來初始化進程地址空間中的代碼區(qū)的。
數(shù)據(jù)區(qū)
進程地址空間中的數(shù)據(jù)區(qū),這里存放的就是所謂的全局變量。什么是全局變量?所謂全局變量就是那些你定義在函數(shù)之外的變量,在C語言中就像這樣:char c; // 全局變量 void func() { }其中字符c就是全局變量,存放在進程地址空間中的數(shù)據(jù)區(qū)。
void func(){ static int a = 10;}注意到,雖然變量a定義在函數(shù)內(nèi)部,但變量a依然具有全局變量的特性,也就是說變量a放在了進程地址空間的數(shù)據(jù)區(qū)域,即使函數(shù)執(zhí)行完后該變量依然存在,而普通的局部變量隨著函數(shù)調(diào)用結(jié)束和函數(shù)棧幀一起被回收掉了,但這里的變量a不會被回收,因為其被放到了數(shù)據(jù)區(qū)。
這樣的變量對每個線程來說也是可見的,也就是說每個線程都可以訪問到該變量。
堆區(qū)
堆區(qū)是程序員比較熟悉的,我們在C/C++中用malloc或者new出來的數(shù)據(jù)就存放在這個區(qū)域,很顯然,只要知道變量的地址,也就是指針,任何一個線程都可以訪問指針指向的數(shù)據(jù),因此堆區(qū)也是線程共享的屬于進程的資源。
棧區(qū)
唉,等等!剛不是說棧區(qū)是線程私有資源嗎,怎么這會兒又說起棧區(qū)了?確實,從線程這個抽象的概念上來說,棧區(qū)是線程私有的,然而從實際的實現(xiàn)上看,棧區(qū)屬于線程私有這一規(guī)則并沒有嚴格遵守,這句話是什么意思?通常來說,注意這里的用詞是通常,通常來說棧區(qū)是線程私有,既然有通常就有不通常的時候。不通常是因為不像進程地址空間之間的嚴格隔離,線程的棧區(qū)沒有嚴格的隔離機制來保護,因此如果一個線程能拿到來自另一個線程棧幀上的指針,那么該線程就可以改變另一個線程的棧區(qū),也就是說這些線程可以任意修改本屬于另一個線程棧區(qū)中的變量。
修改線程私有數(shù)據(jù)
不要擔心,以下代碼足夠簡單:
void thread(void* var) { int* p = (int*)var; *p = 2;} int main() { int a = 1; pthread_t tid; pthread_create(&tid, NULL, thread, (void*)&a); return 0;}這段代碼是什么意思呢?
首先我們在主線程的棧區(qū)定義了一個局部變量,也就是 int a= 1這行代碼,現(xiàn)在我們已經(jīng)知道了,局部變量a屬于主線程私有數(shù)據(jù),但是,接下來我們創(chuàng)建了另外一個線程。在新創(chuàng)建的這個線程中,我們將變量a的地址以參數(shù)的形式傳給了新創(chuàng)建的線程,然后我來看一下thread函數(shù)。在新創(chuàng)建的線程中,我們獲取到了變量a的指針,然后將其修改為了2,也就是這行代碼,我們在新創(chuàng)建的線程中修改了本屬于主線程的私有數(shù)據(jù)。
動態(tài)鏈接庫
進程地址空間中除了以上討論的這些實際上還有其它內(nèi)容,還有什么呢?這就要從可執(zhí)行程序說起了。什么是可執(zhí)行程序呢?在Windows中就是我們熟悉的exe文件,在Linux世界中就是ELF文件,這些可以被操作系統(tǒng)直接運行的程序就是我們所說的可執(zhí)行程序。那么可執(zhí)行程序是怎么來的呢?有的同學可能會說,廢話,不就是編譯器生成的嗎?實際上這個答案只答對了一半。假設我們的項目比較簡單只有幾個源碼文件,編譯器是怎么把這幾個源代碼文件轉(zhuǎn)換為最終的一個可執(zhí)行程序呢?原來,編譯器在將可執(zhí)行程序翻譯成機器指令后,接下來還有一個重要的步驟,這就是鏈接,鏈接完成后生成的才是可執(zhí)行程序。完成鏈接這一過程的就是鏈接器。
文件
最后,如果程序在運行過程中打開了一些文件,那么進程地址空間中還保存有打開的文件信息,進程打開的文件也可以被所有的線程使用,這也屬于線程間的共享資源。
One More Thing:TLS
本文就這些了嗎?實際上關(guān)于線程私有數(shù)據(jù)還有一項沒有詳細講解,因為再講下去本篇就撐爆了,而且本篇已經(jīng)講解的部分足夠用了,剩下的這一點僅僅作為補充,也就是選學部分,如果你對此不感興趣的話完全可以跳過,沒有問題
。關(guān)于線程私有數(shù)據(jù)還有一項技術(shù),那就是線程局部存儲,Thread Local Storage,TLS。這是什么意思呢?其實從名字上也可以看出,所謂線程局部存儲,是指存放在該區(qū)域中的變量有兩個含義:
-
存放在該區(qū)域中的變量是全局變量,所有線程都可以訪問
-
雖然看上去所有線程訪問的都是同一個變量,但該全局變量獨屬于一個線程,一個線程對此變量的修改對其他線程不可見。
int a = 1; // 全局變量 void print_a() { cout<} void run() { ++a; print_a();} void main() { thread t1(run); t1.join(); thread t2(run); t2.join();}怎么樣,這段代碼足夠簡單吧,上述代碼是用C++11寫的,我來講解下這段代碼是什么意思。
- 首先我們創(chuàng)建了一個全局變量a,初始值為1
- 其次我們創(chuàng)建了兩個線程,每個線程對變量a加1
- 線程的join函數(shù)表示該線程運行完畢后才繼續(xù)運行接下來的代碼
23看來我們分析的沒錯,全局變量在兩個線程分別加1后最終變?yōu)?。接下來我們對變量a的定義稍作修改,其它代碼不做改動:
__thread int a = 1; // 線程局部存儲
我們看到全局變量a前面加了一個__thread關(guān)鍵詞用來修飾,也就是說我們告訴編譯器把變量a放在線程局部存儲中,那這會對程序帶來哪些改變呢?簡單運行一下就知道了:
22和你想的一樣嗎?有的同學可能會大吃一驚,為什么我們明明對變量a加了兩次,但第二次運行為什么還是打印2而不是3呢?想一想這是為什么。原來,這就是線程局部存儲的作用所在,線程t1對變量a的修改不會影響到線程t2,線程t1在將變量a加到1后變?yōu)?,但對于線程t2來說此時變量a依然是1,因此加1后依然是2。因此,線程局部存儲可以讓你使用一個獨屬于線程的全局變量。也就是說,雖然該變量可以被所有線程訪問,但該變量在每個線程中都有一個副本,一個線程對改變量的修改不會影響到其它線程。
總結(jié)
怎么樣,沒想到教科書上一句簡單的“線程共享進程資源”背后竟然會有這么多的知識點吧,教科書上的知識看似容易,但,并不簡單。希望本篇能對大家理解進程、線程能有多幫助。





