主頁 > 軟體設計 > 溫故Linux后端編程(三):執行緒那些事兒

溫故Linux后端編程(三):執行緒那些事兒

2021-02-06 12:31:21 軟體設計

在這里插入圖片描述

文章目錄

    • 前言
    • 摘要
    • 執行緒
      • 什么是執行緒
      • 使用執行緒的優勢
      • 執行緒與行程千絲萬縷的糾纏
      • 執行緒間資源共享情況
      • 使用執行緒的弊端
    • 執行緒管理(Thread Managment)
      • 創建執行緒
      • 獲取當前執行緒id
      • 判斷倆執行緒是否相等
      • 連接(Joining)和分離(Detaching)執行緒
      • 執行緒屬性
    • 互斥量
      • 互斥量存在的意義
      • 互斥鎖原語
      • 引數釋義
      • 互斥量使用
      • 死鎖
      • 鎖種
        • 樂觀鎖
        • 悲觀鎖
        • 樂觀鎖 VS 悲觀鎖
        • 自旋鎖 && 互斥鎖
    • 條件變數
      • 條件變數原語
      • 條件變數與互斥鎖
        • 注意事項
      • 虛假喚醒與喚醒丟失
        • ⑴虛假喚醒
        • ⑵喚醒丟失
      • 使用條件變數
    • 執行緒池
    • 番外篇
      • Pthread API函式
      • 多執行緒下的物件創建
      • 物件的銷毀與競態條件
        • shared_ptr/weak_ptr
      • 再聊會兒C++記憶體安全
      • 資源推薦

前言

不知不覺,就到大三了,
不知不覺,就要開始找暑期實習了,
溫故而知新嘛,(資料結構復習兩天發現不對,我還是更喜歡這個,) 所以就來了,

摘要

在多處理器共享記憶體的架構中(如:對稱多處理系統SMP),執行緒可以用于實作程式的并行性,歷史上硬體銷售商實作了各種私有版本的多執行緒庫,使得軟體開發者不得不關心它的移植性,對于UNIX系統,IEEE POSIX 1003.1標準定義了一個C語言多執行緒編程介面,依附于該標準的實作被稱為POSIX theads 或 Pthreads,

該教程介紹了Pthreads的概念、動機和設計思想,內容包含了Pthreads API主要的三大類函式:執行緒管理(Thread Managment)、互斥量(Mutex Variables)和條件變數(Condition Variables),向剛開始學習Pthreads的程式員提供了演示例程,

適于:剛開始學習使用執行緒實作并行程式設計;對于C并行程式設計有基本了解,

執行緒

都說知其然,知其所以然,
不知道,我們專業的要求是這樣的,

什么是執行緒

官方話就是:是作業系統能夠進行運算調度的最小單位,它被包含在行程之中,是行程中的實際運作單位,一條執行緒指的是行程中一個單一順序的控制流,一個行程中可以并發多個執行緒,每條執行緒并行執行不同的任務,

1、提高程式的并發性
2、開銷小,不需要重新分配記憶體
3、通信和共享資料方便

在這里插入圖片描述


使用執行緒的優勢

  • 在同一個行程中的所有執行緒共享同樣的地址空間,較于行程間的通信,在許多情況下執行緒間的通信效率比較高,且易于使用,
    較于沒有使用執行緒的程式,使用執行緒的應用程式有潛在的性能增益和實際的優點:

  • CPU使用I/O交疊作業:例如,一個程式可能有一個需要較長時間的I/O操作,當一個執行緒等待I/O系統呼叫完成時,CPU可以被其它執行緒使用,

  • 優先/實時調度:比較重要的任務可以被調度,替換或者中斷較低優先級的任務,

  • 異步事件處理:頻率和持續時間不確定的任務可以交錯,例如,web服務器可以同時為前一個請求傳輸資料和管理新請求,

  • Pthreads沒有中間的記憶體復制,因為執行緒和一個行程共享同樣的地址空間,沒有資料傳輸,變成cache-to-CPU或memory-to-CPU的帶寬(最壞情況),速度是相當的快,

劣勢啊,劣勢也很明顯,毀譽參半,后面再說,


執行緒與行程千絲萬縷的糾纏

1)執行緒又被叫做輕量級行程,也有PCB,創建執行緒使用的底層函式和行程是一樣的,都是clone,
(2)從內核里看執行緒和行程是一樣的,都有各自不同的PCB,但是PCB指向的記憶體資源的三級頁表是不同的,
(3)行程可以蛻變成執行緒,行程也可以說是主執行緒,就是高速路的主干道,
(4)在Linux下,執行緒是最小的執行單位,行程是最小的分配資源單位,

執行緒間資源共享情況

⑴共享資源

1、檔案描述符表
2、每種信號的處理方式
3、當前作業目錄
4、用戶ID和組ID
5、記憶體地址空間

⑵非共享資源

1、執行緒id
2、處理器現場和堆疊指標
3、獨立的堆疊空間
4、errno變數
5、信號屏蔽字
6、調度優先級

使用執行緒的弊端

1、執行緒不穩定(這個是真的不穩定,后面會專門出一篇“可重入函式對執行緒的影響”,因為現在還沒整理好那塊兒)
2、執行緒除錯困難(這個是真的頭疼,難以除錯的東西,目前我只有一個“段錯誤,核心已轉儲”可以用用,關鍵是錯誤難以復現,很難,很難)
3、執行緒無法使用Unix經典事件,如信號(這個反正我也沒用過,管它)

例如:假設你的程式創建了幾個執行緒,每一個呼叫相同的庫函式:

這個庫函式存取/修改了一個全域結構或記憶體中的位置,
當每個執行緒呼叫這個函式時,可能同時去修改這個全域結構活記憶體位置,
如果函式沒有使用同步機制去阻止資料破壞,這時,就不是執行緒安全的了,

如果你不是100%確定外部庫函式是執行緒安全的,自己負責所可能引發的問題, 
建議:小心使用庫或者物件,當不能明確確定是否是執行緒安全的,若有疑慮,假設其不是執行緒安全的直到得以證明,
可以通過不斷地使用不確定的函式找出問題所在,

看一下這篇(過幾天會重寫):可重入函式對于執行緒安全的意義


執行緒管理(Thread Managment)

創建執行緒

#include<pthread.h>

int pthread_create(pthread_t *thread,const pthread_tattr_t *attr,void *(*start_routine)(void *),void *arg);

/*
引數釋義:
thread:這是一個傳出引數,傳遞一個pthread_t變數進來,用以保存新執行緒的tid(執行緒id)
attr:執行緒屬性設定,NULL代表使用默認屬性(注(1))
(*start_routine)(void *):函式指標,指向新執行緒應該指向的函式模塊
arg:老熟了,給前面那個函式傳參用的,不傳就寫NULL

回傳值:成功回傳0.,失敗回傳錯誤號,錯誤號,錯誤號,前面說過errno不共享的,(執行緒里回傳值統一這樣的,后面不提了)

注(1):創建執行緒時,沒什么特殊情況我們都是使用默認屬性的,不過有時候需要做一些特殊處理,碧如調整優先級啊這些的,
*/

Q:怎樣安全地向一個新創建的執行緒傳遞資料?
A:確保所傳遞的資料是執行緒安全的(不能被其他執行緒修改),下面三個例子演示了那個應該和那個不應該,

代碼演示:

// Example Code - Pthread Creation and Termination  

#include <pthread.h> 
#include <stdio.h> 
#define NUM_THREADS     5 

void *PrintHello(void *thread_id) 
{ 
   int tid; 
   tid = (int)thread_id; 
   printf("Hello World! It's me, thread #%d!\n", tid); 
   pthread_exit(NULL); 
} 

int main(int argc, char *argv[]) 
{ 
   pthread_t threads[NUM_THREADS]; 
   int rc, t; 

   for(t=0; t<NUM_THREADS; t++){ 
      printf("In main: creating thread %d\n", t); 

      rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t); 

      if (rc){ 
         printf("ERROR; return code from pthread_create() is %d\n", rc); 
         exit(-1); 
      } 
   } 
   pthread_exit(NULL); 
} 

接下來演示執行緒安全:

//下面的代碼片段演示了如何向一個執行緒傳遞一個簡單的整數,
//主執行緒為每一個執行緒使用一個唯一的資料結構,確保每個執行緒傳遞的引數是完整的,
int *taskids[NUM_THREADS]; 

for(t=0; t<NUM_THREADS; t++) 
{ 
   taskids[t] = (int *) malloc(sizeof(int)); 
   *taskids[t] = t; 
   printf("Creating thread %d\n", t); 
   rc = pthread_create(&threads[t], NULL, PrintHello,(void *) taskids[t]); 
   ... 
} 
//例子展示了用結構體向執行緒設定/傳遞引數,每個執行緒獲得一個唯一的結構體實體, 

struct thread_data{ 
   int  thread_id; 
   int  sum; 
   char *message; 
}; 

struct thread_data thread_data_array[NUM_THREADS]; 

void *PrintHello(void *threadarg) 
{ 
   struct thread_data *my_data; 
   ... 
   my_data = (struct thread_data *)threadarg; 
   taskid = my_data->thread_id; 
   sum = my_data->sum; 
   hello_msg = my_data->message; 
   ... 
} 

int main (int argc, char *argv[]) 
{ 
   ... 
   thread_data_array[t].thread_id = t; 
   thread_data_array[t].sum = sum; 
   thread_data_array[t].message = messages[t]; 
   rc = pthread_create(&threads[t], NULL, PrintHello,(void *) &thread_data_array[t]); 
   ... 
} 
//例子演示了錯誤地傳遞引數,回圈會在執行緒訪問傳遞的引數前改變傳遞給執行緒的地址的內容, 

int rc, t; 
for(t=0; t<NUM_THREADS; t++)  
{ 
   printf("Creating thread %d\n", t); 
   rc = pthread_create(&threads[t], NULL, PrintHello,(void *) &t); 
   ... 
} 

獲取當前執行緒id

#include<pthread.h>

pthread_t pthread_self(void);

執行緒id的型別是pthread_t,它在當前行程中是唯一的,但是在不同系統中這個型別有不同的實作,它可能是一個整數值,也可能是一個結構體,反正就是你猜不到的東西,


判斷倆執行緒是否相等

#include<pthread.h>

int pthread_self(pthread_t t1,pthread_t t2);

注意這兩個函式中的執行緒ID物件是不透明的,不是輕易能檢查的,因為執行緒ID是不透明的物件,所以C語言的==運算子不能用于比較兩個執行緒ID,


連接(Joining)和分離(Detaching)執行緒

pthread_join(threadid,status)  

pthread_detach(threadid,status)  

pthread_attr_setdetachstate(attr,detachstate)  

pthread_attr_getdetachstate(attr,detachstate)  
pthread_join()函式阻塞呼叫執行緒直到threadid所指定的執行緒終止,  
如果在目標執行緒中呼叫pthread_exit(),程式員可以在主執行緒中獲得目標執行緒的終止狀態, 
 
連接執行緒只能用pthread_join()連接一次,若多次呼叫就會發生邏輯錯誤, 
 
兩種同步方法,互斥量(mutexes)和條件變數(condition variables),稍后討論,

可連接(Joinable or Not)?

當一個執行緒被創建,它有一個屬性定義了它是可連接的(joinable)還是分離的(detached),
只有是可連接的執行緒才能被連接(joined),若果創建的執行緒是分離的,則不能連接, 
POSIX標準的最終草案指定了執行緒必須創建成可連接的,然而,并非所有實作都遵循此約定, 
使用pthread_create()的attr引數可以顯式的創建可連接或分離的執行緒

典型四步如下:

宣告一個pthread_attr_t資料型別的執行緒屬性變數 

用 pthread_attr_init()初始化改屬性變數 

用pthread_attr_setdetachstate()設定可分離狀態屬性 

完了后,用pthread_attr_destroy()釋放屬性所占用的庫資源 

分離(Detaching):

pthread_detach()可以顯式用于分離執行緒,盡管創建時是可連接的,
沒有與pthread_detach()功能相反的函式


又到了演示執行緒安全的時間了

//這個例子演示了用Pthread join函式去等待執行緒終止,
//因為有些實作并不是默認創建執行緒是可連接狀態,例子中顯式地將其創建為可連接的,

#include <pthread.h> 
#include <stdio.h> 
#define NUM_THREADS    3 

void *BusyWork(void *null) 
{ 
   int i; 
   double result=0.0; 
   
   for (i=0; i<1000000; i++) 
   { 
     result = result + (double)random(); 
   } 
   printf("result = %e\n",result); 
   pthread_exit((void *) 0); 
} 

int main (int argc, char *argv[]) 
{
   pthread_t thread[NUM_THREADS]; 
   pthread_attr_t attr; 
   int rc, t; 
   void *status;
    
   /* Initialize and set thread detached attribute */ 
   pthread_attr_init(&attr); 
   pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); 
 
   for(t=0; t<NUM_THREADS; t++) 
   { 
      printf("Creating thread %d\n", t); 

      rc = pthread_create(&thread[t], &attr, BusyWork, NULL);  

      if (rc) 
      { 
         printf("ERROR; return code from pthread_create() is %d/n", rc); 
         exit(-1); 
      } 
   } 

   /* Free attribute and wait for the other threads */ 
   pthread_attr_destroy(&attr); 
   
   for(t=0; t<NUM_THREADS; t++) 
   { 
      rc = pthread_join(thread[t], &status); 
      
      if (rc) 
      { 
         printf("ERROR; return code from pthread_join() is %d\n", rc); 
         exit(-1); 
      } 
      printf("Completed join with thread %d status= %ld\n",t, (long)status); 
   } 
   pthread_exit(NULL); 
} 

當一個執行緒被設定為分離執行緒時,如果執行緒的運行非常快,可能在pthread_create()函式回傳之前就終止了,由于一個執行緒在終止以后可以將執行緒號和系統資源移交給其他的執行緒使用,此時再使用函式pthread_cretae()獲得的執行緒號進行操作將會發生錯誤,


執行緒屬性

linux下執行緒屬性是可以根據實際專案需要進行設定,
之前我們討論的都是執行緒的默認屬性,默認屬性已經可以解決大部分執行緒開發時的需求,
如果需要更高的性能,就需要人為對執行緒屬性進行配置,

 typedef struct
{
    int detachstate; //執行緒的分離狀態
    int schedpolicy; //執行緒的調度策略
    struct sched schedparam;//執行緒的調度引數
    int inheritsched; //執行緒的繼承性
    int scope; //執行緒的作用域
    size_t guardsize; //執行緒堆疊末尾的警戒緩沖區大小
    int stackaddr_set; //執行緒堆疊的設定
    void* stackaddr; //執行緒堆疊的啟始位置
    size_t stacksize; //執行緒堆疊大小
}pthread_attr_t;

//在上面我們可以看到,關于這個結構體中的相關引數

默認的屬性為非系結、非分離、預設的堆疊、與父行程同樣級別的優先級,

執行緒屬性設定的一般套路:

 第一:定義屬性變數并初始化  pthread_attr_t
                            pthread_attr_init()
 第二:呼叫你想設定的屬性的介面函式
                            pthread_attr_setxxxxxxxx()
 第三:創建執行緒的時候,第二個引數使用這個屬性
 
 第四:銷毀屬性
                            pthread_destroy();


互斥量

互斥量存在的意義

做個小實驗吧,兩個執行緒計數,如果最后加起來是20萬那就不用往下看了,

#include<pthread.h>
#include<unistd.h>
#include<stdio.h>

int count = 0;//宣告全域變數,等下就看看它了

void *run(void *arg)
{
	int i = 0;
	for(i = 0;i < 100000; i++)
	{
		count++;
		printf("Count:%d\n",count);
		usleep(2);
	}
	return (void*)0;
}

int main(int argc,char **argv)
{
	pthread_t tid1,tid2;
	int err1,err2;

	err1 = pthread_create(&tid1,NULL,run,NULL);
	err2 = pthread_create(&tid2,NULL,run,NULL);

	if(err1==0 && err2==0)//倆執行緒都成功創建出來
	{
		pthread_join(tid1,NULL);
		pthread_join(tid2,NULL);
	}
	return 0;
}

好,為什么要執行緒同步,那就心照不宣了

算了,官方話還是要說一說的

1、共享資源,多個執行緒都可以對共享資源進行操作
2、執行緒操作共享資源的先后順序不一定
3、處理器對存盤器的操作一般不是原子操作

互斥鎖原語

pthread_mutex_t mutex = PTHREAD_MUREX_INITALIZER //用于初始化互斥鎖,后面簡稱鎖

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); //初始化鎖,和上面那個一個意思,
//初始化一個互斥鎖(互斥量)–>初值可看做1
int pthread_mutex_destroy(pthread_mutex_t *mutex); //銷毀鎖
int pthread_mutex_lock(pthread_mutex_t *mutex); //上鎖
int pthread_mutex_unlok(pthread_mutex_t *mutex); //解鎖
int pthread_mutex_trylock(pthread_mutex_t *mutex); //嘗試上鎖

引數釋義

<這里只釋義那個init>

引數1:傳出引數,呼叫時應傳&mutex
restrict關鍵字:只用于限制指標,告訴編譯器,所有修改該指標指向記憶體中內容的操作,只能通過本指標完成,不能通過除本指標以外的其他變數或指標修改,

引數2:互測驗性,是一個傳入引數,通常傳NULL,選用默認屬性(執行緒間共享).
靜態初始化:如果互斥鎖mutex是靜態分配的(定義在全域,或加了static關鍵字修飾),可以直接使用宏進行初始化,pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
動態初始化:區域變數應采用動態初始化,pthread_mutex_init(&mutex, NULL);

attr物件用于設定互斥量物件的屬性,使用時必須宣告為pthread_mutextattr_t型別,默認值可以是NULL,Pthreads標準定義了三種可選的互斥量屬性:
協議(Protocol): 指定了協議用于阻止互斥量的優先級改變
優先級上限(Prioceiling):指定互斥量的優先級上限
行程共享(Process-shared):指定行程共享互斥量

注意所有實作都提供了這三個可選的互斥量屬性,

Q:有多個執行緒等待同一個鎖定的互斥量,當互斥量被解鎖后,那個執行緒會第一個鎖定互斥量? 

A:除非執行緒使用了優先級調度機制,否則,執行緒會被系統調度器去分配,那個執行緒會第一個鎖定互斥量是隨機的, 

互斥量使用

#include<pthread.h>
#include<unistd.h>
#include<stdio.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int count = 0;
void *run(void *arg)
{
	int i = 0;
	for(i = 0;i < 100000; i++)
	{
        pthread_mutex_lock(&mutex);
		count++;
		pthread_mutex_unlock(&mutex);
		printf("Count:%d\n",count);
		usleep(2);
	}
	return (void*)0;
}

int main(int argc,char **argv)
{
	pthread_t tid1,tid2;
	int err1,err2;
	err1 = pthread_create(&tid1,NULL,run,NULL);
	err2 = pthread_create(&tid2,NULL,run,NULL);
	if(err1==0 && err2==0)
	{
		pthread_join(tid1,NULL);
		pthread_join(tid2,NULL);
	}
	return 0;
}

拿去執行,如果不是20萬也可以不用往下看了,


死鎖

(上一篇 行程·全家桶 在這個問題上花了不少篇幅)

為什么我要強調上鎖和解鎖一定要放在一起寫,就是防止出現人為失誤導致死鎖
死鎖嘛,解不開了,
要么是你忘了解開,別人也就沒得用了
要么就是幾個執行緒互相掐著關鍵資料導致誰也沒辦法完成任務,結果誰也沒辦法解鎖,
這種情況下只有銷毀掉代價最小的那個鎖,讓任務執行下去,不過后面要記得把那個被銷毀的任務重新運作,


鎖種

樂觀鎖

在這里插入圖片描述

樂觀鎖,你看它名字就知道,把事情想得很單純,它總認為資源和資料不會被別人所修改,所以讀取不會上鎖,但是樂觀鎖在進行寫入操作的時候會判斷當前資料是否被修改過,可以使用版本號等機制,

樂觀鎖多適用于多度的應用型別,這樣可以提高吞吐量,

使用自增長的整數表示資料版本號:
在這里插入圖片描述
若這雙寫互不干擾,男的取出,版本號為0,男的寫入,版本號+1;女的取出,版本號為1,女的寫入,版本號為2,
若這雙寫相互干擾了,男的取出,版本號為0;男的還沒寫入,女的取出,版本號為0;男的寫入,版本號為1;女的寫入,發現版本號不匹配,則寫入失敗,應該重新讀取金額數和版本號,

此外,也可以通過時間戳來實作


悲觀鎖

在這里插入圖片描述

悲觀鎖是一種悲觀思想,它總認為最壞的情況可能會出現,它認為資料很可能會被其他人所修改,所以悲觀鎖在持有資料的時候總會把資源 或者 資料 鎖住,這樣其他執行緒想要請求這個資源的時候就會阻塞,直到等到悲觀鎖把資源釋放為止,傳統的關系型資料庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖,悲觀鎖的實作往往依靠資料庫本身的鎖功能實作,

實作有資料庫的鎖之類的,


樂觀鎖 VS 悲觀鎖

只能說,各有千秋吧,

樂觀鎖適用于寫比較少的情況下,即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量,但如果經常產生沖突,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適,

悲觀鎖會造成訪問資料庫時間較長,并發性不好,特別是長事務,
樂觀鎖在現實中使用得較多,


自旋鎖 && 互斥鎖

自旋鎖和互斥鎖嘛,一直在用的,不過以前只是簡單的叫它們:鎖,原來人家有名字的啊,

wait() 曉得不?timewait()曉得不?

互斥鎖:阻塞等待
自旋鎖:等兩下就去問一聲:好了不?我很急啊!好了不?你快點啊,,,哈哈哈哈哈

自旋鎖的原理比較簡單,如果持有鎖的執行緒能在短時間內釋放鎖資源,那么那些等待競爭鎖的執行緒就不需要做內核態和用戶態之間的切換進入阻塞狀態,它們只需要等一等(自旋),等到持有鎖的執行緒釋放鎖之后即可獲取,這樣就避免了用戶行程和內核切換的消耗,

因為自旋鎖避免了作業系統行程調度和執行緒切換,所以自旋鎖通常適用在時間比較短的情況下,由于這個原因,作業系統的內核經常使用自旋鎖,但是,如果長時間上鎖的話,自旋鎖會非常耗費性能,它阻止了其他執行緒的運行和調度,執行緒持有鎖的時間越長,則持有該鎖的執行緒將被 OS(Operating System) 調度程式中斷的風險越大,如果發生中斷情況,那么其他執行緒將保持旋轉狀態(反復嘗試獲取鎖),而持有該鎖的執行緒并不打算釋放鎖,這樣導致的是結果是無限期推遲,直到持有鎖的執行緒可以完成并釋放它為止,

解決上面這種情況一個很好的方式是給自旋鎖設定一個自旋時間,等時間一到立即釋放自旋鎖,適應性自旋鎖意味著自旋時間不是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖擁有的狀態來決定,基本認為一個執行緒背景關系切換的時間是最佳的一個時間,

條件變數

  • 條件變數提供了另一種同步的方式,互斥量通過控制對資料的訪問實作了同步,而條件變數允許根據實際的資料值來實作同步,
  • 沒有條件變數,程式員就必須使用執行緒去輪詢(可能在臨界區),查看條件是否滿足,這樣比較消耗資源,因為執行緒連續繁忙作業,條件變數是一種可以實作這種輪詢的方式,
  • 條件變數往往和互斥一起使用

使用條件變數的代表性順序如下:
在這里插入圖片描述

條件變數原語

//初始化條件變數:
//本人還是喜歡靜態初始化,省事兒
pthread_cont_t cont = PTHREAD_COND_INITIALIZER;

//好,再看看動態初始化
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
//引數釋義:cond:用于接收初始化成功管道條件變數
//attr:通常為NULL,且被忽略

//有初始化那肯定得有銷毀
int pthread_cond_destroy(pthread_cond_t *cond);

//既然說條件變數是用來等待的,那就更要看看這等待的特殊之處了
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);  //無條件等待

int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t mytex,const struct timespec *abstime);  //計時等待

//好,加入等待喚醒大軍了,那得看看怎么去喚醒了
int pthread_cond_signal(pthread_cond_t *cptr); //喚醒一個等待該條件的執行緒,存在多個執行緒是按照其佇列入隊順序喚醒其中一個
int pthread_cond_broadcast(pthread_cond_t * cptr); //廣播,喚醒所喲與等待執行緒

條件變數與互斥鎖

在服務器編程中常用的執行緒池,多個執行緒會操作同一個任務佇列,一旦發現任務佇列中有新的任務,子執行緒將取出任務;這里因為是多執行緒操作,必然會涉及到用互斥鎖保護任務佇列的情況(否則其中一個執行緒操作了任務佇列,取出執行緒到一半時,執行緒切換又取出相同任務),但是互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定,設想,每個執行緒為了獲取新的任務不斷得進行這樣的操作:鎖定任務佇列,檢查任務佇列是否有新的任務,取得新的任務(有新的任務)或不做任何操作(無新的任務),釋放鎖,這將是很消耗資源的,

而條件變數通過允許執行緒阻塞和等待另一個執行緒發送信號的方法彌補了互斥鎖的不足,它常和互斥鎖一起配合使用,使用時,條件變數被用來阻塞一個執行緒,當條件不滿足時,執行緒往往解開相應的互斥鎖并等待條件發生變化,一旦其他的某個執行緒改變了條件變數,他將通知相應的條件變數喚醒一個或多個正被此條件變數阻塞的執行緒,這些執行緒將重新鎖定互斥鎖并重新測驗條件是否滿足,一般說來,條件變數被用來進行執行緒間的同步,對應于執行緒池的場景,我們可以讓執行緒處于等待狀態,當主執行緒將新的任務放入作業佇列時,發出通知(其中一個或多個),得到通知的執行緒重新獲得鎖,取得任務,執行相關操作,

注意事項

(1)必須在互斥鎖的保護下喚醒,否則喚醒可能發生在鎖定條件變數之前,照成死鎖,
(2)喚醒阻塞在條件變數上的所有執行緒的順序由調度策略決定
(3)如果沒有執行緒被阻塞在調度佇列上,那么喚醒將沒有作用,
(4)以前不懂事兒,就喜歡廣播,由于pthread_cond_broadcast函式喚醒所有阻塞在某個條件變數上的執行緒,這些執行緒被喚醒后將再次競爭相應的互斥鎖,所以必須小心使用pthread_cond_broadcast函式,

虛假喚醒與喚醒丟失

⑴虛假喚醒

在多核處理器下,pthread_cond_signal可能會激活多于一個執行緒(阻塞在條件變數上的執行緒),結果是,當一個執行緒呼叫pthread_cond_signal()后,多個呼叫pthread_cond_wait()或pthread_cond_timedwait()的執行緒回傳,這種效應成為”虛假喚醒”(spurious wakeup)

Linux幫助里面有
為什么不去修正,性價比不高嘛,

所以通常的標準解決辦法是這樣的:

在這里插入圖片描述

⑵喚醒丟失

無論哪種等待方式,都必須和一個互斥量配合,以防止多個執行緒來打擾,
互斥鎖必須是普通鎖或適應鎖,并且在進入pthread_cond_wait之前必須由本執行緒加鎖,
在更新等待佇列前,mutex必須保持鎖定狀態. 在執行緒進入掛起,進入等待前,解鎖,(好繞啊,我已經盡力斷句了)
在條件滿足并離開pthread_cond_wait前,上鎖,以恢復它進入cont_wait之前的狀態,

為什么等待會被上鎖?

以免出現喚醒丟失問題,
這里有個大神解釋要不要看:https://stackoverflow.com/questions/4544234/calling-pthread-cond-signal-without-locking-mutex
做事做全套,原始碼也給放這兒了:https://code.woboq.org/userspace/glibc/nptl/pthread_cond_wait.c.html

在放些咱能看懂的中文解釋:將執行緒加入喚醒佇列后方可解鎖,保證了執行緒在陷入wait后至被加入喚醒佇列這段時間內是原子的,
但這種原子性依賴一個前提條件:喚醒者在呼叫pthread_cond_broadcast或pthread_cond_signal喚醒等待者之前也必須對相同的mutex加鎖,
滿足上述條件后,如果一個等待事件A發生在喚醒事件B之前,那么A也同樣在B之前獲得了mutex,那A在被加入喚醒佇列之前B都無法進入喚醒呼叫,因此保證了B一定能夠喚醒A;試想,如果A、B之間沒有mutex來同步,雖然B在A之后發生,但是可能B喚醒時A尚未被加入到喚醒佇列,這便是所謂的喚醒丟失,


在執行緒未獲得相應的互斥鎖時呼叫pthread_cond_signal或pthread_cond_broadcast函式可能會引起喚醒丟失問題,

喚醒丟失往往會在下面的情況下發生:

一個執行緒呼叫pthread_cond_signal或pthread_cond_broadcast函式;
另一個執行緒正處在測驗條件變數和呼叫pthread_cond_wait函式之間;
沒有執行緒正在處在阻塞等待的狀態下,

使用條件變數

//例子演示了使用Pthreads條件變數的幾個函式,主程式創建了三個執行緒,兩個執行緒作業,根系“count”變數,第三個執行緒等待count變數值達到指定的值, 

#include <pthread.h> 
#include <stdio.h> 

#define NUM_THREADS  3 
#define TCOUNT 10 
#define COUNT_LIMIT 12 

int count = 0; 
int thread_ids[3] = {0,1,2}; 
pthread_mutex_t count_mutex; 
pthread_cond_t count_threshold_cv; 

void *inc_count(void *idp)  
{ 
  int j,i; 
  double result=0.0; 
  int *my_id = idp; 
 
  for(i=0; i<TCOUNT; i++) { 
	    pthread_mutex_lock(&count_mutex); 
	    count++; 
	    /*  
	    Check the value of count and signal waiting thread when condition is 
	    reached.  Note that this occurs while mutex is locked.  
	    */ 
	    if (count == COUNT_LIMIT) { 
	        pthread_cond_signal(&count_threshold_cv); 
	        printf("inc_count(): thread %d, count = %d  Threshold reached./n",*my_id, count); 
	    } 
	    printf("inc_count(): thread %d, count = %d, unlocking mutex/n",*my_id, count); 
	    pthread_mutex_unlock(&count_mutex); 

    /* Do some work so threads can alternate on mutex lock */ 
    for (j=0; j<1000; j++) 
        result = result + (double)random(); 
    } 
  pthread_exit(NULL); 
} 

void *watch_count(void *idp)  
{ 
  int *my_id = idp;  
  printf("Starting watch_count(): thread %d/n", *my_id); 
  /* 
  Lock mutex and wait for signal.  Note that the pthread_cond_wait  
  routine will automatically and atomically unlock mutex while it waits.  
  Also, note that if COUNT_LIMIT is reached before this routine is run by 
  the waiting thread, the loop will be skipped to prevent pthread_cond_wait \
  from never returning.  
  */ 
  pthread_mutex_lock(&count_mutex); 
  if (count<COUNT_LIMIT) { 
      pthread_cond_wait(&count_threshold_cv, &count_mutex); 
      printf("watch_count(): thread %d Condition signal received./n", *my_id); 
  } 
  pthread_mutex_unlock(&count_mutex); 
  pthread_exit(NULL); 
} 

int main(int argc, char *argv[]) 
{ 
  int i, rc; 
  pthread_t threads[3]; 
  pthread_attr_t attr; 

  /* Initialize mutex and condition variable objects */ 
  pthread_mutex_init(&count_mutex, NULL); 
  pthread_cond_init (&count_threshold_cv, NULL); 

  /* For portability, explicitly create threads in a joinable state */ 
  pthread_attr_init(&attr); 
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); 
  pthread_create(&threads[0], &attr, inc_count, (void *)&thread_ids[0]); 
  pthread_create(&threads[1], &attr, inc_count, (void *)&thread_ids[1]); 
  pthread_create(&threads[2], &attr, watch_count, (void *)&thread_ids[2]); 
  
  /* Wait for all threads to complete */ 
  for (i=0; i<NUM_THREADS; i++) { 
      pthread_join(threads[i], NULL); 
  } 

  printf ("Main(): Waited on %d  threads. Done./n", NUM_THREADS);
   
  /* Clean up and exit */ 
  pthread_attr_destroy(&attr); 
  pthread_mutex_destroy(&count_mutex); 
  pthread_cond_destroy(&count_threshold_cv); 
  pthread_exit(NULL); 
} 

執行緒池

執行緒池

番外篇

Pthread API函式

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述


多執行緒下的物件創建

物件構造要做到執行緒安全,就一點要求:不要暴露自己,即不要泄露this指標,
那就是做到以下幾點:

不要在建構式中注冊任何回呼
不要在建構式中將this傳給跨執行緒物件
即時在建構式最后一行也不行

對于第一點,如果非要回呼函式才能構造,那就換二段式構造,先構造,在呼叫回呼函式,
對于第三條,如果這個類是個基類呢?它構造完了并不是真的構造完了,還有子類等著呢,

之所以要這樣設計(把this傳給子類那另當別論),就是為了防止構造程序被打斷,構造出一個半成品,

物件的銷毀與競態條件

物件析構,在多執行緒里,由于競態的存在,變得撲朔迷離,
看個例子:

Foo::~Foo(){
	//拿鎖
	//析構
	//解鎖
}
void Foo::update(){
	//拿鎖
	//資料操作
	//解鎖
}

extern Foo *f;//共享資源

A行程操作
delete f;
f = NULL;

B行程操作
if(f)
{
	f->update();
}

那這就有一個很尷尬的情況了:
A在執行“析構”的時候,已經拿到了鎖,而B通過了 f 的判斷,因為那會兒指標還活著,然后被鎖卡住了,
接下來會發生什么?不知道,因為物件析構的時候把鎖也帶走了,,,(鎖屬于物件,物件析構,鎖也跑不了)

那怎么辦?
別怕,參考博客:智能指標

一個動態創建的物件,是否還有效光看指標是看不出來的指標就是指向了一塊記憶體而已,這塊記憶體上的物件如果已經被銷毀,那就根本不能訪問,

shared_ptr/weak_ptr

shared_ptr是參考計數型智能指標,被納入C11標準庫,shared_ptr是一個類模板,它只有一個引數,使用起來很方便,

shared_str是強參考,只要有一個指向x物件的shared_ptr存在,該物件及不會被析構,
weak_ptr是弱參考,它不控制物件的生命周期,但是它知道物件是否還存在,如果物件存在,它可以升級成為shared_ptr,

講這么多不如來個例子實在:

class Observer{
private:
	std::vector<weak_ptr<Observer>> vwo;	//像這樣用啊
}

再聊會兒C++記憶體安全

C++里面可能出現的記憶體問題大致有這么幾個方面

  • 緩沖區溢位
  • 空懸指標/野指標
  • 重復釋放
  • 記憶體泄漏
  • 不配對的new[]/delete
  • 記憶體碎片

對應解決:

  • std::vetor
  • shared_ptr/weak_ptr
  • scoped_ptr,只在物件析構的時候釋放一次
  • scoped_ptr
  • std::vetor

資源推薦

Programing with POSIX thread(POSIX多執行緒程式設計)
需要私信我,

在這里插入圖片描述

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/257141.html

標籤:其他

上一篇:2021美賽F題翻譯(僅供參考)

下一篇:2021數學建模美賽D題-翻譯與思路提供

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more