目錄
1.線程知識補充
1.1 線程私有資源
1.2 線程共享資源
1.3 原生線程庫
2、線程控制接口
2.1 線程創建
2.1.1 一批線程
2.2 線程等待
2.3 線程終止
?2.4 線程實戰
2.5 其他接口
2.5.1 關閉線程pthread_cancel
2.5.2 獲取線程 ID pthread_self
2.5.3 線pthread_detach
3. 深入理解線程
3.1 理解線程庫及線程 ID
3.2 理解線程獨立棧
3.3 理解線程局部存儲
🌇前言
線程是進程內的基本執行單位,作為 CPU 執行的基本單位,線程的控制與任務執行效率息息相關。合理地進行線程管理,能大大提升程序的執行效率,掌握線程的基本操作至關重要。
🏙?正文
1.線程知識補充
在深入討論線程控制接口之前,我們首先需要補充一些關于線程的基礎知識。Linux
?中沒有真線程,只有復用?PCB
?設計思想的?TCB
?結構
1.1 線程私有資源
在 Linux 的多線程實現中,線程本質上是輕量級進程(LWP),即通過復用 PCB 設計的 TCB 結構來模擬線程。因此,盡管 Linux 系統中的多個線程共享同一進程的地址空間,但每個線程仍然需要一定的獨立性和資源。
線程私有資源具體包括:
-
線程 ID:線程的唯一標識符,由內核管理
-
寄存器:每個線程的上下文信息,如寄存器,線程切換時需要保存這些信息
-
獨立棧:每個線程都有獨立的棧空間,用于存儲局部變量和執行上下文
-
錯誤碼(errno):線程異常退出時,通過錯誤碼反饋信息
-
信號屏蔽字:各個線程對于信號的屏蔽字設置不同,確保每個線程能根據需要對信號做出響應
-
調度優先級:線程也需要被調度,調度算法根據優先級來合理分配執行時間
其中,寄存器和獨立棧是線程最關鍵的私有資源,它們保障了線程切換的獨立性以及運行時的穩定性。
1.2 線程共享資源
除了線程的私有資源,多線程還會共享進程的部分資源。線程共享資源不需要額外的開銷,并能在各個線程間隨時訪問。
共享的定義:不需要太多的額外成本,就可以實現隨時訪問資源
基于?多線程看到的是同一塊進程地址空間,理論上?凡是在進程地址空間中出現的資源,多線程都是可以看到的
但實際上為了確保線程調度、運行時的獨立性,只能共享部分資源
在 Linux 中,共享資源包括:
-
共享區、全局數據區、字符常量區、代碼區:這些區域是進程中天然支持共享的資源。
-
文件描述符表:在多線程中進行 I/O 操作時,無需每個線程都重新打開文件,文件描述符表在多個線程間共享。
-
信號處理方式:所有線程共同構成一個整體,信號處理必須統一。
-
當前工作目錄:所有線程共享進程的工作目錄。
-
用戶 ID 和組 ID:進程屬于特定的用戶和組,線程也繼承這些身份。
文件描述符表是多線程共享資源中最重要的部分,它確保了多線程 I/O 操作的高效性和協作性。
1.3 原生線程庫
當我們編譯多線程相關代碼時,通常需要添加 -lpthread
參數,確保能夠使用 pthread 原生線程庫。
這是因為,在 Linux 中并沒有真正意義上的線程,而是通過輕量級進程(LWP)來模擬線程的實現。Linux 系統并不會直接提供線程控制接口,而是通過封裝輕量級進程相關操作,提供了線程控制的接口。
為了使用戶能夠方便地操作線程,Linux 提供了 pthread
庫,這是一個標準的線程庫,也是個第三方庫,被存放在了系統及庫路徑下。封裝了操作系統底層的輕量級進程控制接口。用戶只需在編譯時添加 -lpthread
參數(告訴庫名),即可正常使用線程相關接口。
計算機哲學的體現:通過增加一層軟件抽象來簡化復雜度,解決操作系統對線程支持的不足。
?在?Linux
?中,封裝輕量級進程操作相關接口的庫稱為?pthread
?庫,即?原生線程庫,這個庫文件是所有?Linux
?系統都必須預載的,用戶使用多線程控制相關接口時,只需要指明使用?-lpthread
?庫,即可正常使用多線程控制相關接口
2、線程控制接口
2.1 線程創建
要想控制線程,得先創建線程。對于原生線程庫來說,創建線程使用的是 pthread_create
這個接口。
#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
參數詳解:
-
參數1 pthread_t:* 線程 ID,用于標識線程,本質上它是一個
unsigned long int
類型。
注:pthread_t*
表示這是一個輸出型參數,用于在創建線程后獲取新線程的 ID。 -
參數2 const pthread_attr_t:* 用于設置線程的屬性,如優先級、狀態、私有棧大小等。通常不需要特別處理,傳遞
nullptr
使用默認設置即可。 -
參數3 void *(start_routine) (void ): 這是一個非常重要的參數,類型為返回值為
void*
、參數也為void*
的函數指針。線程啟動時,會自動回調此函數(類似于signal
函數中的參數2)。 -
參數4 void:* 顯然,這個類型與回調函數中的參數類型相匹配,它是線程運行時傳遞給回調函數的參數。
返回值:
成功返回 0
,失敗返回錯誤號。錯誤檢查:
傳統的一些函數是,成功返回0,失敗返回-1,并且對全局變量errno賦值以指示錯誤。
pthreads函數出錯時不會設置全局變量errno(而大部分其他POSIX函數會這樣做)。而是將錯誤代碼通過返回值返回
pthreads同樣也提供了線程內的errno變量,以支持其它使用errno的代碼。對于pthreads函數的錯誤,建議通過返回值業判定,因為讀取返回值要比讀取線程內的errno變量的開銷更小
理解了創建線程函數的各個參數后,就可以嘗試創建一個線程了:
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;void* threadRun(void *arg) {while(true) {cout << "我是次線程,我正在運行..." << endl;sleep(1);}return nullptr;
}int main() {pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);while(true) {cout << "我是主線程 " << " 我創建了一個次線程 " << t << endl;sleep(1);}return 0;
}
這段代碼非常簡單。如果直接編譯,可能會引發報錯:
錯誤信息: 未定義 pthread_create
這個函數。
原因: 沒有指定使用原生線程庫,解決方法是在編譯時添加 -lpthread
來鏈接線程庫。
驗證原生線程庫是否存在:
你可以通過 ldd
命令查看已編譯程序的庫鏈接情況。例如,使用 ldd mythread
命令來查看是否成功鏈接到原生線程庫。
ps -al
命令中的 LWP 是內核中線程的 ID,也可以看作是線程在進程中的唯一標識符。用戶層的
pthread_t
是用戶空間的線程標識符,表示線程控制塊(TCB)的地址它與 LWP ID 是映射的。
程序運行時主線程和次線程的順序如何?
線程的執行順序由操作系統的調度器決定。多線程程序中的主線程和次線程執行順序不確定,具體執行順序依賴于調度器的調度策略。
2.1.1 一批線程
接下來我們演示如何創建一批線程。
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name) {while(true) {cout << "我是次線程 " << (char*)name << endl;sleep(1);}return nullptr;
}int main() {pthread_t pt[NUM];for(int i = 0; i < NUM; i++) {// 注冊新線程的信息char name[64];snprintf(name, sizeof(name), "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}while(true) {cout << "我是主線程,我正在運行" << endl;sleep(1);}return 0;
}
細節:
在傳遞 pthread_create
的參數時,可以通過 起始地址+偏移量
的方式進行傳遞,這樣每個線程就能接收到不同的參數信息。
預期結果: 打印出 thread-1
、thread-2
、thread-3
等。
實際結果: 五個次線程在運行,但打印出來的都是 thread-5
。
原因: char name[64]
是主線程棧區中的局部變量,多個線程共享這塊空間,最后一次的覆蓋導致每個線程都讀取到相同的數據。
解決方法: 在堆區動態分配空間,為每個線程分配獨立的內存區域,以確保信息的獨立性。
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name) {while(true) {cout << "我是次線程 " << (char*)name << endl;sleep(1);}delete[] (char*)name;return nullptr;
}int main() {pthread_t pt[NUM];for(int i = 0; i < NUM; i++) {// 注冊新線程的信息char *name = new char[64];snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}while(true) {cout << "我是主線程,我正在運行" << endl;sleep(1);}return 0;
}
?通過這種方式,程序運行將符合預期,每個線程都會打印出自己獨立的名稱。
2.2 線程等待
?線程等待 為什么需要線程等待?
已經退出的線程,其空間沒有被釋放,仍然在進程的地址空間內。
創建新的線程不會復用剛才退出線程的地址空間
主線程需要等待次線程。在原生線程庫中,提供了 pthread_join
來等待一個線程的運行結束。
#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
參數說明:
-
參數1 pthread_t: 待等待的線程 ID,本質上是一個無符號長整型類型。
-
參數2 void:這是一個輸出型參數,用于獲取次線程的退出結果。如果不關心返回值,可以傳遞
nullptr
。
返回值: 成功返回 0
,失敗返回錯誤號。
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name) {while(true) {cout << "我是次線程 " << (char*)name << endl;sleep(1);}delete[] (char*)name;return nullptr;
}int main() {pthread_t pt[NUM];for(int i = 0; i < NUM; i++) {// 注冊新線程的信息char *name = new char[64];snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}// 等待次線程運行結束for(int i = 0; i < NUM; i++) {int ret = pthread_join(pt[i], nullptr);if(ret != 0)cerr << "等待線程 " << pt[i] << " 失敗!" << endl;}cout << "所有線程都退出了" << endl;return 0;
}
該程序確保了主線程在等待所有次線程結束后才會退出,確保了線程的正常結束。
調用該函數的線程將掛起等待,直到id為thread的線程終止。thread線程以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:
1. 如果thread線程通過return返回,value_ ptr所指向的單元里存放的是thread線程函數的返回值。
2. 如果thread線程被別的線程調用pthread_ cancel異常終掉,value_ ptr所指向的單元里存放的是常數PTHREAD_ CANCELED。
3. 如果thread線程是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數。
4. 如果對thread線程的終止狀態不感興趣,可以傳NULL給value_ ptr參數?
2.3 線程終止
如果需要只終止某個線程而不終止整個進程,可以有三種方法:
1. 從線程函數return。這種方法對主線程不適用,從main函數return相當于調用exit。(主線程return 0 要開始合理使用了)
2. 線程可以調用pthread_ exit終止自己。
3. 一個線程可以調用pthread_ cancel終止同一進程中的另一個線程。
pthread_exit函數
功能:線程終止原型
void pthread_exit(void *value_ptr); 參數 value_ptr:value_ptr不要指向一個局部變量。返回值:無返回值,跟進程一樣,線程結束的時候無法返回到它的調用者(自身)
需要注意,pthread_exit或者return返回的指針所指向的內存單元必須是全局的或者是用malloc分配的,不能在線程函數的棧上分配,因為當其它線程得到這個返回指針時線程函數已經退出了
pthread_join
?中的?void **retval
?是一個輸出型參數,可以把一個?void *
?指針的地址傳遞給?pthread_join
?函數,當線程調用?pthread_exit
?退出時,可以根據此地址對?retval
?賦值,從而起到將退出信息返回給主線程的作用
為什么 pthread_join 中的參數2類型為 void**?
因為主線程和次線程此時并不在同一個棧幀中,要想遠程修改值就得傳地址,類似于 int -> &int,不過這里的 retval 類型是 void*
注意: 直接在 回調方法 中 return 退出信息,主線程中的 retval 也是可以得到信息的,因為類型都是 void*,彼此相互呼應
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name)
{cout << "我是次線程 " << (char*)name << endl;sleep(1);delete[] (char*)name;pthread_exit((void*)"EXIT");// 直接return "EXIT" 也是可以的// return (void*)"EXIT";
}int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注冊新線程的信息char *name = new char[64];snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}// 等待次線程運行結束void *retval = nullptr;for(int i = 0; i < NUM; i++){int ret = pthread_join(pt[i], &retval);if(ret != 0)cerr << "等待線程 " << pt[i] << " 失敗!" << endl;cout << "線程 " << pt[i] << " 等待成功,退出信息是 " << (const char*)retval << endl;}cout << "所有線程都退出了" << endl;return 0;
}
void*
?非常之強大,可以指向任意類型的數據,甚至是一個對象
?2.4 線程實戰
無論是 pthread_create
還是 pthread_join
,它們的參數都有一個共同點:包含了一個 void*
類型的參數。這意味著我們可以通過傳遞對象指針給線程,并在其中執行某些特定任務處理。
我們首先創建一個線程信息類,用于計算從 0
到 N
的累加和。線程信息包括:
-
線程名字(包括 ID)
-
線程編號
-
線程創建時間
-
待計算的值
N
-
計算結果
-
狀態
為了方便訪問成員,權限設置為 public
。
// 線程信息類的狀態
enum class Status
{OK = 0,ERROR
};// 線程信息類
class ThreadData
{
public:ThreadData(const string &name, int id, int n):_name(name), _id(id), _createTime(time(nullptr)), _n(n), _result(0), _status(Status::OK) {}public:string _name;int _id;time_t _createTime;int _n;int _result;Status _status;
};
此時就可以編寫回調方法中的業務邏輯了:
void* threadRun(void *arg)
{ThreadData *td = static_cast<ThreadData*>(arg);// 業務處理for(int i = 0; i <= td->_n; i++)td->_result += i;// 如果業務處理過程中出現異常,可以設置 _status 為 ERRORcout << "線程 " << td->_name << " ID " << td->_id << " CreateTime " << td->_createTime << " 完成..." << endl;pthread_exit((void*)td);
}
主線程在創建線程及等待線程時,使用 ThreadData
對象。在后續修改業務邏輯時,只需修改類及回調方法,而不需要更改創建及等待邏輯,這有效地做到了邏輯解耦。
int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注冊新線程的信息char name[64];snprintf(name, sizeof(name), "thread-%d", i + 1);// 創建對象ThreadData *td = new ThreadData(name, i, 100 * (10 + i));pthread_create(pt + i, nullptr, threadRun, td);sleep(1); // 盡量拉開線程創建時間}// 等待次線程運行結束void *retval = nullptr;for(int i = 0; i < NUM; i++){int ret = pthread_join(pt[i], &retval);if(ret != 0)cerr << "等待線程 " << pt[i] << " 失敗!" << endl;ThreadData *td = static_cast<ThreadData*>(retval);if(td->_status == Status::OK)cout << "線程 " << pt[i] << " 計算 [0, " << td->_n << "] 的累加和結果為 " << td->_result << endl;delete td;}cout << "所有線程都退出了" << endl;return 0;
}
程序運行時,各個線程能夠正確計算累加和。此示例展示了線程如何利用傳遞的對象指針進行任務處理。線程不僅可以用于計算,還可以擴展到其他領域,如網絡傳輸、密集型計算、多路 I/O 等,關鍵在于修改業務邏輯。
2.5 其他接口
與多線程相關的還有一批簡單但重要的接口,我們將一并介紹。
2.5.1 關閉線程pthread_cancel
線程不僅可以被創建,還可以被關閉。我們可以使用 pthread_cancel
來關閉已經創建并正在運行的線程。
#include <pthread.h> int pthread_cancel(pthread_t thread);
參數說明:
pthread_t thread
:表示被關閉的線程 ID。
返回值:
成功返回 0
,失敗返回錯誤號。
該函數使用成功后,線程會被異常信號殺死,退出碼為PTHREAD_CANCELED(-1),?
pthread_join()函數會等待成功,回收資源一樣成功,與pthread_detach.
detach是明確表明推出資源直接交由操作系統直接釋放,一種不關心的狀態,如果在用pthread_join等待的話,資源已經沒有,所以會等待失敗。
#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRun(void *arg)
{const char *ps = static_cast<const char*>(arg);while(true){cout << "線程 " << ps << " 正在運行" << endl;sleep(1);}pthread_exit((void*)10);
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, (void*)"Hello Thread");// 3秒后關閉線程sleep(3);pthread_cancel(t);void *retval = nullptr;pthread_join(t, &retval);cout << "線程 " << t << " 已退出,退出信息為 " << (int64_t)retval << endl;return 0;
}
運行結果:
程序運行 3 秒后,可以看到退出信息為 -1
,這是因為 pthread_cancel
關閉的線程,其退出信息統一為 PTHREAD_CANCELED
即 -1
。
2.5.2 獲取線程 ID pthread_self
線程 ID 是線程的唯一標識符,我們可以通過 pthread_self
獲取當前線程的 ID。
#include <pthread.h> pthread_t pthread_self(void);
#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRun(void *arg)
{cout << "當前次線程的ID為 " << pthread_self() << endl;return nullptr;
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);pthread_join(t, nullptr);cout << "創建的次線程ID為 " << t << endl;return 0;
}
結果:
pthread_self
返回當前線程的 ID,而 t
顯示的是主線程創建時的線程 ID。
2.5.3 線pthread_detach
父進程需要阻塞式等待子進程退出,主線程等待次線程時也是阻塞式等待。如果希望避免一直阻塞,我們可以使用線程分離。
默認情況下,新創建的線程是joinable的,線程退出后,需要對其進行pthread_join操作,否則無法釋放資源,從而造成系統泄漏。
如果不關心線程的返回值,join是一種負擔,這個時候,我們可以告訴系統,當線程退出時,自動釋放線程資源。
注意:?如果線程失去了?joinable
?屬性,就無法被?join
,如果?join
?就會報錯
#include <pthread.h>
int pthread_detach(pthread_t thread);
參數說明:
pthread_t thread
:待分離的線程 ID。
返回值:
成功返回 0
,失敗返回錯誤號。
#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRun(void *arg)
{int n = 3;while(n){cout << "次線程 " << n-- << endl;sleep(1);}
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);pthread_detach(t);int n = 5;while(n){cout << "主線程 " << n-- << endl;sleep(1);}return 0;
}
運行結果:
主線程和次線程并發執行,不需要擔心次線程的退出會導致主線程阻塞。
3. 深入理解線程
3.1 理解線程庫及線程 ID
在見識過原生線程庫提供的一批便利接口后,我們不禁感嘆庫的強大。那么,這樣一個強大的庫究竟是如何工作的呢?
原生線程庫本質上是一個存儲在 /lib64
目錄下的動態庫,想要使用這個庫,在編譯時必須加上 -lpthread
來指定鏈接動態庫。
程序運行時,原生線程庫需要從磁盤加載到內存中,并通過進程地址空間映射到共享區供線程使用。
由于用戶并不會直接操作輕量級進程的接口,因此需要借助第三方庫進行封裝,就像用戶可能不了解操作系統提供的文件接口一樣,而使用 C 語言封裝的 FILE
庫。
對于原生線程庫來說,線程不僅僅是一個,而是多個。因此,在線程庫中創建 TCB
(線程控制塊)結構,類似于進程的 PCB
(進程控制塊),其中存儲線程的各種信息,例如線程獨立棧信息等。
在內存中,整個線程庫就像一個“數組”,每一塊空間存儲了 TCB
信息,每個 TCB
的起始地址就表示當前線程的 ID。由于地址是唯一的,因此線程 ID也是唯一的。LWP ID 是內核為每個線程分配的唯一標識符,線程在內核中的標識。
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%x", t);return id;
}void *threadRun(void *arg)
{cout << "我是[次線程],我的ID是 " << toHex(pthread_self()) << endl;return (void*)0;
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);pthread_join(t, nullptr);cout << "我是[主線程],我的ID是 " << toHex(pthread_self()) << endl;return 0;
}
我們之前打印 pthread_t
類型的線程 ID 時,實際打印的就是地址,不過它是以十進制顯示的。我們可以通過一個函數將其轉換為十六進制顯示:
運行結果:
線程 ID 確實能轉換為地址(虛擬進程地址空間上的地址)。
注意: 即便是 C++11 提供的 thread
線程庫,在 Linux 平臺中運行時,也需要帶上 -lpthread
選項,因為它本質上是對原生線程庫的封裝。
3.2 理解線程獨立棧
線程之間存在獨立棧,保證它們在執行任務時不會相互干擾。我們可以通過以下代碼來驗證這一點:
多個線程使用同一個入口函數,并打印其中臨時變量的地址:
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%x", t);return id;
}void *threadRun(void *arg)
{int tmp = 0;cout << "thread " << toHex(pthread_self()) << " &tmp: " << &tmp << endl;return (void*)0;
}int main()
{pthread_t t[5];for(int i = 0; i < 5; i++){pthread_create(t + i, nullptr, threadRun, nullptr);sleep(1);}for(int i = 0; i < 5; i++)pthread_join(t[i], nullptr);return 0;
}
運行結果:
可以看到,五個線程打印出的臨時變量地址不相同,證明每個線程都有獨立的棧空間。
為什么 CPU 能夠區分這些棧結構呢?
答案是:通過棧頂指針 ebp
和棧底指針 esp
來進行切換。ebp
和 esp
是 CPU 中兩個非常重要的寄存器,即使是程序啟動時,也需要借助這兩個寄存器來為 main
函數開辟對應的棧區。
除了移動 esp
擴大棧區外,還可以同時移動 ebp
和 esp
來更改當前棧區。因此,在多線程中,棧區的切換是通過這兩個寄存器來完成的。
3.3 理解線程局部存儲
線程之間共享全局變量,操作全局變量時會影響其他線程:
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 100;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%x", t);return id;
}void *threadRun(void *arg)
{cout << "thread: " << toHex(pthread_self()) << " g_val: " << ++g_val << " &g_val: " << &g_val << endl;return (void*)0;
}int main()
{pthread_t t[3];for(int i = 0; i < 3; i++){pthread_create(t + i, nullptr, threadRun, nullptr);sleep(1);}for(int i = 0; i < 3; i++)pthread_join(t[i], nullptr);return 0;
}
運行結果:
在三個線程的影響下,g_val
最終變成了 103
。
如果想讓每個線程看到不同的全局變量,可以使用 __thread
修飾符,這樣全局變量就不再存儲在全局數據區,而是存儲到每個線程的局部存儲區中。
__thread int g_val = 100;
運行結果:
通過 __thread
修飾后,每個線程看到的 g_val
都是不同的,并且地址變大了。
解釋:
“全局變量” 的地址變大是因為它不再存儲在全局數據區,而是存儲在線程的局部存儲區中。線程的局部存儲區位于共享區,并且共享區的地址天然大于全局數據區。