環球今日訊![ Linux ] 線程獨立棧,線程分離,Linux線程互斥

2022-12-15 18:11:22 來源:51CTO博客

1.線程棧

我們使用的線程庫是用戶級線程庫(pthread),我們使用 ldd mythread 可以查看mythread的鏈接信息。


(資料圖片)

因此對于一個線程(tast_struct)都是通過在共享空間內執行pthread_create執行線程創建的。所有的代碼執行都是在進程的進程地址空間內進行執行的。在了解這些基本的概念之后,我們回顧上一篇的一個問題,pthread_t究竟是什么呢?

1.1pthread_t

上篇文章我就提到過pthread_t是一個無符號長整型整數,但是并沒有說pthread_t具體是什么?但是我們把他轉化為一個16進制數字時,我們發現這個數字特別想一個地址,那么這里我們需要確認的是,pthread_t 線程Id就是一個地址。而是什么地址呢?我們這里需要知道的是,線程的全部實現,并沒有全部體現在操作系統內,而是操作系統提供執行流,具體的線程結構由庫來進行管理。庫可以創建多個線程->因此庫也要管理線程。而庫要管理線程也是要先描述再組織。因此在共享區里面包含了struct thread_info,里面就會保存pthread_t tid,線程私有棧等。而申請一個新的線程,庫就又會在共享區內創建該線程對應的tid,私有棧等等。而返回的就是該結構的地址。因此pthread_t里面保存的就是對應用戶級線程的控制結構體的起始地址!

主線程的獨立棧結構,用的就是地址空間內的棧區新線程用的棧結構,用的是庫中提供的棧結構

pthread_t 到底是什么類型呢?取決于實現。對于Linux目前實現的NPTL(原生線程庫)實現而言,pthread_t類型的線程ID,本質就是一個進程地址空間上的一個地址。

Linux中,用戶級線程庫和LWP是1:1的。

1.2用戶級的線程id與內核LWP的對應關系

我們剛剛已經知道了用戶級線程id和內核LWP的對應是1:1的。那么我們如果使用代碼來驗證一下呢?

#include #include #include #include #include //僅僅是了解using namespace std;// 帶__thread 給每個線程拷一份__thread int global_value = 100;void *startRoutine(void *args){    while (true)    {        cout << "thread " << pthread_self() << " global_value: "             << global_value << " &global_value: " << &global_value             << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid) << endl;        sleep(1);    }}int main(){    pthread_t tid1;    pthread_t tid2;    pthread_t tid3;    pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");    pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");    pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");    pthread_join(tid1, nullptr);    pthread_join(tid2, nullptr);    pthread_join(tid3, nullptr);    return 0;}

我們同樣使用監控腳本來看看當前系統下的LWP

while :; do ps -aL |head -1 && ps -aL|grep mythread;sleep 1;done

通過打印的結果我們發現 是能夠看到用戶級線程id和內核LWP的對應是1:1的。

2.分離線程

默認情況下,新建線程是joinable的,joinable就是可join的,線程退出后,需要對其進行pthread_join操作,否則無法釋放資源,從而造成系統泄漏如果不關心線程的返回值,join的一種負擔;這個時候,我們可以告訴系統,當線程退出時,自動釋放線程資源。

在什么時候下會使用線程分離呢?

我們都知道主線程會join等待新線程,如果新線程一直不退出,主線程就會一直等待,等新線程退出之后釋放新線程的資源,這與我們的進程阻塞式等待類似。如果當主線程并不關心或者不需要新線程的退出碼時,新線程可以自己退出后自己釋放自己的資源。那么主線程就可以不需要等待新線程了。這就完成了線程間的解耦。也叫做線程分離。

2.1 pthread_detch

函數原型:int pthread_detach(pthread_t thread);可以是線程組內其他線程對目標線程進行分離,也可以是線程自己分離pthread_detach(pthread_self());

注意:joinable和分離是沖突的,一個線程不能即是joinable的又是分離的。

#include #include #include #include #include #include #include //僅僅是了解using namespace std;// 帶__thread 給每個線程拷一份__thread int global_value = 100;void *startRoutine(void *args){    //線程分離    //pthread_detach(pthread_self());    while (true)    {        cout << "thread " << pthread_self() << " global_value: "             << global_value << " &global_value: " << &global_value             << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid) << endl;        sleep(1);    }}int main(){    pthread_t tid1;    pthread_t tid2;    pthread_t tid3;    pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");    pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");    pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");    sleep(1);    //傾向于讓主線程分離其他線程    pthread_detach(tid1);    pthread_detach(tid2);    pthread_detach(tid3);    //一旦分離不能join    int n1 = pthread_join(tid1, nullptr);    cout<<"strerror(n1): "<< strerror(n1)<

通過這個實驗也驗證了一個線程不能即是detach又被join的。

3.線程互斥

3.1互斥相關概念

臨界資源:多線程執行流共享的資源就叫做臨界資源臨界區:每個線程內部,訪問臨界資源的代碼,就叫做臨界區互斥:任何時刻,互斥保證有且僅有一個執行流進入臨界區,訪問臨界資源,通常對臨界資源起保護作用原子性:不會被恩和調度機制打斷的操作,該操作只有兩種狀態,要么完成,要么未完成。

3.2 互斥量mutex

大部分情況,線程使用的數據都是局部變量,變量的地址空間在線程??臻g內,這種情況,變量歸屬單個線程,其他線程無法獲得這種變量。 但有時候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過數據的共享,完成線程之間的交互。 多個線程并發的操作共享變量,會帶來一些問題。

3.3 售票系統案例驗證共享變量會有問題

為了驗證共享變量會出問題的情況,我們模擬實現一個售票系統的案例。

#include #include #include #include #include #include #include  //僅僅是了解using namespace std;int tickets = 10000; // 臨界資源void *getTickets(void *args){    const char *name = static_cast(args);    while (true)    {        //臨界區        if (tickets > 0)        {            cout << name << " 搶到了票,票的編號是:" << tickets << endl;            tickets--;        }        else        {                        cout << name << " 已經放棄搶票了,因為沒有票了....." << endl;            break;        }    }    return nullptr;}int main(){    pthread_t tid1;    pthread_t tid2;    pthread_t tid3;    pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");    pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");    pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");    // sleep(1);    // // 傾向于讓主線程分離其他線程    // pthread_detach(tid1);    // pthread_detach(tid2);    // pthread_detach(tid3);    // 一旦分離不能join    int n1 = pthread_join(tid1, nullptr);    cout << "strerror(n1): " << strerror(n1) << endl;    int n2 = pthread_join(tid2, nullptr);    cout << "strerror(n2): " << strerror(n2) << endl;    int n3 = pthread_join(tid3, nullptr);    cout << "strerror(n3): " << strerror(n3) << endl;    return 0;}

執行結果我們可以發現,看似好像沒有什么問題,但是其實是存在bug的。在這段代碼中

這一段代碼是既對票做判斷,又對票做--,--是并不是由一條語句執行的,而是被翻譯成3條語句執行的。

CPU對tickets--這句話,要翻譯成:

取數據。將數據從內存取到cpu寄存器內load :將共享變量ticket從內存加載到寄存器中做運算。在寄存器內對數據進行運算。update : 更新寄存器里面的值,執行-1操作寫回數據。將數據從寄存器寫回內存。store :將新值,從寄存器寫回共享變量ticket的內存地址

我們可以看看ticket--部分的匯編代碼:

取出ticket--部分的匯編代碼

objdump -d a.out > test.objdump

152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34

153 400651: 83 e8 01 sub $0x1,%eax

154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34

而這3個步驟中,線程在任何地方都有可能切換走,而CPU內的寄存器是被所有的執行共享的,但是寄存器里面的數據是屬于當前執行流的上下文數據。因此線程要被切換的時候,需要保存上下文;線程要被換回的時候,需要恢復上下文。

因此為了從程序中看到可能錯誤的數據,我們需要加一個usleep來模擬漫長的業務過程,可能有很多個線程會進入該代碼段。

#include #include #include #include #include #include #include  //僅僅是了解using namespace std;// // 帶__thread 給每個線程拷一份// __thread int global_value = 100;// void *startRoutine(void *args)// {//     //線程分離//     //pthread_detach(pthread_self());//     while (true)//     {//         cout << "thread " << pthread_self() << " global_value: "http://              << global_value << " &global_value: " << &global_value//              << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid) << endl;//         sleep(1);//     }//}int tickets = 10000; // 臨界資源void *getTickets(void *args){    const char *name = static_cast(args);    while (true)    {        //臨界區        if (tickets > 0)        {            usleep(1000);//模擬漫長的業務            cout << name << " 搶到了票,票的編號是:" << tickets << endl;            tickets--;        }        else        {                        cout << name << " 已經放棄搶票了,因為沒有票了....." << endl;            break;        }    }    return nullptr;}int main(){    pthread_t tid1;    pthread_t tid2;    pthread_t tid3;    pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");    pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");    pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");    // 一旦分離不能join    int n1 = pthread_join(tid1, nullptr);    cout << "strerror(n1): " << strerror(n1) << endl;    int n2 = pthread_join(tid2, nullptr);    cout << "strerror(n2): " << strerror(n2) << endl;    int n3 = pthread_join(tid3, nullptr);    cout << "strerror(n3): " << strerror(n3) << endl;    return 0;}

此時我們確實看到了,產生了臟數據。

3.4 解決搶票問題

要解決以上的問題,我們需要做到三點:

代碼必須要有互斥行為:當代碼進入臨界區執行時,不允許其他線程進入該臨界區。如果多個線程同時要求執行臨界區的代碼,并且臨界區沒有線程在執行,那么只能允許一個線程進入該臨界區。如果線程不在臨界區中執行,那么該線程不能組織其他線程進入臨界區。

要做到這三點,本質上就是需要一把鎖。Linux上提供的這把鎖叫做互斥量。

在臨界區內,只能夠允許一個線程執行,不允許多個線程同時執行,因此一旦我們給買票的過程加上一把鎖,在某一時刻,只能夠允許一個線程買票,因此可以保證整個買票的過程是原子的。

3.5互斥量的接口

3.5.1初始化互斥量

申請鎖:

初始化互斥量的兩種方法:

方法一:靜態分配pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER、方法二:動態分配int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 參數: mutex:要初始化的互斥量 attr:NULL

3.5.2 銷毀互斥量

釋放鎖:

銷毀互斥量需要注意:

使用PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要銷毀不要銷毀一個已經加鎖的互斥量已經銷毀的互斥量,要確保后面不會有線程再次嘗試加鎖

函數原型:int pthread_mutex_destroy(pthread_mutex_t *mutex);

3.5.3 編碼

在我們上述的售票系統中,其中很明顯的是,票數tickets屬于臨界資源,我們需要對其進行加鎖。

在我們申請鎖成功之后,我們對互斥量進行加鎖和解鎖,我們將使用pthread_mutex_lock和pthread_mutex_unlock。

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失敗返回錯誤號

注意:我們加鎖只需要對臨界區加鎖,而且加鎖的粒度越細越好。而加鎖的本質是讓線程執行臨界區的代碼串行化。

調用pthread_mutex_lock時,可能會遇到一下情況:

互斥量處于未鎖狀態,該函數將會互斥量鎖定,同時返回成功發起函數調用時,其他線程已經鎖定互斥量,或者存在其他線程同時申請互斥量,但沒有競爭到互斥量,那么pthread_mutex_lock調用會陷入阻塞(執行流被掛起),等待互斥鎖解鎖。
#include #include #include #include #include #include #include  //僅僅是了解using namespace std;int tickets = 10000; // 臨界資源pthread_mutex_t mutex;//定義鎖void *getTickets(void *args){    const char *name = static_cast(args);    while (true)    {        //臨界區        pthread_mutex_lock(&mutex);        if (tickets > 0)        {            usleep(1000);            cout << name << " 搶到了票,票的編號是:" << tickets << endl;            tickets--;            pthread_mutex_unlock(&mutex);        }        else        {                        cout << name << " 已經放棄搶票了,因為沒有票了....." << endl;            pthread_mutex_unlock(&mutex);            break;        }    }    return nullptr;}int main(){    pthread_mutex_init(&mutex,nullptr);    pthread_t tid1;    pthread_t tid2;    pthread_t tid3;    pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");    pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");    pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");    // 一旦分離不能join    int n1 = pthread_join(tid1, nullptr);    cout << "strerror(n1): " << strerror(n1) << endl;    int n2 = pthread_join(tid2, nullptr);    cout << "strerror(n2): " << strerror(n2) << endl;    int n3 = pthread_join(tid3, nullptr);    cout << "strerror(n3): " << strerror(n3) << endl;    pthread_mutex_destroy(&mutex);    return 0;}

3.5.4 互斥鎖的相關問題

加鎖是一套規范,通過臨界區對臨界資源進行訪問的時候,要加就都需要加鎖,不能有的線程加鎖有的線程不加鎖。鎖保護的是臨界區,任何線程執行臨界區代碼訪問臨界資源,都必須現申請鎖,前提是都必須看到鎖!那么這把鎖本身也是臨界資源!而鎖的設計者也考慮了這個問題,pthread_mutex_lock線程競爭鎖的過程,就是原子的!

標簽: 臨界資源 共享變量

上一篇:每日播報!使用Rsync在 Linux 上傳輸文件的示例
下一篇:每日熱聞!狂神說 spring5