
文章目錄
- 前言
- 摘要
- 執行緒
- 什么是執行緒
- 使用執行緒的優勢
- 執行緒與行程千絲萬縷的糾纏
- 執行緒間資源共享情況
- 使用執行緒的弊端
- 執行緒管理(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題翻譯(僅供參考)
