文章目錄
- 一、執行緒的基礎概念
- 1.1. 執行緒是什么
- 在Linux中沒有真正意義上的執行緒,執行緒是用行程模擬的,資料結構也是用的task_struct
- 1.2. 執行緒的優點
- 1.3. 執行緒的缺點
- 1.4. 執行緒的用途
- 二、執行緒的操作
- 2.1. 創建執行緒
- 2.2. 執行緒的例外
- 2.3. ps -aL查看輕量級行程
- 2.4. 獲取當前執行緒的執行緒ID
- 2.5. 執行緒的終止
- (1)從執行緒函式return
- (2) 呼叫pthread_exit() 終止自己
- (3) 呼叫pthread_cancel終止同一行程的另外一個執行緒
- 2.6. 執行緒的等待
- 2.6.1. 為什么需要執行緒等待?
- 2.6.2. 執行緒等待的操作函式
- 2.7. 執行緒的分離
- 三、 行程和執行緒的區別匯總
- 3.1. 行程和執行緒的概念
- 3.5. 為什么要引入執行緒
- 3.6. 行程和執行緒的區別
- 四、 執行緒互斥
- 4.1. 多個執行緒訪問臨界資源帶來的問題
- 4.2. 鎖
- 4.2.1. 鎖(互斥量)的介面
- 4.2.2. 互斥量(鎖)的實作原理
- 4.3. 死鎖
- 4.3.1. 死鎖的四個必要條件
- 4.3.2. 避免死鎖的方法
- 五、 可重入函式和執行緒安全
- 六、執行緒同步
- 6.1. 條件變數及其介面
- 6.2. 為什么等待的時候需要傳入互斥鎖
- 七、 生產者消費者模型
- 7.1. 生產者消費者遵循的規則(321規則)
- 7.2. 生產者消費者模型的優點
- 7.3. 基于BlockingQueue(阻塞佇列)的生產者消費者模型
一、執行緒的基礎概念
1.1. 執行緒是什么
1.在一個程式里的一個執行路線就叫做執行緒,更準確的定義是執行緒是“一個行程內部的控制序列或執行流”
2.一切行程至少都是一個執行執行緒(行程:執行緒=1:n)
3.執行緒在行程內部運行,本質是在行程地址空間運行(一個行程的多個執行緒共用一塊地址空間)
4.在Linux系統中,看到的PCB都是輕量級行程(執行緒的本質是輕量級行程)
5.通過行程的虛擬地址空間,可以看到行程的大部分資源,選擇將行程合理分配給每個執行流,就形成了執行緒執行流(行程是申請資源,執行緒是分配資源)
創建行程,我們從無到有創建了很多東西,申請了很多資源,比如行程控制塊、地址空間、頁表、將磁盤中的代碼和資料加載到記憶體中,而執行緒,就是該行程中的一個執行路線,更準確的定義是:執行緒是“一個行程內部的控制序列(執行流)”
舉一個很簡單的例子,比如QQ可以一邊視頻聊天,一邊文字聊天,一邊視頻傳輸,這里的行程就是整個QQ,而執行緒則是該行程中的三個執行路線,

綜上,我們可以總結行程和執行緒的關系:
行程是承擔分配系統資源的基本物體, 這里要注意,光一個task_struct并不是行程,行程創建的一整套資源(行程控制塊、地址空間、頁表等)才稱之為行程,
執行緒是CPU調度和分配的基本單位,是行程里面的執行流(執行緒在行程的地址空間內運行), 也就是說,一個行程中的所有執行緒,用的都是該行程的地址空間,行程和執行緒是1:n的關系,有行程必有執行緒,

在Linux中沒有真正意義上的執行緒,執行緒是用行程模擬的,資料結構也是用的task_struct
Linux執行緒本質上就是行程,只是執行緒間共享所有資源,如上圖所示, 每個執行緒都有自己task_struct,因為每個執行緒可被CPU調度,多執行緒間又共享同一行程資源,這兩點剛好滿足執行緒的定義,Linux就是這樣用行程實作了執行緒,所以執行緒又稱為輕量級行程,
1.2. 執行緒的優點
1.創建一個新執行緒的代價要比創建一個新行程小得多
行程是承擔資源分配的基本物體,需要去申請各種資源,而執行緒是CPU調度的基本單位,承擔資源分配的角色,創建執行緒的代價要比行程小得多,
2.與行程之間的切換相比,執行緒之間的切換需要作業系統做的作業要少很多
每個行程都有自己獨立的代碼和資料空間,當切換行程時,需要保存/恢復行程運行環境,還需要切換記憶體地址空間(更新快表、更新快取),因此行程之間的切換會有較大的開銷,
執行緒在行程的地址空間內部運行,同一類執行緒共享代碼和資料空間,每個執行緒都有自己獨立的運行堆疊和程式計數器,因此同一行程內的各個執行緒間不需要切換行程運行環境和記憶體地址空間,執行緒之間的切換開銷小,
3.執行緒占用的資源要比行程少很多
執行緒只是占一個行程資源的一部分
4.能充分利用多處理器的可并行數量
當然行程也可以并行,
5.在等待慢速I/O操作結束的同時,程式可執行其他的計算任務
比如:
I/O比較慢,我們可以一個執行緒執行I/O,另外一個執行緒執行計算,
計算密集型應用(加密,解密等等,占用CPU),為了能在多處理器系統上運行,將計算分解到多個執行緒中實作,
I/O密集型應用(訪問資料庫,列印內容等等,占用記憶體、帶寬),為了提高性能,將I/O操作重疊,執行緒可以同時等待不同的I/O操作,
6.執行緒間通信的開銷要比行程間通信少
各個行程的記憶體地址空間相互獨立,只能通過請求作業系統內核的幫助來完成行程間通信,開銷大,
同一行程下的各個執行緒間共享記憶體地址空間,可以直接通過讀/寫記憶體空間進行通信,
1.3. 執行緒的缺點
1.性能的損失
由于執行緒之間資源師共享的,因此多執行緒的臨界資源一定會變多,而臨界資源我們需要保證它的安全性,因此需要進行一系列的加鎖、解鎖、互斥等動作,
而這些動作都會帶來副作用的,即導致性能的降低,
2.健壯性(魯棒性)降低
健壯性就是程式在例外情況下是否能正常作業,
因為執行緒是共享資源的,所以執行緒之間是會互相影響的,如果遇到了不確定的情況,訪問了原本不該訪問的資源,會導致出現一些未知的錯誤,而行程具有獨立性,所以行程的獨立性是要強于執行緒,
3.缺乏訪問的控制
在創建一個全域變數,多行程中只要有一方發生了寫入,那么這個變數就會發生寫時拷貝,因為行程之間是具有獨立性的,
而在多執行緒中,大家共享地址空間,如果一方在寫入,另外一方在讀取,那么就有可能會發生問題,
4.編程難度提高
在調式方面,一個執行緒出現錯誤會影響整體,因此除錯難度會增大,
1.4. 執行緒的用途
合理的使用多執行緒,能提高CPU密集型程式的執行效率,
合理的使用多執行緒,能提高IO密集型程式的用戶體驗(如生活中我們一邊寫代碼一邊下載開發工具,就是多執行緒運行的一種表現),
二、執行緒的操作
Linux沒有真正意義上的執行緒,而是用行程模擬的,Linux雖然沒有暴露創建執行緒的介面,但是暴露了創建輕量級行程的介面,這個介面就是一個非常接近底層的多執行緒庫,叫做pthread執行緒庫,這個庫采用的標準是POSIX,
2.1. 創建執行緒

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
thread:輸出型引數,執行緒的ID,作業系統不知道它的存在,這個執行緒ID是庫提供的
因為作業系統沒有執行緒的概念,用戶又得使用執行緒,那么只能用一個庫來實作
因此執行緒管理的動作,比如:執行緒創建、執行緒終止,執行緒等待,執行緒分離等都由這個庫實作
attr:執行緒的屬性,一般默認為NULL,交給庫管理
start_routine:函式指標,回傳值為void*,引數為void*,
這個指標指向的函式是執行緒的入口,
多執行緒就是把行程的代碼拆成很多塊,我們的一個執行緒執行的就是多個代碼塊之中的某一塊
arg:給執行緒入口函式傳遞的引數
回傳值:成功回傳0,出錯回傳錯誤碼
Compile and link with -pthread.//在鏈接的時候需要鏈接pthread庫
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
//新執行緒的代碼
void* thread_run(void*arg){
while(1){
printf("I am %s,my pid:%d\n",(char*)arg,getpid());
sleep(1);
}
}
int main(){
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"thread 1");//創建新執行緒,執行thread_run函式
//主執行緒的代碼
while(1){
printf("I am main thread,my pid:%d\n",getpid());
sleep(2);
}
return 0;
}

可以看到它們的pid是相同的,說明它們是同一個行程的不同執行流,
2.2. 執行緒的例外
如果一個執行緒發生了錯誤,比如除零或者野指標:

那么整個行程的所有執行緒都會終止:

執行緒是行程的執行分支,執行緒出例外,就類似行程出例外,進而觸發信號機制,終止行程,行程終止,作業系統釋放行程的資源,該行程內的所有執行緒也就隨即退出,這說明執行緒的魯棒性不強,
2.3. ps -aL查看輕量級行程

可以看到它們的PID相同,但是LWP不同,這是因為LWP是輕量級行程的標識,PID是行程的標識,
在單行程單執行緒之中,PID和LWP是一樣的,而單行程多執行緒之中,linux引入了執行緒組的概念,
執行緒組中每一個執行緒(輕量級行程)都存在一個行程描述符LWP,這個輕量級行程描述符就是用戶級行程ID,是OS調度的做小單位作業系統真正調度的時候調度的是LWP,
2.4. 獲取當前執行緒的執行緒ID

#include <pthread.h>
pthread_t pthread_self(void);
Compile and link with -pthread.


pthread_ create函式會產生一個執行緒ID,存放在第一個引數指向的地址中,該執行緒ID和前面的執行緒PID和LWP不是一回事,PID和LWP屬于行程調度的范疇,
因為執行緒是輕量級行程,是作業系統調度器的最小單位,所以需要一個數值來唯一表示該執行緒,
pthread_ create函式第一個引數指向一個虛擬記憶體單元,該記憶體單元的地址即為新創建執行緒的執行緒ID,屬于執行緒庫的范疇,執行緒庫的后續操作,就是根據該執行緒ID來操作執行緒的,

2.5. 執行緒的終止
如果需要只終止某個執行緒而不終止整個行程,可以有三種方法:
(1)從執行緒函式return
這種方法對主執行緒不適用,從main函式return相當于呼叫exit(),exit() 會讓整個行程終止,主執行緒結束,整個行程也會隨之結束,因為行程是承擔系統分配資源的基本物體,所有的執行緒都是基于這個資源進行分配的,當行程都不在后,自然要進行資源的回收,


(2) 呼叫pthread_exit() 終止自己

當執行緒呼叫這個函式以后,執行緒會終止,主執行緒呼叫這個函式后主執行緒終止,新執行緒不受影響,
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
//新執行緒的代碼
void* thread_run(void*arg){
while(1){
printf("I am %s,my pthread id:%lu, my pid:%d\n",(char*)arg,pthread_self(),getpid());
sleep(1);
}
}
int main(){
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"thread 1");//創建新執行緒
//主執行緒的代碼
printf("I am main thread,my thread id:%lu ,my pid:%d\n",pthread_self(), getpid());
sleep(2);
//獲取新執行緒的退出碼
void*ret=NULL;
pthread_join(tid,&ret);
printf("pthread quit code:%d\n",(long long)ret);//在64位下指標為8位元組
return 0;
}

通過這個函式終止的執行緒,其退出碼為-1
(3) 呼叫pthread_cancel終止同一行程的另外一個執行緒

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
//新執行緒的代碼
void* thread_run(void*arg){
while(1){
printf("I am %s,my pthread id:%lu, my pid:%d\n",(char*)arg,pthread_self(),getpid());
sleep(1);
}
}
int main(){
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"thread 1");//創建新執行緒
//主執行緒的代碼
printf("I am main thread,my thread id:%lu ,my pid:%d\n",pthread_self(), getpid());
sleep(2);
pthread_cancel(tid);//新執行緒退出
printf("new thread %lu be cancled!\n",tid);
pthread_exit((void*)10);//主執行緒退出
return 0;
}

2.6. 執行緒的等待
2.6.1. 為什么需要執行緒等待?
新執行緒退出時,必須被等待,因為已經退出的執行緒,它的空間不會被釋放,仍然在行程的地址空間內,這樣創建的執行緒不會復用剛才退出執行緒的地址空間,有點類似僵尸行程的問題,
2.6.2. 執行緒等待的操作函式
執行緒的底層是輕量級行程,執行緒退出的時候,會將退出碼寫入行程的PCB之中,因此,我們是可以獲取它的退出碼的,

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
thread:要等待執行緒的ID
retval:輸出型引數,獲取執行緒退出的退出碼,
回傳值:成功回傳0,失敗回傳錯誤碼
Compile and link with -pthread.
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
//新執行緒的代碼
void* thread_run(void*arg){
printf("I am %s,my pthread id:%lu, my pid:%d\n",(char*)arg,pthread_self(),getpid());
sleep(5);
return (void*)10;
}
int main(){
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"thread 1");//創建新執行緒
//主執行緒的代碼
printf("I am main thread,my thread id:%lu ,mypid:%d\n",pthread_self(),getpid());
sleep(2);
void*ret=NULL;
pthread_join(tid,&ret);
printf("pthread quit code:%d\n",(long long)ret);//在64位下指標為8位元組
return 0;
}

呼叫該函式的執行緒將阻塞等待,直到id為thread的執行緒終止,thread執行緒以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:
- 如果thread執行緒通過return回傳,retval所指向的單元里存放的是thread執行緒函式的回傳值,
- 如果thread執行緒被別的執行緒呼叫pthread_ cancel例外終掉,retval所指向的單元里存放的是常數PTHREAD_CANCELED(-1),
- 如果thread執行緒是自己呼叫pthread_exit終止的,retval所指向的單元存放的是傳給pthread_exit的引數,
- 如果對thread執行緒的終止狀態不感興趣,可以傳NULL給retval引數,
如果執行緒出例外,那整個行程都會終止,因此無法獲取退出碼,
2.7. 執行緒的分離
默認情況下,新創建的執行緒是需要被等待退出的,否則會無法釋放資源,如果不關心執行緒的回傳值,這時執行緒等待就是一種負擔,這個時候我們可以告訴作業系統進,當執行緒退出時,自動釋放執行緒資源,這就是執行緒分離,
當新執行緒分離之后,主執行緒就不會再關注新執行緒的情況,新執行緒的資源就獨立了,執行緒退出之后,自己就釋放了自己的資源,不再往自己的PCB之中寫入退出碼,主執行緒也不再需要進行等待,

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
//新執行緒的代碼
void* thread_run(void*arg){
pthread_detach(pthread_self());//新執行緒自己分離自己
while(1){
printf("I am %s,my pthread id:%lu, my pid:%d\n",(char*)arg,pthread_self(),getpid());
sleep(1);
}
}
int main(){
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"thread 1");//創建新執行緒
//主執行緒的代碼
printf("I am main thread,my thread id:%lu ,my pid:%d\n",pthread_self(), getpid());
sleep(2);
void*ret=NULL;
pthread_join(tid,&ret); //主執行緒等待新執行緒
printf("pthread quit code:%d\n",(long long)ret);//在64位下指標為8位元組
pthread_exit((void*)10); //主執行緒退出
}

可以看到新執行緒分離以后,主執行緒并沒有等待新執行緒,而是直接執行主執行緒退出的代碼,
補充一點:即使執行緒分離以后,其中任何一個執行緒出現了例外,整個行程還是會被終止,
三、 行程和執行緒的區別匯總
3.1. 行程和執行緒的概念
行程: 行程是作業系統資源分配的基本物體
執行緒: 執行緒是CPU調度和分配的基本單位
執行緒共享行程資料,但也擁有自己的一部分資料:執行緒ID、暫存器、堆疊、errno、信號屏蔽字、調度優先級
行程的多個執行緒共享同一地址空間,因此Text Segment、Data Segment都是共享的,如果定義一個函式,在各執行緒中都可以呼叫,如果定義一個全域變數,在各執行緒中都可以訪問到,除此之外,各執行緒還共享以下行程資源和環境:檔案描述符表、每種信號的處理方式(SIG_ IGN、SIG_ DFL或者自定義的信號處理函式)、當前作業目錄、用戶id和組id
行程和執行緒的關系:
1.一個執行緒只能屬于一個行程,但是一個行程可以有多個執行緒(至少一個執行緒),只有一個執行緒的叫單執行緒,一個行程至少有一個執行緒,行程:執行緒=1:n
2.資源分配給行程之后,行程內部的執行緒都可以共享該行程的資源
3.在處理機上運行的是執行緒
4.執行緒在執行的程序中需要協作同步,不同行程的執行緒需要利用訊息通信來實作同步
3.5. 為什么要引入執行緒
1.更加易于調度
2.提高并發性,因為可以創建多個執行緒去執行同一個行程的不同部分
3.開銷少,因為創建行程的話要創建PCB,存放背景關系資訊,檔案資訊等等,開銷比較大,創建執行緒的話開銷就會比較少
4.充分發揮多處理器的功能,如果創建出多執行緒行程,那么可以讓執行緒在不同的處理器上運行,這樣不僅可以提高效率,同時也發揮了每個處理器的作用,
3.6. 行程和執行緒的區別
根本區別: 行程是作業系統分配資源的基本物體,執行緒是CPU調度的基本單位
開銷方面: 每個行程都有自己獨立的代碼和資料空間,因此行程之間的切換會有較大的開銷,但是執行緒在行程的地址空間內部運行,因此同一類執行緒共享代碼和資料空間,每個執行緒都有自己獨立的運行堆疊和程式計數器,因此執行緒之間的切換開銷小,
所處環境: 在作業系統中能同時運行多個行程,在同一個行程中有多個執行緒同時執行
記憶體分配: 系統在運行的時候會給每個行程分配不同的記憶體空間,但是不會給執行緒分配,執行緒使用的資源均來自于行程
包含關系: 執行緒是行程的一部分,沒有執行緒的行程叫做單執行緒行程,有多個執行緒的行程叫做多執行緒行程
四、 執行緒互斥
首先補充幾個概念:
- 臨界資源:多執行緒執行流共享的資源就叫做臨界資源
- 臨界區:每個執行緒內部,訪問臨界資源的代碼,就叫做臨界區
- 互斥:任何時刻,互斥保證有且只有一個執行流進入臨界區,訪問臨界資源,通常對臨界資源起保護作用
- 原子性:不會被任何調度機制打斷的操作,該操作只有兩態,要么完成,要么未完 成
比如我們的執行緒都去訪問同一個全域變數,這個全域變數就是臨界資源,而我們的main函式之中的資源,其它的執行緒也能看到,但是不會去進行訪問,因此不是臨界資源,
下面的代碼可以驗證臨界資源:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int a=10;//全域變數,所有的執行緒都能訪問
void *thread_run(void*arg){
while(1){
printf("%s,%lu,pid:%d,global a:%d,%p\n",(char*)arg,pthread_self(),getgid(),a,&a);
sleep(1);
}
return (void*)10;
}
int main(){
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1,NULL,thread_run,"thread 1");
pthread_create(&tid2,NULL,thread_run,"thread 2");
printf("before a:%d,%p\n",a,&a);
sleep(3);
a=100;
printf("after a:%d,%p\n",a,&a);
pthread_exit((void*)0);
}

可以看到它們共享的全域變數是同一個地址,因此是臨界資源,當主執行緒將其修改后,其他執行緒訪問的就是修改后的值,
另外,thread_run這個函式被兩個執行流執行,因此該函式是重入函式,
4.1. 多個執行緒訪問臨界資源帶來的問題
正常情況,假設我們定義一個變數 i ,這個變數 i 一定是保存在記憶體的堆疊當中的,我們要對這個變數 i 進行計算的時候,是CPU(兩大核心功能:算術運算和邏輯運算)來計算的,假設要對變數 i = 10 進行 +1 操作,首先要將記憶體堆疊中的 i 的值為 10 告知給暫存器,此時,暫存器中就有一個值 10,讓后讓CPU對暫存器中的這個 10 進行 +1 操作,CPU +1 操作完畢后,將結果 11 回寫到暫存器當中,此時暫存器中的值被改為 11,然后將暫存器中的值回寫到記憶體當中,此時 i 的值為 11,
執行緒不安全:
而在多執行緒的情況下:假設有兩個執行緒,執行緒A和執行緒B,執行緒A和執行緒B都想對全域變數 i 進行++,
假設全域變數 i 的值為 10,執行緒A從記憶體中把全域變數 i = 10 讀到暫存器當中,此時,執行緒A的時間片到了,執行緒A被切換出來了,執行緒A的背景關系資訊中保存的是暫存器中的i = 10,程式計數器中保存的是下一條即將要執行的 ++ 指令,若此時執行緒B獲取了CPU資源,也想對全域變數 i 進行 ++ 操作,因為此時執行緒A并未將運算結果回傳到記憶體當中,所以執行緒B從記憶體當中讀到的全域變數 i 的值還是10,然后將 i 的值讀到暫存器中,然后再在CPU中進行 ++ 操作,然后將 ++ 后的結果 11,回寫到暫存器,暫存器再回寫到記憶體,此時記憶體當中 i 的值已經被執行緒B機型 ++ 后改為了 11,然后執行緒B將CPU資源讓出來,此時執行緒A再切換回來的時候,它要執行的下一條指令是程式計數器中保存的對 i 進行 ++ 操作 ,而執行緒A此時 ++ 的 i 的值是從背景關系資訊中獲取的,背景關系資訊中此時的 i = 10 ,此時執行緒A在CPU中完成對 i 的 ++ 操作,然后將結果 11 回寫給暫存器,然后由暫存器再回寫給記憶體,此時記憶體中的 i 被執行緒B改為了 11,雖然 ,執行緒A和執行緒B都對全域變數 i 進行了 ++ ,按理說最終全域變數 i 的值應該為12,而此時全域變數 i 的值卻為11,
執行緒A對全域變數 i 加了一次,執行緒B也對全域變數 i 加了一次,而此時,全域變數的值為 11 而不是 12,由此就產生了多個執行緒同時操作臨界資源的時候有可能產生二義性問題(執行緒不安全現象)
比如下面的四個新執行緒搶票程式:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100; //初始100張票,臨界資源
void *route(void *arg) {
int ticket_sum=0;//記錄搶票的個數
while (1) { //臨界區
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", (char*)arg, ticket);
ticket_sum++:
ticket--;
}
else {
printf("I am %s,i get %d tickets!\n",(char*)arg,ticket_sum);
break;
}
}
}
int main( ) {
//創建四個新執行緒執行搶票代碼
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}

因為ticket為臨界資源,而多個執行緒同時訪問臨界資源,導致資訊不一致的問題
比如票數為1的時候,有三個執行緒進入if條件判斷,一個執行緒對其進行了修改,臨界資源ticket變為了0,而另外一個執行緒當它將ticket加載到暫存器進行自減操作時,ticket已經被修改成了0,這就出現ticket為負數的情況,
為了減少出現這種沖突的情況,我們可以讓執行緒在進入if判斷之前先休眠幾秒:


當然這種方法并不好,最優解還是要加鎖,
4.2. 鎖
為了解決多個執行緒同時訪問臨界資源帶來的問題,提出了鎖的概念,linux之中將這把鎖叫做互斥量
加鎖的粒度越小越好,因為加鎖的地方是串行的,加鎖的地方越多,串行的地方也隨之越多,對多執行緒的榷訓作用也就越大,
4.2.1. 鎖(互斥量)的介面
初始化鎖和釋放鎖:

#include <pthread.h>
//銷毀鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//初始化鎖
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
mutex:要釋放或初始化的鎖
atrr:系統自動設定,不關心,
//也可以使用這個全域變數,但是太麻煩
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
初始化完鎖以后還要加鎖和解鎖:

對上面的代碼進行加鎖:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100; //初始100張票,臨界資源
pthread_mutex_t lock;//創建鎖
void *route(void *arg) {
int ticket_sum=0;//記錄搶票的個數
while (1) { //臨界區
pthread_mutex_lock(&lock);//進入回圈時加鎖
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", (char*)arg, ticket);
ticket_sum++:
ticket--;
pthread_mutex_unlock(&lock);//出回圈時解鎖
//執行緒切換是在內核態轉為用戶態時,因此可以增加一些系統呼叫,防止快的行程把票全搶了
usleep(1000);
}
else {
printf("I am %s,i get %d tickets!\n",(char*)arg,ticket_sum);
pthread_mutex_unlock(&lock);//出回圈時解鎖
break;
}
}
}
int main( ) {
pthread_mutex_init(&lock,NULL);//初始化鎖
//創建四個新執行緒執行搶票代碼
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&lock);//釋放鎖
}

4.2.2. 互斥量(鎖)的實作原理
為了實作互斥鎖操作,大多數體系結構都提供了swap或exchange指令,該指令的作用是把暫存器和記憶體單元的資料相交換,由于只有一條指令,保證了原子性,即使是多處理器平臺,訪問記憶體的總線周期也有先后,一個處理 器上的交換指令執行時另一個處理器的交換指令只能等待總線周期,
互斥鎖的底層是一個互斥量,而互斥量的本質就是一個計數器,計數器的取值只有兩種情況,一種是 1 ,一種是 0 ;
1:表示當前臨界資源可以被訪問,
0:表示當前臨界資源不可以被訪問,


整個程序中,為1的mutex只有一份,exchange一潭訓編就完成了暫存器和記憶體資料的交換,
假設A執行緒在申請鎖,由于每個執行緒的暫存器是私有的,而鎖是共享的,在交換的程序之中,A會將自身暫存器之中的0和互斥鎖的1進行交換,
如果此時B搶占了A行程(發生了行程切換),由于背景關系保護,A執行緒CPU暫存器中的1會被保存到TSS中,B與互斥鎖交換,此時的互斥鎖的內容為0,if判斷不成立,B被掛起等待,
A執行緒回來之后,從TSS中恢復資料,由于TSS中保存了從互斥鎖中交換的1(TSS中的1加載到CPU暫存器中),if判斷成功,A申請鎖成功,
解鎖:將CPU暫存器中的1和互斥鎖中的0進行交換,
4.3. 死鎖
死鎖是指在同一組行程中的各個行程均占有不會釋放的資源,但因互相申請被其它行程所占有不會釋放的資源而處于一種永久等待的狀態,比如執行緒A獲取到互斥鎖1 ,執行緒B獲取到互斥鎖2的時候,執行緒A和執行緒B同時還想獲取對方手里的鎖(執行緒A還想獲取互斥鎖2,執行緒B還想獲取互斥鎖1),此時就會導致死鎖,
4.3.1. 死鎖的四個必要條件
-
互斥條件:
一個資源每次只能被一個執行流使用 -
請求與保持條件:
一個執行流引請求資源而阻塞時,對方已獲得的資源保持不放
解決辦法:允許行程只獲得運行初期需要的資源,便開始運行,在運行程序中逐步釋放掉分配到的已經使用完畢的資源,然后再去請求新的資源,
-
不剝奪條件:
一個執行流已獲得的資源,在未使用完之前,不能強行剝離 -
回圈等待條件:
若干執行流之間形成一種頭尾相接的回圈等待資源的關系
4.3.2. 避免死鎖的方法
- 破壞死鎖的四個必要條件(隨便破壞一個即可)
- 加鎖順序一致
- 避免鎖未釋放的場景
- 資源一次性分配(能不用鎖就不用鎖)
五、 可重入函式和執行緒安全
執行緒安全:
多個執行緒并發同一段代碼時,出現不同的結果,常見對全域變數或者靜態變數進行操作,并且沒有鎖保護的情況下,會出現該問題(多個執行緒,呼叫不可重入函式導致的),STL和智能指標都不是執行緒安全的,STL極度追求效率、加鎖會造成效率的影響,
可重入函式:
同一個函式,被不同的執行流呼叫,當前一個執行流還沒執行完,就有其它的執行流再次進入,我們成之為重入函式,
一個函式在重入的情況下,運行結果不會出現任何問題,該函式被稱為可重入函式,否則是不可重入函式(大部分的函式都是不可重入的)
常見執行緒不安全情況:
- 不保護共享變數的函式
- 函式狀態隨著被呼叫,狀態發生變化的函式
- 回傳指向靜態變數指標的函式
- 呼叫執行緒不安全函式的函式
常見的執行緒安全的情況:
- 每個執行緒對全域變數或者靜態變數只有讀取的權限,沒有寫入的權限,就不會改變共享資源,一般就是安全的
- 類或者介面對于執行緒來說都是原子操作
- 多個執行緒之間的切換不會導致該介面的執行結果存在二義性
常見不可重入的情況:
- 呼叫了malloc/free函式,因為malloc函式是用全域鏈表來管理堆的
- 呼叫了標準I/O庫函式,標準I/O庫的很多實作都以不可重入的方式使用全域資料結構
- 可重入函式體內使用了靜態的資料結構
常見可重入的情況:
-
不使用全域變數或者靜態變數
每個執行緒私有一個堆疊,而全域變數或者靜態變數是共享的,因此不使用全域或者靜態變數就能在一定程度上保證安全吸頂, -
不使用malloc或者new開辟空間
malloc和new的空間都是在堆上的,因此STL的容器基本上是不可重入的,因為它們都會自動進行擴容, -
不呼叫不可重入函式
-
不回傳靜態或者全域資料,所有資料都由函式的呼叫者提供
-
使用本地資料,或者通過制作全域資料的本地拷貝來保護全域資料
比如C語言提供的介面基本上都包括全域變數errno,在執行緒之中就將其拷貝了一份,保證每個執行緒是私有的
可重入與執行緒安全聯系
- 函式是可重入的,那就是執行緒安全
- 函式是不可重入的,那就不能由多個執行緒使用,有可能引發執行緒安全問題
- 如果一個函式中有全域變數,那么這個函式既不是執行緒安全也不是可重入的
可重入與執行緒安全區別
- 可重入函式是執行緒安全函式的一種
- 執行緒安全不一定是可重入的,而可重入函式一定是執行緒安全的
六、執行緒同步
在保證資料安全的前提下(一般使用加鎖的方式),讓多個執行緒能夠按照某種特定的順序訪問臨界資源,從而有效的避免饑餓問題,這種就叫做同步, 同步是為了協同高效完成某些事物,
比如有兩個執行緒,執行緒A負責往佇列之中添加資料(佇列為空就添加),B負責從佇列之中讀取資料(佇列不為空),
如果執行緒A的優先級高于執行緒B,競爭力比B強, 假設1萬次中,A連續成功的競爭申請到了9千次鎖,但是只在第一次放入了資料,后面因為佇列不為空,因此只是重復的進行申請鎖和釋放鎖的程序,
這樣雖然不會出錯,成功的保護了臨界資源,但是A做了很多沒有意義的事情,這樣效率就會非常低下,非常不合理,
合理的方式是:
A申請鎖,放資料,釋放鎖、當A條件不滿足(佇列不為空)后就不再進行申請了,直接通知B來申請;
B再來申請鎖,讀資料,釋放鎖、當B執行緒的條件不滿足后(佇列為空),也不再繼續了,然后通知A,
執行緒同步的編碼方式:
- 當執行緒訪問臨界資源,如果條件不滿足,就掛起等待,釋放鎖
- 發現條件不滿足,通知對方
6.1. 條件變數及其介面
當一個執行緒互斥地訪問某個變數時,它可能發現在其它執行緒改變狀態之前,它什么也做不了,
例如一個執行緒訪問佇列時,發現佇列為空,它只能等待,只到其它執行緒將一個節點添加到佇列中,這種情況就需要用到條件變數,

#include <pthread.h>
//釋放條件變數
int pthread_cond_destroy(pthread_cond_t *cond);
//初始化條件變數
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
cond:要釋放或初始化的條件變數
attr:系統自動設定,不關心
//全域變數
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
條件等待:

#include <pthread.h>
//不阻塞等待,
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
//阻塞等待,直到被喚醒
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
和鎖類似,條件變數需要喚醒等待的執行緒:

以下面的代碼為例:
#include <stdio.h>
#include <pthread.h>
#include<unistd.h>
pthread_mutex_t lock;//創建一個鎖
pthread_cond_t eat_cond;//創建吃飯的條件變數
pthread_cond_t cook_cond;//創建做飯的條件變數
int rice_bowl=0;//一開始飯的碗數為0
void* Eat(void*arg){
while(1){
sleep(1);
pthread_mutex_lock(&lock);//加鎖
if(rice_bowl<=0){
pthread_cond_wait(&eat_cond,&lock);//當沒飯時,吃飯的執行緒等待
}
rice_bowl--;//吃飯,飯碗-1
printf("I am %s,i am eating!\n",(char*)arg);
pthread_mutex_unlock(&lock);
if(rice_bowl<=0){
pthread_cond_signal(&cook_cond);//通知做飯的執行緒做飯
}
}
}
void*Cook(void*arg){
while(1){
sleep(1);
pthread_mutex_lock(&lock);
if(rice_bowl>0){
pthread_cond_wait(&cook_cond,&lock);//當飯的數量多于0,做飯的執行緒等待
}
rice_bowl++;//做飯,飯碗數量+1
printf("I am %s,i am cooking!\n",(char*)arg);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&eat_cond);//通知吃飯的行程吃飯
}
}
int main(){
pthread_mutex_init(&lock,NULL);//初始化鎖
pthread_cond_init(&eat_cond,NULL);//初始化條件變數
pthread_cond_init(&cook_cond,NULL);//初始化條件變數
pthread_t t1,t2;//創建吃飯和做飯的行程
pthread_create(&t2,NULL,Eat,"Diner");
pthread_create(&t1,NULL,Cook,"Cook");
while(1){}
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_mutex_destroy(&lock);//釋放鎖
pthread_cond_destroy(&eat_cond);//釋放條件變數
pthread_cond_destroy(&cook_cond);//釋放條件變數
return 0;
}

如果是一個Cook做飯,兩個Diner吃的情況:
#include <stdio.h>
#include <pthread.h>
#include<unistd.h>
pthread_mutex_t lock;//創建一個鎖
pthread_cond_t eat_cond;//創建吃飯的條件變數
pthread_cond_t cook_cond;//創建做飯的條件變數
int rice_bowl=0;//一開始飯的碗數為0
void* Eat(void*arg){
while(1){
sleep(2);//為了防止出現一個行程每次都搶到鎖的情況,可以增加一些系統呼叫,休眠時間由1秒增加到2秒
pthread_mutex_lock(&lock);//加鎖
while(rice_bowl<=0){
pthread_cond_wait(&eat_cond,&lock);//當沒飯時,吃飯的執行緒等待
}
rice_bowl--;//吃飯,飯碗-1
printf("I am %s,i am eating!The number of bowl:%d\n",(char*)arg,rice_bowl);
pthread_mutex_unlock(&lock);
if(rice_bowl<=0){
pthread_cond_signal(&cook_cond);//通知做飯的執行緒做飯
}
}
}
void*Cook(void*arg){
while(1){
sleep(1);
pthread_mutex_lock(&lock);
while(rice_bowl>0){
pthread_cond_wait(&cook_cond,&lock);//當飯的數量多于0,做飯的執行緒等待
}
rice_bowl++;//做飯,飯碗數量+1
printf("I am %s,i am cooking!The number of bowl:%d\n",(char*)arg,rice_bowl);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&eat_cond);//通知吃飯的行程吃飯
}
}
int main(){
pthread_mutex_init(&lock,NULL);//初始化鎖
pthread_cond_init(&eat_cond,NULL);//初始化條件變數
pthread_cond_init(&cook_cond,NULL);//初始化條件變數
pthread_t t1,t2,t3,t4;//創建吃飯和做飯的行程
pthread_create(&t2,NULL,Eat,"Diner");
pthread_create(&t1,NULL,Cook,"Cook");
pthread_create(&t3,NULL,Eat,"Diner");
while(1){}
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_mutex_destroy(&lock);//釋放鎖
pthread_cond_destroy(&eat_cond);//釋放條件變數
pthread_cond_destroy(&cook_cond);//釋放條件變數
return 0;
}

如果是多對多的情況,繼續使用if判斷,一開始沒有飯,此時Diner1拿到了鎖,則Diner1判斷碗里沒有面后將自己放入PCB等待佇列中進行等待,然后釋放互斥鎖,假設此時,Cook1拿到了互斥鎖,然后Cook1做了一碗飯,然后釋放鎖并通知PCB等待佇列,此時Diner1已經出隊,假設此時Diner2拿到了鎖,并吃了一碗飯,然后釋放鎖,然后Diner1又拿到了鎖,而此時Diner1將要執行的是跳過pthread_cond_wait函式,則Diner1跳過了判斷碗里是否有面,直接往碗里吃面,此時rice_bowl的值就由0變成-1,為了防止這種情況要把if判斷改成while回圈,
6.2. 為什么等待的時候需要傳入互斥鎖
pthread_cond_wait函式會在內部對互斥鎖進行解鎖,當有執行緒進去之后要把鎖釋放別人才能用,解鎖之后,其他的執行流才能獲取到這把互斥鎖,所以,需要傳入互斥鎖,否則,如果在呼叫pthread_cond_wait執行緒在進行等待的時候,不釋放互斥鎖,其他執行緒就不能訪問臨界資源,
當pthread_cond_wait函式回傳時,回傳到了臨界區內,所以該函式會讓執行緒重新擁有鎖,
因此,這個函式的功能可以總結如下:
- 等待條件變數滿足;
- 把獲得的鎖釋放掉;(注意:1,2兩步是一個原子操作)
當然如果條件滿足了,那么就不需要釋放鎖,所以釋放鎖這一步和等待條件滿足一定是一起執行(指原子操作), - pthread_cond_wait()被喚醒時,它解除阻塞,并且嘗試獲取鎖(不一定拿到鎖),因此,一般在使用的時候都是在一個回圈里使用pthread_cond_wait()函式,因為它在回傳的時候不一定能拿到鎖(這可能會發生餓死情形,當然這取決于作業系統的調度策略),
七、 生產者消費者模型
生產者消費者模型是生產者將產品放在交易場所,消費者將東西拿回去,
在計算機中本質是有一段記憶體空間,有多個執行緒進行生產,有多個執行緒進行消費
7.1. 生產者消費者遵循的規則(321規則)

7.2. 生產者消費者模型的優點
- 解耦
- 支持并發
- 支持忙閑不均

耦合性:是一種軟體度量,是指一程式中,模塊及模塊之間資訊或引數依賴的程度,內聚性則是耦合性的相對概念,低耦合性是結構良好程式的特性,低耦合性程式的可讀性及可維護性會比較好,

7.3. 基于BlockingQueue(阻塞佇列)的生產者消費者模型
BlockingQueue 在多執行緒編程中阻塞佇列(Blocking Queue)是一種常用于實作生產者和消費者模型的資料結構,其與普通的佇列區別在于,當佇列為空時,從佇列獲取元素的操作將會被阻塞,直到佇列中被放入了元素;當佇列滿時,往佇列里存放元素的操作也會被阻塞,直到有元素被從佇列中取出(以上的操作都是基于不同的執行緒來說的,執行緒在對阻塞佇列行程操作時會被阻塞),

下面是兩個生產者和兩個消費者的模型:
BlockQueue.hpp:
#ifndef _QUEUE_BLOCK_H_ //防止頭檔案重復包含
#define _QUEUE_BLOCK_H_
#include<iostream>
#include<pthread.h>
#include <unistd.h>
#include<queue>
class BlockQueue{
private:
std:: queue<int> q;
size_t cap;
pthread_mutex_t lock; //生產者和消費者共同競爭的鎖
pthread_mutex_t c_lock;//消費者的鎖
pthread_mutex_t p_lock;//生產者的鎖
pthread_cond_t c_cond;//消費者在該條件變數下等
pthread_cond_t p_cond;//生產者在該條件變數下等
public:
BlockQueue(size_t _cap):cap(_cap){//建構式,初始化鎖和條件變數
pthread_mutex_init(&lock,nullptr);
pthread_mutex_init(&c_lock,nullptr);
pthread_mutex_init(&p_lock,nullptr);
pthread_cond_init(&c_cond,nullptr);
pthread_cond_init(&p_cond,nullptr);
}
bool IsFull(){//判斷佇列是否為滿
return q.size()>=cap;
}
bool IsEmpty(){//判斷佇列是否為空
return q.empty();
}
void LockQueue(){//實作加鎖
pthread_mutex_lock(&lock);
}
void UnlockQueue(){//實作解鎖
pthread_mutex_unlock(&lock);
}
void LockQueueComsumer(){//實作消費者加鎖
pthread_mutex_lock(&c_lock);
}
void UnlockQueueComsumer(){//實作消費者解鎖
pthread_mutex_unlock(&c_lock);
}
void LockQueueProducer(){//實作生產者加鎖
pthread_mutex_lock(&p_lock);
}
void UnlockQueueProducer(){//實作生產者解鎖
pthread_mutex_unlock(&p_lock);
}
void WakeUpComsumer(){//喚醒消費者
std::cout<<"wake up consumer!"<<std::endl;
pthread_cond_signal(&c_cond);
}
void WakeUpProducer(){//喚醒生產者
std::cout<<"wake up producer!"<<std::endl;
pthread_cond_signal(&p_cond);
}
void ComsumerWait(){//消費者等待
std::cout<<"consumer wait!"<<std::endl;
pthread_cond_wait(&c_cond,&lock);
}
void ProducerWait(){//生產者等待
std::cout<<"producer wait!"<<std::endl;
pthread_cond_wait(&p_cond,&lock);
}
void Put(int in){//生產者進行寫入
LockQueue();//生產者和消費者會競爭同一把鎖
while(IsFull()){//判斷是否已滿,如果已滿,生產者進行等待并喚醒消費者進行讀資料
WakeUpComsumer();//喚醒消費者進行讀資料
std::cout<<"queue full,notify consumer,producer stop!"<<std::endl;
ProducerWait();//生產者等待消費者進行讀資料
}
q.push(in);
UnlockQueue();//釋放掉鎖
}
void Get(int& out){//消費者進行讀取
LockQueue();//生產者和消費者會競爭同一把鎖
while(IsEmpty()){//如果為空,消費者進行等待,通知生產者寫入資料
WakeUpProducer();//喚醒生產者進行寫入
std::cout<<"queue empty,notify producer,consumer stop"<<std::endl;
ComsumerWait();//消費者等待生產者進行寫入
}
out=q.front();
q.pop();
UnlockQueue();//釋放掉鎖
}
~BlockQueue(){
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&c_cond);
pthread_cond_destroy(&p_cond);
}
};
main.cpp:
#include"BlockQueue.hpp"
using namespace std;
void*consumer_run(void*arg){
BlockQueue*bq=(BlockQueue*)arg;
while(true){
int n=0;
bq->LockQueueComsumer();//兩個消費者先競爭一個鎖
bq->Get(n);
cout<<"consumer "<<pthread_self()<<" data is:"<<n<<endl;
bq->UnlockQueueComsumer();//釋放掉消費者的鎖
sleep(1);
}
}
void*producer_run(void*arg){
sleep(1); //為了讓消費者先搶到鎖,讓生產者先休眠1秒
BlockQueue*bq=(BlockQueue*)arg;
int count=0;
while(true){
bq->LockQueueProducer();//兩個生產者先競爭一個鎖
count=count%5+1;
bq->Put(count);
cout<<"producer "<<pthread_self()<<" data is:"<<count<<endl;
bq->UnlockQueueProducer();//釋放生產者的鎖
sleep(1);
}
}
int main(){
BlockQueue*bq=new BlockQueue(5);
pthread_t c1;//創建生產者和消費者的執行緒
pthread_t c2;//創建生產者和消費者的執行緒
pthread_t p1;//創建生產者和消費者的執行緒
pthread_t p2;//創建生產者和消費者的執行緒
pthread_create(&c1,nullptr,consumer_run,(void*)bq);
pthread_create(&c2,nullptr,consumer_run,(void*)bq);
pthread_create(&p1,nullptr,producer_run,(void*)bq);
pthread_create(&p2,nullptr,producer_run,(void*)bq);
//等待執行緒
pthread_join(c1,nullptr);
pthread_join(p1,nullptr);
pthread_join(c2,nullptr);
pthread_join(p2,nullptr);
//釋放new出來的佇列
delete bq;
return 0;
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/342213.html
標籤:其他
上一篇:Linux動態庫和靜態庫


