一、線程等待的原理
pThread_join 函數(shù)用于實(shí)現(xiàn)線程等待。其中的 retval 參數(shù)用于傳遞目標(biāo)線程的退出狀態(tài)。當(dāng)目標(biāo)線程結(jié)束時(shí),pthread_join 會(huì)將目標(biāo)線程的退出狀態(tài)(即線程函數(shù)的返回值或通過 pthread_exit 傳遞的參數(shù))存儲(chǔ)在 *retval 所指向的內(nèi)存位置上。換句話說,pthread_join 會(huì)修改 retval 所指向的 void * 類型變量的值。
以下是相關(guān)的代碼示例:
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; int g_val = 100; void *threadRoutine(void *args) { const char *name = (const char *)args; int cnt = 5; while (true) { printf("%s, pid: %d, g_val: %d, &g_val: 0X%pn", name, getpid(), g_val, &g_val); sleep(1); cnt--; if (cnt == 0) break; } pthread_exit((void *)100); } int main() { pthread_t pid; pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1"); void *ret; pthread_join(pid, &ret); cout << "Thread returned: " << (long long int)ret << endl; return 0; }
通過上面的代碼和圖片,我們可以看到,新線程的輸出參數(shù)可以被主線程獲取,并且全局變量可以被所有線程訪問,是共享資源,因此全局函數(shù)也可以被所有線程訪問。
&ret 接收退出狀態(tài)的具體過程如下:當(dāng)調(diào)用 pthread_join 時(shí),pthread_join 會(huì)阻塞當(dāng)前線程,直到由 thread 參數(shù)指定的目標(biāo)線程終止。一旦目標(biāo)線程終止,pthread_join 會(huì)將該線程調(diào)用 pthread_exit 時(shí)傳遞的 void* 指針(即退出狀態(tài))賦值給 &ret 所指向的 void* 變量,即 ret。pthread_join 成功完成等待和狀態(tài)獲取后,會(huì)返回 0,表示操作成功,當(dāng)前線程可以繼續(xù)執(zhí)行后續(xù)代碼。
二、線程的局部存儲(chǔ)
全局變量是被所有線程共享的。如果我們的線程需要有自己的私有數(shù)據(jù),即只能自己訪問而其他線程不能訪問,我們可以在全局變量前加上關(guān)鍵字 __thread 來修飾,這是編譯器為我們提供的只能用來修飾內(nèi)置類型的關(guān)鍵字。
以下是相關(guān)的代碼示例:
#include <iostream> #include <pthread.h> #include <vector> #include <string> #include <unistd.h> using namespace std; #define NUM 3 int *p = nullptr; __thread int val = 100; class ThreadInfo { public: ThreadInfo(const string &threadname) : threadname_(threadname) {} public: string threadname_; }; string toHex(pthread_t tid) { char buffer[64]; snprintf(buffer, sizeof(buffer), "%p", tid); return buffer; } void *threadroutine(void *args) { int i = 0; ThreadInfo *ti = static_cast<ThreadInfo*>(args); while(i < 5) { printf("%s, tid: %s, pid: %d, val: %d, &val: 0X%pn", ti->threadname_.c_str(), toHex(pthread_self()).c_str(), getpid(), val, &val); val++; i++; sleep(1); } return nullptr; } int main() { vector<pthread_t> tids; vector<ThreadInfo> thread_datas; for(int i = 0; i < NUM; i++) { thread_datas.emplace_back("Thread-" + to_string(i + 1)); pthread_t tid; pthread_create(&tid, nullptr, threadroutine, &thread_datas.back()); tids.push_back(tid); } for(auto tid : tids) { pthread_join(tid, nullptr); } return 0; }
通過觀察我們可以發(fā)現(xiàn),在相同線程的情況下,val 的值是遞增的,但對(duì)于不同的線程之間,val 值是沒有關(guān)系的。因此,我們通過關(guān)鍵字 __thread 實(shí)現(xiàn)了線程的局部存儲(chǔ),這些屬于每個(gè)線程的 val 的地址在線程的獨(dú)立棧中。
三、初步理解線程互斥
-
互斥的概念
- 臨界資源:多線程執(zhí)行流共享的資源稱為臨界資源。
- 臨界區(qū):每個(gè)線程內(nèi)部,訪問臨界資源的代碼稱為臨界區(qū)。
- 互斥:任何時(shí)刻,有且只有一個(gè)執(zhí)行流進(jìn)入臨界區(qū),訪問臨界資源(對(duì)臨界資源起保護(hù)作用)。
- 原子性:不會(huì)被任何調(diào)度機(jī)制打斷的操作,是不可再分隔的動(dòng)作,該操作只有兩種狀態(tài),一是完成,二是未完成(早期化學(xué)中,原子是組成物質(zhì)的最小的不可分割的單位,在這樣的背景下提出的原子性)。
在大部分情況下,線程使用的數(shù)據(jù)都是局部變量,變量的地址空間在線程棧空間內(nèi),這種情況下,變量屬于單個(gè)線程,其他線程無法獲得這個(gè)變量。但有時(shí)候,很多變量需要在線程之間共享,這些變量被稱為共享變量,可以通過數(shù)據(jù)的共享,完成線程之間的交互。
-
需要互斥的原因
在各個(gè)線程訪問共享變量的時(shí)候,會(huì)出現(xiàn)多進(jìn)程并發(fā)的操作,可能會(huì)帶來一些問題。
下面是一個(gè)經(jīng)典的搶票問題,每個(gè)線程訪問到共享資源的票數(shù)就給它減一,就相當(dāng)于是搶走一張票。
以下是相關(guān)的代碼示例:
#include <iostream> #include <cstdio> #include <cstring> #include <vector> #include <unistd.h> #include <pthread.h> using namespace std; #define NUM 4 class threadData { public: threadData(int number) { threadname = "thread-" + to_string(number); } public: string threadname; }; int tickets = 1000; void *getTicket(void *args) { threadData *td = static_cast<threadData*>(args); const char *name = td->threadname.c_str(); while (true) { if(tickets > 0) { usleep(1000); printf("who=%s, get a ticket: %dn", name, tickets); tickets--; } else break; } printf("%s ... quitn", name); return nullptr; } int main() { vector<pthread_t> tids; vector<threadData> thread_datas; for (int i = 1; i <= NUM; i++) { thread_datas.emplace_back(i); pthread_t tid; pthread_create(&tid, nullptr, getTicket, &thread_datas.back()); tids.push_back(tid); } for(auto tid : tids) { pthread_join(tid, nullptr); } return 0; }
我們將程序執(zhí)行兩遍:
第一遍:
第二遍:
我們發(fā)現(xiàn),搶票怎么還能搶出第0票呢,甚至還有-1、-2票?而且竟然還有搶到一張票的情況,下面我們來詳解一下。
首先,如果我們只討論一個(gè)線程,整個(gè)搶票的過程就是,ticket 在內(nèi)存中,線程讀取 ticket,然后線程把 ticket 變量放到 CPU 上,CPU 進(jìn)行 — 操作,然后再放回內(nèi)存中,將原來的值覆蓋。我們這么說,這個(gè)過程是不是變得很慢了呢,所以在我們讀取 ticket 之后,其他線程也來讀取了,最后我們執(zhí)行一圈后,如果他們都是一起執(zhí)行完的,那么原來1000的值就變成了999,他們都搶到了第1000張票,這就是重復(fù)搶到同一張票的原因。出現(xiàn)負(fù)數(shù)也是這個(gè)原因,只不過不是同一時(shí)間做出返回內(nèi)存的行為,在 CPU 進(jìn)行計(jì)算的時(shí)候,要重新讀取數(shù)據(jù),如果開始時(shí)所有線程都 ticket==1,判斷這里就能過得去,然后一個(gè)線程拿到了最后一張票1,其他三個(gè)線程就拿到了“假票”0、-1、-2,這就是我們要進(jìn)行進(jìn)程互斥的原因。