全方面了解linux多線程服務「linux多線程服務器編程」

1.什麼是線程?

linux內核中是沒有線程這個概念的,而是輕量級進程的概念:LWP。一般我們所說的線程概念是C庫當中的概念。

1.1線程是怎樣描述的?

線程實際上也是一個task_struct,工作線程拷貝主線程的task_struct,然後共用主線程的mm_struct。線程ID是在用task_struct中pid描述的,而task_struct中tgid是線程組ID,表示線程屬於該線程組,對於主線程而言,其pid和tgid是相同的,我們一般看到的進程ID就是tgid。

即:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

獲取線程ID和主線程ID的值:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

但是獲取該gettid系統調用接口並沒有被封裝起來,如果確實需要獲取線程ID,可使用:

#include <sys/syscall.h>
int TID = syscall(SYS_gettid);

則對線程組而言,所有的tgid一定是一樣的,所有的pid一定是不一樣的。主線程pid和tgid一樣,工作線程pid和tgid一定不一樣。

1.2如何查看一個線程的ID

命令:ps -eLf

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

上述polkitd進程是多線程的,進程ID為731,進程內有6個線程,線程ID為731,764,765,768,781,791。

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

1.3多線程如何避免調用棧混亂的問題?

工作線程和主線程共用一個mm_struct,如果都向棧中壓棧,必然會導致調用棧出錯。

實際上工作線程壓棧是壓了共享區,該共享區包含了許多線程獨有的資源。如圖:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

每一個線程,默認在共享區中佔有的空間為8M,可以使用ulimit -s修改。

進程是資源分配的基本單位,線程是調度的基本單位。

1.3.1線程獨有資源

  • 線程ID
  • 一組寄存器
  • errno
  • 信號屏蔽字
  • 調度優先級

1.3.2線程共享資源和環境

  • 文件描述符表
  • 信號的處理方式
  • 當前工作目錄
  • 用戶id和組id

1.4為什麼要有多線程?

舉個生活中的例子, 這就好比去銀行辦理業務。 到達銀行後, 首先取一個號碼, 然後坐下來安心等待。 這時候你一定希望, 辦理業務的窗口越多越好。 如果把整個營業大廳當成一個進程的話, 那麼每一個窗口就是一個工作線程。

1.4.1線程帶來的優勢

1、線程會共享內存地址空間。

2、創建線程花費的時間要少於創建進程花費的時間。

3、終止線程花費的時間要少於終止進程花費的時間。

4、線程之間上下文切換的開銷, 要小於進程之間的上下文切換。

5、線程之間數據的共享比進程之間的共享要簡單。

6、充分利用多處理器的可並行數量。(線程會提高運行效率,但當線程多到一定程度後,可能會導致效率下降,因為會有線程調度切換。)

1.4.2線程帶來的缺點

健壯性降低:多個線程之中, 只要有一個線程不夠健壯存在bug(如訪問了非法地址引發的段錯誤) , 就會導致進程內的所有線程一起完蛋。

線程模型作為一種並發的編程模型, 效率並沒有想像的那麼高, 會出現複雜度高、 易出錯、 難以測試和定位的問題。

【文章福利】需要C/C++ Linux服務器架構師學習資料加群812855908(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等)

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

1.5注意

1、並不是只有主線程才能創建線程, 被創建出來的線程同樣可以創建線程。

2、不存在類似於fork函數那樣的父子關係, 大家都歸屬於同一個線程組, 進程ID都相等, group_leader都指向主線程, 而且各有各的線程ID。

通過group_leader指針, 每個線程都能找到主線程。 主線程存在一個鏈表頭,後面創建的每一個線程都會鏈入到該雙向鏈表中。

3、並非只有主線程才能調用pthread_join連接其他線程, 同一線程組內的任意線程都可以對某線程執行pthread_join函數。

4、並非只有主線程才能調用pthread_detach函數, 其實任意線程都可以對同一線程組內的線程執行分離操作。

線程的對等關係:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

2.線程創建

接口:int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

參數解釋

1、thread:線程標識符,是一個出參

2、attr:線程屬性

3、star_routine:函數指針,保存線程入口函數的地址

4、arg:給線程入口函數傳參

返回值:成功返回0,失敗返回error number

詳解:

第一個參數是pthread_t類型的指針, 線程創建成功的話,會將分配的線程ID填入該指針指向的地址。 線程的後續操作將使用該值作為線程的唯一標識。

第二個參數是pthread_attr_t類型, 通過該參數可以定製線程的屬性, 比如可以指定新建線程棧的大小、 調度策略等。 如果創建線程無特殊的要求, 該值也可以是NULL, 表示採用默認屬性。

第三個參數是線程需要執行的函數。 創建線程, 是為了讓線程執行一定的任務。 線程創建成功之後, 該線程就會執行start_routine函數, 該函數之於線程, 就如同main函數之於主線程。

第四個參數是新建線程執行的start_routine函數的入參。

pthread_create錯誤碼及描述:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

2.1傳入參數arg的選擇

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

不要使用臨時變量傳參,使用堆上開闢的變量可以。

例:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *ThreadWork(void *arg)
{
  int *p = (int*)arg;
  printf("i am work thread:%p,   data:%dn",pthread_self(),*p);
  pthread_exit(NULL);
}
int main()
{
  int i = 1;
  pthread_t tid;
  int ret = pthread_create(&tid,NULL,ThreadWork,(void*)&i);//不要傳臨時變量,這裡是示範
  if(ret != 0)
  {
    perror("pthread_create");
    return -1;
  }
  while(1)
  {
    printf("i am main work threadn");
    sleep(1);
  }
  return 0;
}

2.2線程ID以及進程地址空間

線程獲取自身的ID:

#include <pthread.h>
pthread_t pthread_self(void);

判斷兩個線程ID是否對應着同一個線程:

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

返回為0時,則表示兩個線程為同一個線程,非0時,表示不是同一個線程。

用戶調用pthread_create函數時, 首先要為線程分配線程棧, 而線程棧的位置就落在共享區。 調用mmap函數為線程分配棧空間。 pthread_create函數分配的pthread_t類型的線程ID, 不過是分配出來的空間里的一個地址, 更確切地說是一個結構體的指針。

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

即:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

2.3線程注意點

1、線程ID是進程地址空間內的一個地址, 要在同一個線程組內進行線程之間的比較才有意義。 不同線程組內的兩個線程, 哪怕兩者的pthread_t值是一樣的, 也不是同一個線程。

2、線程ID就有可能會被複用:

1、線程退出。

2、線程組的其他線程對該線程執行了pthread_join, 或者線程退出前將分離狀態設置為已分離。

3、再次調用pthread_create創建線程。

2.4線程創建出來的默認值

線程創建的第二個參數是pthread_attr_t類型的指針, pthread_attr_init函數會將線程的屬性重置成默認值。

線程屬性及默認值:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

如果確實需要很多的線程, 可以調用接口來調整線程棧的大小:

#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t *attr,size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr,size_t *stacksize);

3.線程終止

線程終止,但進程不會終止的方法:

1、入口函數的return返回,線程就退出了

2、線程調用pthread_exit(NULL),誰調用誰退出

#include <pthread.h>

void pthread_exit(void *retval);

參數:retval是返回信息,」臨終遺言「,可以給可以不給

該變量不能使用臨時變量。

可使用:全局變量、堆上開闢的空間、字符串常量。

pthread_exit和線程啟動函數(start_routine) 執行return是有區別的。 在start_routine中調用的任何層級的函數執行pthread_exit() 都會引發線程退出, 而return, 只能是在start_routine函數內執行才能導致線程退出。

3、其它線程調用了pthread_cancel函數取消了該線程

int pthread_cancel(pthread_t thread);

thread:線程標識符

調用該函數的執行流可以取消其它線程,但是需要知道其它線程的線程標識符,也可以執行流自己取消自己,傳入自己的線程標識符。

如果線程組中的任何一個線程調用了exit函數, 或者主線程在main函數中執行了return語句, 那麼整個線程組內的所有線程都會終止。

4.線程等待

4.1線程等待接口

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

調用該函數,該執行流在等待線程退出的時候,該執行流是阻塞在pthread_joind當中的。

4.2線程等待和進程等待的不同

第一點不同之處是進程之間的等待只能是父進程等待子進程, 而線程則不然。線程組內的成員是對等的關係, 只要是在一個線程組內, 就可以對另外一個線程執行連接(join) 操作。

第二點不同之處是進程可以等待任一子進程的退出 , 但是線程的連接操作沒有類似的接口, 即不能連接線程組內的任一線程, 必須明確指明要連接的線程的線程ID。

pthread_join()錯誤碼:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

4.3為什麼要等待退出的線程?

如果不連接已經退出的線程, 會導致資源無法釋放。 所謂資源指的又是什麼呢?

1、已經退出的線程, 其空間沒有被釋放, 仍然在進程的地址空間之內。

2、新創建的線程, 沒有復用剛才退出的線程的地址空間。

如果不執行連接操作, 線程的資源就不能被釋放, 也不能被複用, 這就造成了資源的泄漏。

縱然調用了pthread_join, 也並沒有立即調用munmap來釋放掉退出線程的棧, 它們是被後建的線程復用了。 釋放線程資源的時候, 若進程可能再次創建線程, 而頻繁地munmap和mmap會影響性能, 所以將該棧緩存起來, 放到一個鏈表之中, 如果有新的創建線程的請求, 會首先在棧緩存鏈表中尋找空間合適的棧, 有的話, 直接將該棧分配給新創建的線程。

例:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>


void *ThreadWork(void *arg)
{
  int *p = (int*)arg;
  printf("pid :  %dn",syscall(SYS_gettid));
  printf("i am work thread:%p,   data:%dn",pthread_self(),*p);
  sleep(3);
  pthread_exit(NULL);
}

int main()
{
  int i = 1;
  pthread_t tid;
  int ret = pthread_create(&tid,NULL,ThreadWork,(void*)&i);//不要傳臨時變量,這裡是示範
  if(ret != 0)
  {
    perror("pthread_create");
    return -1;
  }
  pthread_join(tid,NULL);//線程等待
  while(1)
  {
    printf("i am main work threadn");
    sleep(1);
  }
  return 0;
}

5.線程分離

接口:#include <pthread.h>
int pthread_detach(pthread_t thread);

默認情況下, 新創建的線程處於可連接(Joinable) 的狀態, 可連接狀態的線程退出後, 需要對其執行連接操作, 否則線程資源無法釋放, 從而造成資源泄漏。

如果其他線程並不關心線程的返回值, 那麼連接操作就會變成一種負擔: 你不需要它, 但是你不去執行連接操作又會造成資源泄漏。 這時候你需要的東西只是:線程退出時, 系統自動將線程相關的資源釋放掉, 無須等待連接。

可以是線程組內其他線程對目標線程進行分離, 也可以是線程自己執行pthread_detach函數。

線程的狀態之中, 可連接狀態和已分離狀態是衝突的, 一個線程不能既是可連接的, 又是已分離的。 因此, 如果線程處於已分離的狀態, 其他線程嘗試連接線程時, 會返回EINVAL錯誤。

pthread_detach錯誤碼:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

注意:這裡的已分離不是指線程失去控制,不歸線程組管,而是指線程退出後,系統會自動釋放線程資源。若是線程組內的任意線程執行了exit函數,即使是已分離的線程,也仍會收到影響,一併退出。

6.線程安全

線程安全中涉及到的概念:

臨界資源:多線程中都能訪問到的資源
臨界區:每個線程內部,訪問臨界資源的代碼,就叫臨界區

6.1什麼是線程不安全?

多個線程訪問同一塊臨界資源,導致資源產生二義性的現象。

6.1.1舉一個例子

  • 假設現在有兩個線程A和B,單核CPU的情況下,此時有一個int類型的全局變量為100,A和B的入口函數都要對這個全局變量進行–操作。
  • 線程A先拿到CPU資源後,對全局變量進行–操作並不是原子性操作,也就是意味着,A在執行–的過程中有可能會被打斷。假設A剛剛將全局變量的值讀到寄存器當中,就被切換出去了,此時程序計數器保存了下一條執行的指令,上下文信息保存寄存器中的值,這兩個東西是用來線程A再次拿到CPU資源後,恢復現場使用的。
  • 此時,線程B拿到了CPU資源,對全局變量進行了–操作,並且將100減為了99,回寫到了內存中。
  • A再次擁有了CPU資源後,恢復現場,繼續往下執行,從寄存器中讀到的值仍為100,減完之後為99,回寫到內存中為99。

上述例子中,線程A和B都對全局變量進行了–操作,全局變量的值應該變為98,但程序現在實際的結果為99,所以這就導致了線程不安全。

6.2如何解決線程不安全現象?

解決方案只需做到下述三點即可:

1、代碼必須要有互斥的行為: 當一個線程正在臨界區中執行時, 不允許其他線程進入該臨界區中。

2、如果多個線程同時要求執行臨界區的代碼, 並且當前臨界區並沒有線程在執行, 那麼只能允許一個線程進入該臨界區。

3、如果線程不在臨界區中執行, 那麼該線程不能阻止其他線程進入臨界區。

則本質上,我們需要對該臨界區加一把鎖:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

鎖是一個很普遍的需求, 當然用戶可以自行實現鎖來保護臨界區。 但是實現一個正確並且高效的鎖非常困難。 縱然拋下高效不談, 讓用戶從零開始實現一個正確的鎖也並不容易。 正是因為這種需求具有普遍性, 所以Linux提供了互斥量。

6.3互斥量接口

6.3.1互斥量的初始化

1、靜態分配:

#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

2、動態分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

調用int pthread_mutex_init()函數後,互斥量是處於沒有加鎖的狀態。

6.3.2互斥量的銷毀

int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意:

1、使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量無須銷毀。

2、不要銷毀一個已加鎖的互斥量, 或者是真正配合條件變量使用的互斥量。

3、已經銷毀的互斥量, 要確保後面不會有線程再嘗試加鎖。

當互斥量處於已加鎖的狀態, 或者正在和條件變量配合使用, 調用pthread_mutex_destroy函數會返回EBUSY錯誤碼。

6.3.3互斥量的加鎖

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

第一個接口:int pthread_mutex_lock(pthread_mutex_t *mutex);

1、該接口是阻塞加鎖接口。

2、mutex為傳入互斥鎖變量的地址

3、如果mutex當中的計數器為1,pthread_mutex_lock接口就返回了,表示加鎖成功,同時計數器當中的值會被更改為0.

4、如果mutex當中的計數器為0,pthread_mutex_lock接口就阻塞了,pthread_mutex_lock接口沒有返回了,阻塞在函數內部,直到加鎖成功

第二個接口:int pthread_mutex_trylock(pthread_mutex_t *mutex);

1、該接口為非阻塞接口

2、mutex中計數器為1時,加鎖成功,計數器置為0,然後返回

3、mutex中計數器為0時,加鎖失敗,但也會返回,此時加鎖是失敗狀態,一定不要去訪問臨界資源

4、非阻塞接口一般都需要搭配循環來使用。

第三個接口:int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

1、帶有超時時間的加鎖接口

2、不能直接獲取互斥鎖的時候,會等待abs_timeout時間

3、如果在這個時間內加鎖成功了,直接返回,不需要再繼續等待剩餘的時間,並且表示加鎖成功

4、如果超出了該時間,也返回了,但是加鎖失敗了,需要循環加鎖

上述三個加鎖接口,第一個接口用的最多。

6.3.4互斥量的解鎖

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  1. 對上述所有的加鎖接口,都可使用該函數解鎖
  2. 解鎖的時候,會將互斥鎖當中計數器的值從0變為1,表示其它線程可以獲取互斥量

6.4互斥鎖的本質

1、在互斥鎖內部有一個計數器,其實就是互斥量,計數器的值只能為0或者為1

2、當線程獲取互斥鎖的時候,如果計數器當前值為0,表示當前線程不能獲取到互斥鎖,也就是沒有獲取到互斥鎖,就不要去訪問臨界資源

3、當前線程獲取互斥鎖的時候,如果計數器當前值為1,表示當前線程可以獲取到互斥鎖,也就是意味着可以訪問臨界資源

6.5互斥鎖中的計數器如何保證了原子性?

獲取鎖資源的時候(加鎖):

1、寄存器當中值直接賦值為0

2、將寄存器當中的值和計數器當中的值進行交換

3、判斷寄存器當中的值,得出加鎖結果

兩種情況:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

例:4個線程,對同一個全局變量進行減減操作

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
#define NUMBER 4
int g_val = 100;

pthread_mutex_t mutex;//定義互斥鎖


void *ThreadWork(void *arg)
{
  int *p = (int*)arg;
  pthread_detach(pthread_self());//自己分離自己,不用主線程回收它的資源了
  while(1)
  {
    pthread_mutex_lock(&mutex);//加鎖
    if(g_val > 0)
    {
      printf("i am pid : %d,i get g_val : %dn",(int)syscall(SYS_gettid),g_val);
      --g_val;
      usleep(2);
    }
    else{
      pthread_mutex_unlock(&mutex);//在所有可能退出的地方,進行解鎖
      break;
    }
    pthread_mutex_unlock(&mutex);//解鎖

  }
  pthread_exit(NULL);
}

int main()
{
  pthread_t tid[NUMBER];
  pthread_mutex_init(&mutex,NULL);//互斥鎖初始化
  int i = 0;
  for(;i < NUMBER;++i)
  {
    int ret = pthread_create(&tid[i],NULL,ThreadWork,(void*)&g_val);//不要傳臨時變量,這裡是示範
    if(ret != 0)
    {
      perror("pthread_create");
      return -1;
     }
  }
  //pthread_join(tid,NULL);//線程等待
  //pthread_detach(tid);//線程分離
  pthread_mutex_destroy(&mutex);//銷毀互斥鎖
  while(1)
  {
    printf("i am main work threadn");
    sleep(1);
  }
  return 0;
}

6.6互斥鎖公平嘛?

互斥鎖是不公平的。

內核維護等待隊列, 互斥量實現了大體上的公平; 由於等待線程被喚醒後, 並不自動持有互斥量, 需要和剛進入臨界區的線程競爭(搶鎖), 所以互斥量並沒有做到先來先服務。

6.7互斥鎖的類型

1、PTHREAD_MUTEX_NORMAL: 最普通的一種互斥鎖。 它不具備死鎖檢測功能, 如線程對自己鎖定的互斥量再次加鎖, 則會發生死鎖。

2、
PTHREAD_MUTEX_RECURSIVE_NP: 支持遞歸的一種互斥鎖, 該互斥量的內部維護有互斥鎖的所有者和一個鎖計數器。 當線程第一次取到互斥鎖時, 會將鎖計數器置1, 後續同一個線程再次執行加鎖操作時, 會遞增該鎖計數器的值。 解鎖則遞減該鎖計數器的值, 直到降至0, 才會真正釋放該互斥量, 此時其他線程才能獲取到該互斥量。 解鎖時, 如果互斥量的所有者不是調用解鎖的線程, 則會返回EPERM。

3、
PTHREAD_MUTEX_ERRORCHECK_NP: 支持死鎖檢測的互斥鎖。 互斥量的內部會記錄互斥鎖的當前所有者的線程ID(調度域的線程ID) 。 如果互斥量的持有線程再次調用加鎖操作, 則會返回EDEADLK。 解鎖時, 如果發現調用解鎖操作的線程並不是互斥鎖的持有者, 則會返回EPERM。

4、自旋鎖,自旋鎖採用了和互斥量完全不同的策略, 自旋鎖加鎖失敗, 並不會讓出CPU, 而是不停地嘗試加鎖, 直到成功為止。 這種機制在臨界區非常小且對臨界區的爭奪並不激烈的場景下, 效果非常好。自旋鎖的效果好, 但是副作用也大, 如果使用不當, 自旋鎖的持有者遲遲無法釋放鎖, 那麼, 自旋接近於死循環, 會消耗大量的CPU資源, 造成CPU使用率飆高。 因此, 使用自旋鎖時, 一定要確保臨界區儘可能地小, 不要有系統調用, 不要調用sleep。 使用strcpy/memcpy等函數也需要謹慎判斷操作內存的大小, 以及是否會引起缺頁中斷。

5、PTHREAD_MUTEX_ADAPTIVE_NP:自適應鎖,首先與自旋鎖一樣, 持續嘗試獲取, 但過了一定時間仍然不能申請到鎖, 就放棄嘗試, 讓出CPU並等待。 PTHREAD_MUTEX_ADAPTIVE_NP類型的互斥量, 採用的就是這種機制。

6.8死鎖和活鎖

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

線程1已經成功拿到了互斥量1, 正在申請互斥量2, 而同時在另一個CPU上,線程2已經拿到了互斥量2, 正在申請互斥量1。 彼此佔有對方正在申請的互斥量,結局就是誰也沒辦法拿到想要的互斥量, 於是死鎖就發生了。

6.8.1死鎖概念

死鎖是指在一組進程中的各個進程均佔有不會釋放的資源,但因互相申請被其它進程所佔有不會釋放的資源而處於一種永久等待的狀態。

6.8.2死鎖的四個必要條件

1、互斥條件:一個資源只能被一個執行流使用

2、請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源不會釋放

3、不剝奪條件:一個執行流已獲得的資源,在未使用完之前,不能強行剝奪

4、循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源的關係

6.8.3避免死鎖

1、破壞死鎖的四個必要條件(實際上只能破壞條件2和4)

2、加鎖順序一致(按照先後順序申請互斥鎖)

3、避免未釋放鎖的情況

4、資源一次性分配

6.8.4活鎖

避免死鎖的另一種方式是嘗試一下,如果取不到鎖就返回。

int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abs_timeout);

這兩個函數反映了一種,不行就算了的思想。

trylock不行就回退的思想有可能會引發活鎖(live lock) 。 生活中也經常遇到兩個人迎面走來, 雙方都想給對方讓路, 但是讓的方向卻不協調, 反而互相堵住的情況 。 活鎖現象與這種場景有點類似。

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

線程1首先申請鎖mutex_a後, 之後嘗試申請mutex_b, 失敗以後, 釋放mutex_a進入下一輪循環, 同時線程2會因為嘗試申請mutex_a失敗,而釋放mutex_b, 如果兩個線程恰好一直保持這種節奏, 就可能在很長的時間內兩者都一次次地擦肩而過。 當然這畢竟不是死鎖, 終究會有一個線程同時持有兩把鎖而結束這種情況。 儘管如此, 活鎖的確會降低性能。

6.8.5死鎖調試

查看多個線程堆棧:thread apply all bt
跳轉到線程中:t 線程號
查看具體的調用堆棧:f 堆棧號
直接從pid號用gdb調試:gdb attach pid

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
#define NUMBER 2

pthread_mutex_t mutex1;//定義互斥鎖
pthread_mutex_t mutex2;


void *ThreadWork1(void *arg)
{
  int *p = (int*)arg;
  pthread_mutex_lock(&mutex1);
  
  sleep(2);
  
  pthread_mutex_lock(&mutex2);
  pthread_mutex_unlock(&mutex2);
  pthread_mutex_unlock(&mutex1);
  return NULL;
}

void *ThreadWork2(void *arg)
{
  int *p = (int*)arg;
  pthread_mutex_lock(&mutex2);
  
  sleep(2);
  
  pthread_mutex_lock(&mutex1);
  pthread_mutex_unlock(&mutex1);
  pthread_mutex_unlock(&mutex2);
  return NULL;
}
int main()
{
  pthread_t tid[NUMBER];
  pthread_mutex_init(&mutex1,NULL);//互斥鎖初始化
  pthread_mutex_init(&mutex2,NULL);//互斥鎖初始化
  int i = 0;
  int ret = pthread_create(&tid[0],NULL,ThreadWork1,(void*)&i);
  if(ret != 0)
  {
    perror("pthread_create");
    return -1;
  }
  ret = pthread_create(&tid[1],NULL,ThreadWork2,(void*)&i);
  if(ret != 0)
  {
    perror("pthread_create");
    return -1;
  }
  //pthread_join(tid,NULL);//線程等待
  //pthread_join(tid,NULL);//線程等待
  //pthread_detach(tid);//線程分離
  pthread_join(tid[0],NULL);
  pthread_join(tid[1],NULL);
  pthread_mutex_destroy(&mutex1);//銷毀互斥鎖
  pthread_mutex_destroy(&mutex2);//銷毀互斥鎖
  while(1)
  {
    printf("i am main work threadn");
    sleep(1);
  }
  return 0;
}

在上述代碼中,一定會出現死鎖,線程1拿到了互斥鎖1,又再去申請線程2的互斥鎖2,線程2拿到了互斥鎖2又再去申請線程1的互斥鎖1。

開始調試:

1、找到進程號

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

2、開始調試

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

3、查看多個線程堆棧

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

4、跳轉到線程中

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

5、查看具體調用堆棧

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

6、查看互斥鎖1和互斥鎖2,分別被誰拿着

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

6.9讀寫鎖

6.9.1什麼是讀寫鎖?

大部分情況下,對於共享變量的訪問特點:只是讀取共享變量的值,而不是修改,只有在少數情況下,才會真正的修改共享變量的值。

在這種情況下,讀請求之間是同步的,它們之間的並發訪問是安全的。然而寫請求必須鎖住讀請求和其它寫請求。

即讀線程可多個同時讀,而寫線程只允許同一時間內一個線程去寫。

6.9.2讀寫鎖接口

#include <pthread.h>
//銷毀
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

//初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
              const pthread_rwlockattr_t *restrict attr);
「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

讀寫鎖的默認屬性:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

對於調用pthread_rwlock_init初始化的讀寫鎖,在不需要讀寫鎖的時候,需要調用pthread_rwlock_destroy銷毀。

6.9.3讀者加鎖

#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //阻塞類型的讀加鎖接口
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); //非阻塞類型的讀加鎖接口

最大的好處就是,允許多個線程以只讀加鎖的方式獲取到讀寫鎖;

本質上,讀寫鎖的內部維護了一個引用計數,每當線程以讀方式獲取讀寫鎖時,該引用計數+1;

當釋放以讀加鎖的方式的讀寫鎖時,會先對引用計數進行-1,直到引用計數的值為0的時候,才真正釋放了這把讀寫鎖。

6.9.4寫者加鎖

#include <pthread.h>
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);// 非阻塞寫
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//阻塞寫

寫鎖用的是獨佔模式,如果當前讀寫鎖被某寫線程佔用着,則不允許任何讀鎖通過請求,也不允許任何寫鎖請求通過,讀鎖請求和寫鎖請求都要陷入阻塞,直到線程釋放寫鎖。

6.9.5 解鎖

#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

不論是讀者加鎖還是寫者加鎖,都採用該接口進行解釋。

讀者解鎖,只有當引用計數為0的時候,才真正釋放了讀寫鎖。

6.9.6讀寫鎖的競爭策略

對於讀寫鎖而言,目前有兩種策略,讀者優先和攜着優先;

讀寫鎖的類型有如下幾種:

PTHREAD_RWLOCK_PREFER_READER_NP, //讀者優先
PTHREAD_RWLOCK_PREFER_WRITER_NP, //很唬人, 但是也是讀者優先
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP, //寫者優先
PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP

讀者優先:讀鎖來請求可以立即響應,只要有一個讀鎖沒完成,那麼寫鎖就無法寫。這種策略是不公平的,極端情況下,寫現場很可能被餓死,即線程總是拿不到鎖資源。

寫者優先:只要線程申請了寫鎖,那麼在寫鎖後面到來的讀鎖請求就會統統被阻塞,不能先於寫鎖拿到鎖。

讀寫鎖實現中的變量及含義

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

對於讀請求而言:如果

1. 無線程持有寫鎖,即_writer = 0.

2. 採用讀者優先策略或者當前沒有寫鎖申請請求,即 _nr_writers_queue = 0

3. 當滿足這兩個條件時,讀鎖請求立即獲得讀鎖,返回之前執行_nr_readers++,表示多了一個線程正在讀

4. 不滿足這兩個條件時,執行_nr_readers_queued++,表示增加了一個讀鎖等待者,然後調用futex,陷入阻塞。醒來之後,執行_nr_readers_queued- -,再次判斷是否滿足條件1,2

對於寫請求而言:如果

1. 無線程持有寫鎖,即_writer = 0.

2. 沒有線程持有讀鎖,即_nr_readers = 0.

3. 如果上述條件滿足,就會立即拿到鎖,將_writer 置為當前線程的ID

4. 如果不滿足,則執行_nr_writers_queue++, 表示增加了一個寫鎖等待者線程,然後執行futex陷入等待。醒來後,先執行_nr_writers_queue- -,再繼續判斷條件1,2

對於解鎖,如果當前是寫鎖:

1. 執行_writer = 0.,表示釋放寫鎖。

2. 根據_nr_writers_queue判斷有沒有寫鎖,如果有則喚醒一個寫鎖,如果沒有寫鎖等待者,則喚醒所有的讀鎖等待者。

對於解鎖,如果當前是讀鎖:

1. 執行_nr_readers- -,表示讀鎖佔有者少了一個。

2. 判斷_nr_readers是否等於0,是的話則表示當前線程是最後一個讀鎖佔有者,需要喚醒寫鎖等待者或讀鎖等待者

3. 根據_nr_writers_queue判斷是否存在寫鎖等待者,若有,則喚醒一個寫鎖等待線程

4. 如果沒有寫鎖等待者,判斷是否存在讀鎖等待者,若有,則喚醒全部的讀鎖等待者

讀寫鎖很容易造成,讀者餓死或者寫者餓死。

也可以設計公平的讀寫鎖。

代碼:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <fcntl.h>

#define THREADCOUNT 100

static int count = 0;
static pthread_rwlock_t lock;

void* Read(void* i)
{
  while(1)
  {
    pthread_rwlock_rdlock(&lock);
    printf("i am 讀線程 : %d, 現在的count是%dn", (int)syscall(SYS_gettid), count);
    pthread_rwlock_unlock(&lock);
    //sleep(1);
  }
}

void* Write(void* i)
{
  while(1)
  {
    pthread_rwlock_wrlock(&lock);
    ++count;
    printf("i am 寫線程 : %d, 現在的count是: %dn", (int)syscall(SYS_gettid), count);
    pthread_rwlock_unlock(&lock);
    sleep(1);
  }
}


int main()
{
  //close(1);
  //int fd = open("./dup2_result.txt", O_CREAT | O_RDWR);
  //dup2(fd, 1);
  pthread_t tid[THREADCOUNT];
  pthread_rwlock_init(&lock, NULL);
  for(int i = 0; i < THREADCOUNT; ++i)
  {
    if(i % 2 == 0)
    {
      pthread_create(&tid[i], NULL, Read, (void*)&i);
    }
    else
    {
      pthread_create(&tid[i], NULL, Write, (void*)&i);
    }
  }

  for(int i = 0; i < THREADCOUNT; ++i)
  {
    pthread_join(tid[i], NULL);
  }

  pthread_rwlock_destroy(&lock);
  return 0;
}

上述代碼很容易觸發線程餓死。
讀餓死或者寫餓死。

7.線程間同步

7.1為什麼需要線程同步?

線程同步是為了對臨界資源訪問的合理性。

例如:

就像工廠里生產車間沒有原料了, 所有生產車間都停工了, 工人們都在車間睡覺。 突然進來一批原料, 如果原料充足, 你會發廣播給所有車間, 原料來了, 快來開工吧。 如果進來的原料很少, 只夠一個車間開工的, 你可能只會通知一個車間開工。

7.2如何做到線程間同步?

條件等待是線程間同步的另一種方法。

如果條件不滿足, 它能做的事情就是等待, 等到條件滿足為止。 通常條件的達成, 很可能取決於另一個線程, 比如生產者-消費者模型。 當另外一個線程發現條件符合的時候, 它會選擇一個時機去通知等待在這個條件上的線程。 有兩種可能性, 一種是喚醒一個線程, 一種是廣播, 喚醒其他線程。

則在這個情況下,需要做到:

1、線程在條件不滿足的情況下, 主動讓出互斥量, 讓其他線程去折騰, 線程在此處等待, 等待條件的滿足;

2、一旦條件滿足, 線程就可以立刻被喚醒。

3、線程之所以可以安心等待, 依賴的是其他線程的協作, 它確信會有一個線程在發現條件滿足以後, 將向它發送信號, 並且讓出互斥量。

7.3條件變量

本質上是PCB等待隊列 + 等待接口 + 喚醒接口。

7.3.1條件變量的初始化

靜態初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

動態初始化

pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

7.3.2條件變量的等待

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict conpthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);

為什麼這兩個接口中有互斥鎖?

條件不會無緣無故地突然變得滿足了, 必然會牽扯到共享數據的變化。 所以一定要有互斥鎖來保護。 沒有互斥鎖, 就無法安全地獲取和修改共享數據。

同步並沒有保證互斥,而保證互斥是使用到了互斥鎖。

pthread_mutex_lock(&m)
while(condition_is_false)
{
	pthread_mutex_unlock(&m);
	//解鎖之後, 等待之前, 可能條件已經滿足, 信號已經發出, 但是該信號可能會被錯過
	cond_wait(&cv);
	pthread_mutex_lock(&m);
}

上面的解鎖和等待不是原子操作。 解鎖以後, 調用cond_wait之前,如果已經有其他線程獲取到了互斥量, 並且滿足了條件, 同時發出了通知信號, 那麼cond_wait將錯過這個信號, 可能會導致線程永遠處於阻塞狀態。 所以解鎖加等待必須是一個原子性的操作, 以確保已經註冊到事件的等待隊列之前, 不會有其他線程可以獲得互斥量。

那先註冊等待事件, 後釋放鎖不行嗎? 注意, 條件等待是個阻塞型的接口, 不單單是註冊在事件的等待隊列上, 線程也會因此阻塞於此, 從而導致互斥量無法釋放, 其他線程獲取不到互斥量, 也就無法通過改變共享數據使等待的條件得到滿足, 因此這就造成了死鎖。

pthread_mutex_lock(&m);
while(condition_is_false)
	pthread_cond_wait(&v,&m);//此處會阻塞
/*如果代碼運行到此處, 則表示我們等待的條件已經滿足了,
*並且在此持有了互斥量
*/
/*在滿足條件的情況下, 做你想做的事情。
*/
pthread_mutex_unlock(&m);

pthread_cond_wait函數只能由擁有互斥量的線程來調用, 當該函數返回的時候, 系統會確保該線程再次持有互斥量, 所以這個接口容易給人一種誤解, 就是該線程一直在持有互斥量。 事實上並不是這樣的。 這個接口向系統聲明了我在PCB等待序列中之後, 就把互斥量給釋放了。 這樣其他線程就有機會持有互斥量,操作共享數據, 觸發變化, 使線程等待的條件得到滿足。

pthread_cond_wait內部會進行解鎖邏輯,則一定要先放到PCB等待序列中,再進行解鎖。
while(condition_is_false)
	pthread_cond_wait(&v,&m);//此處會阻塞
if(condition_is_false)
	pthread_cond_wait(&v,&m);//此處會阻塞

喚醒以後, 再次檢查條件是否滿足, 是不是多此一舉?

因為喚醒中存在虛假喚醒(spurious wakeup) , 換言之,條件尚未滿足, pthread_cond_wait就返了。 在一些實現中, 即使沒有其他線程向條件變量發送信號, 等待此條件變量的線程也有可能會醒來。

條件滿足了發送信號, 但等到調用pthread_cond_wait的線程得到CPU資源時, 條件又再次不滿足了。 好在無論是哪種情況, 醒來之後再次測試條件是否滿足就可以解決虛假等待的問題。

pthread_cond_wait內部實現邏輯:

  1. 將調用pthread_cond_wait函數的執行流放入到PCB等待隊列當中
  2. 解鎖
  3. 等待被喚醒
  4. 被喚醒之後:

1、從PCB等待隊列中移除出來

2、搶佔互斥鎖

情況1:拿到互斥鎖,pthread_cond_wait就返回了

情況2:沒有拿到互斥鎖,阻塞在pthread_cond_wait內部搶鎖的邏輯中

當阻塞在pthread_cond_wait函數搶鎖邏輯中時,一旦執行流時間耗盡,意味着線程就被切換出來了,程序計數器就保存的是搶鎖的指令,上下文信息保存的就是寄存器的值

當再次擁有CPU資源後,恢復搶鎖邏輯

直到搶鎖成功,pthread_cond_wait函數才會返回

7.3.3條件變量的喚醒

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_signal負責喚醒等待在條件變量上的一個線程。

pthread_cond_broadcast,就是廣播喚醒等待在條件變量上的所有線程。

先發送信號,然後解鎖互斥量,這個順序是必須的嘛?

先通知條件變量、 後解鎖互斥量, 效率會比先解鎖、 後通知條件變量低。 因為先通知後解鎖, 執行pthread_cond_wait的線程可能在互斥量已然處於加鎖狀態的時候醒來, 發現互斥量仍然沒有解鎖, 就會再次休眠, 從而導致了多餘的上下文切換。

7.3.4條件變量的銷毀

int pthread_cond_destroy(pthread_cond_t *cond);

注意:

1、永遠不要用一個條件變量對另一個條件變量賦值, 即pthread_cond_t cond_b = cond_a不合法, 這種行為是未定義的。

2、使用PTHREAD_COND_INITIALIZE靜態初始化的條件變量, 不需要被銷毀。

3、要調用pthread_cond_destroy銷毀的條件變量可以調用pthread_cond_init重新進行初始化。

4、不要引用已經銷毀的條件變量, 這種行為是未定義的。

例:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
#define NUMBER 2

int g_bowl = 0;
pthread_mutex_t mutex;//定義互斥鎖
pthread_cond_t cond1;//條件變量
pthread_cond_t cond2;//條件變量


void *WorkProduct(void *arg)
{
  int *p = (int*)arg;
  while(1)
  {
    pthread_mutex_lock(&mutex);
    while(*p > 0)
    {
      pthread_cond_wait(&cond2,&mutex);//條件等待,條件不滿足,陷入阻塞
    }
    ++(*p);
    printf("i am workproduct :%d,i product %dn",(int)syscall(SYS_gettid),*p);
    pthread_cond_signal(&cond1);//通知消費者
    pthread_mutex_unlock(&mutex);//釋放鎖
  }
  return NULL;
}

void *WorkConsume(void *arg)
{
  int *p = (int*)arg;
  while(1)
  {
    pthread_mutex_lock(&mutex);
    while(*p <= 0)
    {
      pthread_cond_wait(&cond1,&mutex);//條件等待,條件不滿足,陷入阻塞
    }
    printf("i am workconsume :%d,i consume %dn",(int)syscall(SYS_gettid),*p);
    --(*p);
    pthread_cond_signal(&cond2);//通知生產者
    pthread_mutex_unlock(&mutex);//釋放鎖
  }
  return NULL;
}
int main()
{
  pthread_t cons[NUMBER],prod[NUMBER];
  pthread_mutex_init(&mutex,NULL);//互斥鎖初始化
  pthread_cond_init(&cond1,NULL);//條件變量初始化
  pthread_cond_init(&cond2,NULL);//條件變量初始化
  int i = 0;
  for(;i < NUMBER;++i)
  {
    int ret = pthread_create(&prod[i],NULL,WorkProduct,(void*)&g_bowl);
    if(ret != 0)
    {
      perror("pthread_create");
      return -1;
    }
    ret = pthread_create(&cons[i],NULL,WorkConsume,(void*)&g_bowl);
    if(ret != 0)
    {
      perror("pthread_create");
      return -1;
    }
  }
  for(i = 0;i < NUMBER;++i)
  {
    pthread_join(cons[i],NULL);//線程等待
    pthread_join(prod[i],NULL);
  }
  pthread_mutex_destroy(&mutex);//銷毀互斥鎖
  pthread_cond_destroy(&cond1);
  pthread_cond_destroy(&cond2);
  while(1)
  {
    printf("i am main work threadn");
    sleep(1);
  }
  return 0;
}

在這裡為什麼有兩個條件變量呢?
若所有的線程只使用一個條件變量,會導致所有線程最後都進入PCB等待隊列。

thread apply all bt查看:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

7.3.5情況分析:兩個生產者,兩個消費者,一個PCB等待隊列

1、最開始的情況,兩個消費者搶到了鎖,此時生產者未生產,則都放入PCB等待隊列中

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

2、一個生產者搶到了鎖,生產了一份材料,喚醒一個消費者,此時三者搶鎖,若兩個生產者分別先後搶到了鎖,則都進入PCB等待隊列中

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

3、只有一個消費者,則必會搶到鎖,消費材料,喚醒PCB等待隊列,若此時喚醒的是,消費者,則現在是這樣一個情況:

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

4、兩個消費者在外邊搶鎖,一定都會進入PCB等待隊列中

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

解決上述問題可採用兩種方法:

1、使用int pthread_cond_broadcast(pthread_cond_t *cond);,喚醒PCB等待隊列中所有的線程。此時所有線程都會同時執行搶鎖邏輯,太消費資源了。此方法不妥

2、採用兩個PCB等待序列,一個放生產者,一個放消費者,生產者喚醒消費者,消費者喚醒生產者。

8.線程取消

8.1線程取消函數接口

int pthread_cancel(pthread_t thread);

一個線程可以通過調用該函數向另一個線程發送取消請求。 這不是個阻塞型接口, 發出請求後, 函數就立刻返回了, 而不會等待目標線程退出之後才返回。

調用pthread_cancel時, 會向目標線程發送一個SIGCANCEL的信號, 該信號就是kill -l中消失的32號信號。

線程的默認取消狀態是PTHREAD_CANCEL_ENABLE。即是可被取消的。

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

什麼是取消點? 可通過man pthreads查看取消點
就是對於某些函數, 如果線程允許取消且取消類型是延遲取消, 並且線程也收到了取消請求, 那麼當執行到這些函數的時候, 線程就可以退出了。

8.2線程取消帶來的弊端

目標線程可能會持有互斥量、 信號量或其他類型的鎖, 這時候如果收到取消請求, 並且取消類型是異步取消, 那麼可能目標線程掌握的資源還沒有來得及釋放就被迫退出了, 這可能會給其他線程帶來不可恢復的後果, 比如死鎖(其他線程再也無法獲得資源) 。

注意:

輕易不要調用pthread_cancel函數, 在外部殺死線程是很糟糕的做法,畢竟如果想通知目標線程退出, 還可以採取其他方法。

如果不得不允許線程取消, 那麼在某些非常關鍵不容有失的代碼區域, 暫時將線程設置成不可取消狀態, 退出關鍵區域之後, 再恢復成可以取消的狀態。

在非關鍵的區域, 也要將線程設置成延遲取消, 永遠不要設置成異步取消。

8.2線程清理函數

假設遇到取消請求, 線程執行到了取消點, 卻沒有來得及做清理動作(如動態申請的內存沒有釋放, 申請的互斥量沒有解鎖等) , 可能會導致錯誤的產生, 比如死鎖, 甚至是進程崩潰。

為了避免這種情況, 線程可以設置一個或多個清理函數, 線程取消或退出時,會自動執行這些清理函數, 以確保資源處於一致的狀態。

如果線程被取消, 清理函數則會負責解鎖操作。

void pthread_cleanup_push(void (*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);

這兩個函數必須同時出現, 並且屬於同一個語法塊。

何時會觸發註冊的清理函數:?

1、當線程的主函數是調用pthread_exit返回的, 清理函數總是會被執行。

2、當線程是被其他線程調用pthread_cancel取消的, 清理函數總是會被執行。

3、當線程的主函數是通過return返回的, 並且pthread_cleanup_pop的唯一參數execute是0時, 清理函數不會被執行.

4、線程的主函數是通過return返回的, 並且pthread_cleanup_pop的唯一參數execute是非零值時, 清理函數會執行一次。

代碼:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
#define NUMBER 2
int g_bowl = 0;

pthread_mutex_t mutex;//定義互斥鎖

void clean(void *arg)
{
  printf("Clean up:%sn",(char*)arg);
  pthread_mutex_unlock(&mutex);//釋放鎖
}

void *WorkCancel(void *arg)
{
  pthread_mutex_lock(&mutex);
  pthread_cleanup_push(clean,"clean up handler");//清除函數的push
  struct timespec t = {3,0};//取消點
  nanosleep(&t,0);
  pthread_cleanup_pop(0);//清除
  pthread_mutex_unlock(&mutex);
}

void *WorkWhile(void *arg)
{
  sleep(5);
  pthread_mutex_lock(&mutex);
  printf("i get the mutexn");//若能拿到資源,則表示取消清理函數成功!
  pthread_mutex_unlock(&mutex);
  return NULL;
}
int main()
{
  pthread_t cons,prod;
  pthread_mutex_init(&mutex,NULL);//互斥鎖初始化
  int ret = pthread_create(&prod,NULL,WorkCancel,(void*)&g_bowl);//該線程拿到鎖,然後掛掉
  if(ret != 0)
  {
    perror("pthread_create");
    return -1;
  }
  int ret1 = pthread_create(&cons,NULL,WorkWhile,(void*)&ret);//測試該線程是否可以拿到鎖
  if(ret1 != 0)
  {
    perror("pthread_create");
    return -1;
  }

  pthread_cancel(prod);//取消該線程
  pthread_join(prod,NULL);//線程等待
  pthread_join(cons,NULL);//線程等待
  pthread_mutex_destroy(&mutex);//銷毀互斥鎖
  while(1)
  {
    sleep(1);
  }
  return 0;
}

結果:只要拿到鎖,就表明線程清理函數成功了。

「Linux」多線程詳解,一篇文章徹底搞懂多線程中各個難點

9.多線程與fork()

永遠不要在多線程程序裏面調用fork。

Linux的fork函數, 會複製一個進程, 對於多線程程序而言, fork函數複製的是用fork的那個線程, 而並不複製其他的線程。 fork之後其他線程都不見了。 Linux存在forkall語義的系統調用, 無法做到將多線程全部複製。

多線程程序在fork之前, 其他線程可能正持有互斥量處理臨界區的代碼。 fork之後, 其他線程都不見了, 那麼互斥量的值可能處於不可用的狀態, 也不會有其他線程來將互斥量解鎖。

10.生產者與消費者模型

10.1生產者與消費者模型的本質

本質上是一個線程安全的隊列,和兩種角色的線程(生產者和消費者)

存在三種關係:

1、生產者與生產者互斥

2、消費者與消費者互斥

3、生產者與消費者同步+互斥

10.2為什麼需要生產者與消費者模型?

生產者和消費者彼此之間不直接通訊,而通過阻塞隊列來進行通訊,所以生產者生成完數據之後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列中取,阻塞隊列就相當於一個緩衝區,平衡了生產者和消費者的處理能力。這個阻塞隊列就是用來給生產者和消費解耦的。

10.3優點

1、解耦

2、支持高並發

3、支持忙閑不均

10.4實現兩個消費者線程,兩個生產者線程的生產者消費者模型

生產者生成時用的同一個全局變量,故對該全局變量進行了加鎖。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <queue>
#include <sys/syscall.h>

#define PTHREAD_COUNT 2
int data = 0;//全局變量作為插入數據
pthread_mutex_t mutex1;
class ModelOfConProd{
  public:
    ModelOfConProd()//構造
    {
      _capacity = 10;
      pthread_mutex_init(&_mutex,NULL);
      pthread_cond_init(&_cons,NULL);
      pthread_cond_init(&_prod,NULL);
    }
    ~ModelOfConProd()//析構
    {
      _capacity = 0;
      pthread_mutex_destroy(&_mutex);
      pthread_cond_destroy(&_cons);
      pthread_cond_destroy(&_prod);
    }

    void Push(int data)//push數據,生產者線程使用的
    {
      pthread_mutex_lock(&_mutex);
      while((int)_queue.size() >= _capacity)
      {
        pthread_cond_wait(&_prod,&_mutex);
      }
      _queue.push(data);
      pthread_mutex_unlock(&_mutex);
      pthread_cond_signal(&_cons);
    }


    void Pop(int& data)//pop數據,消費者線程使用的
    {
      pthread_mutex_lock(&_mutex);
      while(_queue.empty())
      {
        pthread_cond_wait(&_cons,&_mutex);
      }
      data = _queue.front();
      _queue.pop();
      pthread_mutex_unlock(&_mutex);
      pthread_cond_signal(&_prod);
    }

  private:
    int _capacity;//容量大小,限制容量大小
    std::queue<int> _queue;//隊列
    pthread_mutex_t _mutex;//互斥鎖
    pthread_cond_t _cons;//消費者條件變量
    pthread_cond_t _prod;//生產者條件變量
};

void *ConsumerStart(void *arg)//消費者入口函數
{
  ModelOfConProd *cp = (ModelOfConProd *)arg;
  while(1)
  {
    cp->Push(data);
    printf("i am pid : %d,i push :%dn",(int)syscall(SYS_gettid),data);
    pthread_mutex_lock(&mutex1);//++的時候,給該全局變量加鎖
    ++data;
    pthread_mutex_unlock(&mutex1);
  }
}


void *ProductsStart(void *arg)//生產者入口函數
{
  ModelOfConProd *cp = (ModelOfConProd *)arg;
  int data = 0;
  while(1)
  {
    cp->Pop(data);
    printf("i am pid : %d,i pop :%dn",(int)syscall(SYS_gettid),data);
  }
}


int main()
{
  ModelOfConProd *cp = new ModelOfConProd;
  pthread_mutex_init(&mutex1,NULL);
  pthread_t cons[PTHREAD_COUNT],prod[PTHREAD_COUNT];
  for(int i = 0;i < PTHREAD_COUNT; ++i)
  {
    int ret = pthread_create(&cons[i],NULL,ConsumerStart,(void*)cp);
    if(ret < 0)
    {
      perror("pthread_create");
      return -1;
    }
    ret = pthread_create(&prod[i],NULL,ProductsStart,(void*)cp);
    if(ret < 0)
    {
      perror("pthread_create");
      return -1;
    }
  }


  for(int i = 0;i < PTHREAD_COUNT;++i)
  {
    pthread_join(cons[i],NULL);
    pthread_join(prod[i],NULL);
  }


  pthread_mutex_destroy(&mutex1);

  return 0;
}

11.寫多線程時應注意

  1. 先考慮代碼的核心邏輯(先實現)
  2. 考慮核心邏輯中是否訪問臨界資源或者說執行臨界區代碼,如果有就需要保持互斥
  3. 考慮線程之間是否需要同步

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/280093.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-20 18:54
下一篇 2024-12-20 18:54

相關推薦

發表回復

登錄後才能評論