這是程序記憶體空間分配/使用的基本功問題,和執行緒沒多大關係。
對任何一個程序,它裡面存在如下幾個靜態記憶體區域:
1、常量區
2、全域性變數區
3、靜態變數區
4、程式碼區
這幾個區域是在執行單元載入時靜態分配的,位置、大小均固定。
當程序執行起來後,產生另外兩個動態區域,這就是堆和棧。
大多情況下,棧是CPU直接支援的一個記憶體區域。函式的區域性變數便位於這個區域。
堆是一個沒有嚴格定義的區域。一般情況下,使用者手動申請/歸還的記憶體區域都被稱為堆。
對於傳統的單執行緒模型,以上便是全部。這個模型必須搞得滾瓜爛熟,後面才好繼續。
單執行緒模型裡,函式呼叫是怎麼回事呢?
很簡單,透過CPU直接支援的棧區,自動維護“函式呼叫鏈”:
對於printSth函式,當它獲得執行權時,只需知道當前棧頂位置,然後基於這個位置就能推斷出屬於自己的區域性變數的位置。
當它執行結束之後,就要透過pop指令清除自己用過的區域性變數,把main函數里面呼叫printf函式的那條指令的位置取出、然後透過ret指令跳轉到下一條指令繼續執行。
main() {
fun1();
printSth(...);
fun2();
}
比如,對上面這個場景,printSth執行結束後,下一條指令就是呼叫fun2.
如果printSth裡面還呼叫了fun3,可依此類推:
這就是所謂的呼叫鏈。
只要維護好這個呼叫鏈資訊,程式就可以有條不紊的按設計預想執行了。
徹底搞明白呼叫鏈如何維護之後,我們很容易想到:如果我另外再申請一塊記憶體,把它的起始地址放進CPU的堆疊暫存器;那麼,是不是就可以用這塊地址另外維護一條呼叫鏈了呢?
這就是執行緒的原理。
所謂“新開一條執行緒”,實質上就是另外申請了一塊記憶體,然後把這塊記憶體當作堆疊,維護另外一條呼叫鏈。
而所謂“執行緒獲得執行權”呢,實質上就是把對應執行緒的棧頂指標等資訊載入CPU的棧指示器,使得它沿著這條呼叫鏈繼續執行下去——執行一段時間,把它的棧頂指標等資訊找個地方儲存、然後載入另一個執行緒的棧頂指標等資訊,這就是所謂的“執行緒切換”。
執行緒有兩種。
如果維護呼叫鏈(以及執行現場)的任務全部放在使用者空間,不讓作業系統知道,這就叫“使用者態執行緒”。
反之,如果作業系統自己提供了開闢新執行緒以及維護它的呼叫鏈的一整套方法,這就叫“核心態執行緒”。
兩者的差別就是後者是作業系統管理的,可以得到多CPU之類的直接支援。
但在記憶體空間使用上,兩者並無根本區別:它們都是另外申請了一塊空間用作堆疊,然後像傳統的單執行緒程式一樣,用這個堆疊維護呼叫鏈(以及區域性變數等資訊)。
執行緒和程序的區別就在於,執行緒只有呼叫鏈,而程序還包含常量區、全域性變數區等其他區域,同時還有各種資源的所有權。
換句話說,作業系統認為,諸如動態申請記憶體、核心物件等各種資源,哪怕是在某個執行緒裡面申請的,它的所有權仍然屬於程序所有——所以,執行緒退出除了會清理呼叫鏈資訊外,並不釋放其他資源;而程序退出就會自動歸還它申請的各種資源(某些特殊資源除外:並不能盲目認為一旦程序退出一切就會變回原樣)。
明白了這個之後,問題迎刃而解:
1、所有執行緒都是在各自獨立的棧區維護的呼叫鏈(以及執行現場)
2、執行緒區域性變數處於各自所屬的棧區
3、不允許跨執行緒直接傳遞區域性變數的引用/指標,因為它們隨時可能失效
這點一定要注意。和單執行緒程式不同,跨執行緒傳遞區域性變數指標給被呼叫者是沒有絲毫保障的;傳了,就一定會出事!
4、執行緒中取得的、程序生存期有效的資源,要麼直接/間接掛載到全域性變數/全域性靜態變數上,要麼就一定要線上程結束前釋放。不然就會造成資源洩露(搜尋不被全域性變數和區域性變數索引的記憶體並主動釋放,這正是垃圾回收的原理)。
5、執行緒由誰啟動這個資訊並不在呼叫鏈上。換句話說,所有執行緒都是平等的,它們各自獨立使用自己的專屬棧區(但主執行緒較為特殊,大多實現中,它的退出就意味著程序結束;除此之外,它們是平等的)。
這是程序記憶體空間分配/使用的基本功問題,和執行緒沒多大關係。
對任何一個程序,它裡面存在如下幾個靜態記憶體區域:
1、常量區
2、全域性變數區
3、靜態變數區
4、程式碼區
這幾個區域是在執行單元載入時靜態分配的,位置、大小均固定。
當程序執行起來後,產生另外兩個動態區域,這就是堆和棧。
大多情況下,棧是CPU直接支援的一個記憶體區域。函式的區域性變數便位於這個區域。
堆是一個沒有嚴格定義的區域。一般情況下,使用者手動申請/歸還的記憶體區域都被稱為堆。
對於傳統的單執行緒模型,以上便是全部。這個模型必須搞得滾瓜爛熟,後面才好繼續。
單執行緒模型裡,函式呼叫是怎麼回事呢?
很簡單,透過CPU直接支援的棧區,自動維護“函式呼叫鏈”:
棧頂printSth函式的區域性變數main函數里面呼叫printSth函式的那條指令的位置main函式的區域性變數棧底對於printSth函式,當它獲得執行權時,只需知道當前棧頂位置,然後基於這個位置就能推斷出屬於自己的區域性變數的位置。
當它執行結束之後,就要透過pop指令清除自己用過的區域性變數,把main函數里面呼叫printf函式的那條指令的位置取出、然後透過ret指令跳轉到下一條指令繼續執行。
main() {
fun1();
printSth(...);
fun2();
}
比如,對上面這個場景,printSth執行結束後,下一條指令就是呼叫fun2.
如果printSth裡面還呼叫了fun3,可依此類推:
棧頂fun3的區域性變數printSth裡面呼叫fun3的那條指令的位置printSth函式的區域性變數main函數里面呼叫printSth函式的那條指令的位置main函式的區域性變數棧底這就是所謂的呼叫鏈。
只要維護好這個呼叫鏈資訊,程式就可以有條不紊的按設計預想執行了。
徹底搞明白呼叫鏈如何維護之後,我們很容易想到:如果我另外再申請一塊記憶體,把它的起始地址放進CPU的堆疊暫存器;那麼,是不是就可以用這塊地址另外維護一條呼叫鏈了呢?
這就是執行緒的原理。
所謂“新開一條執行緒”,實質上就是另外申請了一塊記憶體,然後把這塊記憶體當作堆疊,維護另外一條呼叫鏈。
而所謂“執行緒獲得執行權”呢,實質上就是把對應執行緒的棧頂指標等資訊載入CPU的棧指示器,使得它沿著這條呼叫鏈繼續執行下去——執行一段時間,把它的棧頂指標等資訊找個地方儲存、然後載入另一個執行緒的棧頂指標等資訊,這就是所謂的“執行緒切換”。
執行緒有兩種。
如果維護呼叫鏈(以及執行現場)的任務全部放在使用者空間,不讓作業系統知道,這就叫“使用者態執行緒”。
反之,如果作業系統自己提供了開闢新執行緒以及維護它的呼叫鏈的一整套方法,這就叫“核心態執行緒”。
兩者的差別就是後者是作業系統管理的,可以得到多CPU之類的直接支援。
但在記憶體空間使用上,兩者並無根本區別:它們都是另外申請了一塊空間用作堆疊,然後像傳統的單執行緒程式一樣,用這個堆疊維護呼叫鏈(以及區域性變數等資訊)。
執行緒和程序的區別就在於,執行緒只有呼叫鏈,而程序還包含常量區、全域性變數區等其他區域,同時還有各種資源的所有權。
換句話說,作業系統認為,諸如動態申請記憶體、核心物件等各種資源,哪怕是在某個執行緒裡面申請的,它的所有權仍然屬於程序所有——所以,執行緒退出除了會清理呼叫鏈資訊外,並不釋放其他資源;而程序退出就會自動歸還它申請的各種資源(某些特殊資源除外:並不能盲目認為一旦程序退出一切就會變回原樣)。
明白了這個之後,問題迎刃而解:
1、所有執行緒都是在各自獨立的棧區維護的呼叫鏈(以及執行現場)
2、執行緒區域性變數處於各自所屬的棧區
3、不允許跨執行緒直接傳遞區域性變數的引用/指標,因為它們隨時可能失效
這點一定要注意。和單執行緒程式不同,跨執行緒傳遞區域性變數指標給被呼叫者是沒有絲毫保障的;傳了,就一定會出事!
4、執行緒中取得的、程序生存期有效的資源,要麼直接/間接掛載到全域性變數/全域性靜態變數上,要麼就一定要線上程結束前釋放。不然就會造成資源洩露(搜尋不被全域性變數和區域性變數索引的記憶體並主動釋放,這正是垃圾回收的原理)。
5、執行緒由誰啟動這個資訊並不在呼叫鏈上。換句話說,所有執行緒都是平等的,它們各自獨立使用自己的專屬棧區(但主執行緒較為特殊,大多實現中,它的退出就意味著程序結束;除此之外,它們是平等的)。