【Linux】深入理解線程控制

一、線程等待的原理

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; }

【Linux】深入理解線程控制

通過上面的代碼和圖片,我們可以看到,新線程的輸出參數(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; }

【Linux】深入理解線程控制

通過觀察我們可以發(fā)現(xiàn),在相同線程的情況下,val 的值是遞增的,但對(duì)于不同的線程之間,val 值是沒有關(guān)系的。因此,我們通過關(guān)鍵字 __thread 實(shí)現(xiàn)了線程的局部存儲(chǔ),這些屬于每個(gè)線程的 val 的地址在線程的獨(dú)立中。

三、初步理解線程互斥

  1. 互斥的概念

    • 臨界資源多線程執(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ù)的共享,完成線程之間的交互。

  2. 需要互斥的原因

    在各個(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í)行兩遍:

第一遍:

【Linux】深入理解線程控制

第二遍:

【Linux】深入理解線程控制

我們發(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)程互斥的原因。

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點(diǎn)贊14 分享