文章目錄:
一、線程同步的目的
我們在進程部分講解了同步,互斥,臨界區等相關概念,這些概念在線程中也是一樣的。進程各自擁有自己的地址空間,進程間通訊方式如果爲共享內存,那麼就需要對進程進行同步控制。
多個線程會共享進程之間很多資源,存在數據共享性大,線程協作性高等特點,如果不對多線程進行同步控制,如果多線程共同讀取共享數據,不會對共享數據進行修改等改變操作,那還不會出現什麼問題,但是一旦多個線程對共享數據進行寫,修改操作,就會讓程序的結果變得不可控,產生不一致結果,這並不是我們想看到的,故爲了讓每次的程序結果一致,同步,必須採取一定的線程同步機制讓線程同步。
二、線程同步方式
線程同步方式和進程的很像,都是對臨界資源進行控制,處理線程互斥,同步這兩種關係。主要有四種方式:互斥量,讀寫鎖,信號量,條件變量
。
(一)互斥量(互斥鎖)
1. 基本概念
互斥量(也稱爲互斥鎖)
類似於進程通信方式的二元信號量,互斥鎖是通過簡單加鎖的方法來控制對臨界資源的訪問,實現任意時間只有一個進程訪問臨界資源
。互斥鎖只有兩種狀態,線程在進入臨界區之前,加鎖操作
。線程在退出臨界區之後,解鎖操作
。鎖有兩種狀態 加鎖,解鎖
。
【1. 使用步驟:】
- 線程在訪問臨界資源之前,對臨界區代碼進行
加鎖操作
,如果鎖是加鎖狀態
的,則線程執行加鎖操作將被阻塞
,直到鎖解鎖可解除解鎖。即表示這塊臨界區只允許一個線程進行訪問。 - 進行臨界區代碼執行,即對臨界資源進行操作的代碼運行。
- 退出臨界區之後,進行
解鎖操作
。
可以概括爲:
在這種方式下,每次只有一個線程可以訪問臨界資源。
【2. 互斥鎖的特點:】
- 原子性: 即加鎖、解鎖操作是一個原子操作,在一個線程執行過程中不可能被其他線程打斷。
- 唯一性: 一旦一個線程執行了加鎖操作,在其解鎖之前,沒有其他線程可以成功再次加鎖。
- 非繁忙等待: 如果一個線程已經鎖定了一個互斥量,第二個線程又試圖去鎖定這個互斥量,則第二個線程將被阻塞掛起,不佔用任何CPU資源,直到第一個線程解除對這個互斥量的鎖定爲止,第二個線程則被喚醒並繼續執行,同時鎖定這個互斥量。
- 一般用於解決競爭/互斥關係,即多個線程對於資源的爭奪。
- 加鎖,解鎖在代碼中必須成對出現,即一個線程加鎖,解鎖;不能一個線程加鎖,一個線程解鎖,會容易出現死鎖。
- 一把鎖只能用於對一個資源的互斥訪問,不能實現多個資源的多線程互斥問題。
【3. 互斥鎖函數操作:】
-
定義互斥鎖: 一般將鎖定義到
全局
,方便多個線程可以對其操作。pthread_mutex_t mutex;//定義名爲mutex的互斥鎖
-
初始化互斥鎖:
對定義的互斥鎖進行初始化,函數原型如下:# include<pthread.h> int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr) //成功返回0,失敗返回錯誤編號
參數:
(1)第一個參數:將定義的互斥鎖地址
傳入即可。
(2)第二個參數:一般將attr設置爲NULL
,表示用默認的屬性初始化互斥量。初始化的鎖處於解鎖狀態
-
加鎖:
對互斥鎖進行加鎖獲取臨界資源的方式主要兩種:直接去加鎖,嘗試加鎖。- 直接加鎖:
# include<pthread.h> int pthread_mutex_lock(pthread_mutex_t* mutex) //成功返回0,失敗返回錯誤編號
參數爲互斥鎖地址,
如果要加鎖的互斥量已被上鎖,則調用者一直阻塞,直到互斥鎖解鎖後再上鎖
。- 嘗試加鎖:
# include<pthread.h> int pthread_mutex_trylock(pthread_mutex_t* mutex) //成功返回0,失敗返回錯誤編號
參數爲互斥鎖地址,調用此函數,如果互斥鎖爲加鎖,則上鎖,返回0;
若互斥鎖已加鎖,則函數加鎖失敗,返回EBUSY,線程不會阻塞。
-
解鎖:
一般和加鎖函數成對出現,函數原型爲:# include<pthread.h> int pthread_mutex_unlock(pthread_mutex_t* mutex) //成功返回0,失敗返回錯誤編號
參數爲要解鎖的互斥鎖地址。
-
銷燬互斥鎖:
使用完互斥鎖後,必須對其進行銷燬,釋放資源,函數運行爲:# include<pthread.h> int pthread_mutex_destroy(pthread_mutex_t* mutex) //成功返回0,失敗返回錯誤編號
2. 實例
實現: 兩個線程,主線程負責接收用戶輸入,函數線程負責將用戶輸入打印到終端頁面。
思考:
- 數據傳遞:線程之間需要
共享數據
,所以不能定義爲局部的,必須定義爲全局
,堆都可以,我們使用全局數組buff來存儲數據。 - 線程關係:主線程沒有接收到數據時,函數線程不能打印;函數線程打印時,主線程不能讀取;主線程寫入時,函數線程不能打印。
總結出來就是:buff是臨界資源,主線程和函數線程對其進行訪問,所以在處理buff前進行加鎖操作,處理完成後解鎖。
處理:
加鎖→主線程寫入數據,寫完→解鎖;
加鎖→函數線程讀取數據,讀完→解鎖
那麼代碼爲:
# include<unistd.h>
# include<stdio.h>
# include<string.h>
# include<time.h>
# include<assert.h>
# include<pthread.h>
pthread_mutex_t mutex;//定義互斥鎖
char buff[128]={0};
void* fun(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
if(strncmp(buff,"end",3)==0)
{
break;
}
printf("fun:%s\n",buff);
memset(buff,0,128);
int n=rand()%3+1;
sleep(n);
pthread_mutex_unlock(&mutex);
n=rand()%3+1;
sleep(1);
}
}
int main()
{
srand((unsigned int)(time(NULL)*time(NULL)));
pthread_mutex_init(&mutex,NULL);//初始化的鎖是打開狀態
pthread_t id;
int res=pthread_create(&id,NULL,fun,NULL);//創建線程
assert(res==0);
while(1)
{
pthread_mutex_lock(&mutex);
printf("input:");
fgets(buff,127,stdin);
pthread_mutex_unlock(&mutex);
if(strncmp(buff,"end",3)==0)
{
break;
}
int n=rand()%3+1;
sleep(1);
}
pthread_join(id,NULL);
pthread_mutex_destroy(&mutex);
}
可以看到主線程獲取寫入一個,函數線程輸出一個。主線程先運行,創建函數線程後並不代表讓出CPU,直到sleep時纔會讓出CPU,這時纔會出現:函數線程和線程併發運行,搶佔CPU資源。
(二)讀寫鎖
1. 基本概念
讀寫鎖是進階的互斥鎖,它允許更高的並行性。互斥鎖有兩種狀態:加鎖,解鎖;讀寫鎖有三種狀態:
讀模式下加鎖狀態
寫模式下加鎖狀態
不加鎖狀態。
一次只有一個線程可以佔有寫模式的讀寫鎖,但是多個線程可以同時佔有讀模式的讀寫鎖。 故讀寫鎖也叫做共享-獨佔鎖
,當讀寫鎖以讀模式
鎖住時,它是以共享模式
鎖住的,當它以寫模式
鎖住時,它是以獨佔模式
鎖住的。
【1. 讀寫鎖的特點:】
- 多個讀者可以同時讀,即當讀寫鎖是
讀加鎖
狀態時,所有試圖以讀模式
對它進行加鎖
的線程都可以得到訪問權
,但如果線程希望以寫模式
對鎖進行加鎖
,它必須阻塞
直到所有的線程釋放讀鎖,故不存在讀-寫的可能。 - 只允許一個寫者寫,不允許寫-寫,寫-讀。即當讀寫鎖是
寫加鎖
狀態時,在這個鎖被解鎖之前,所有試圖對這個鎖加鎖
(讀鎖或寫鎖)的線程都會被阻塞
。 - 寫者優於讀者。當讀寫鎖處於
讀模式鎖住
狀態時,如果有另外的線程試圖以寫模式加鎖
,讀寫鎖通常會阻塞隨後的讀模式鎖請求
,也就是說:一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者。 - 讀寫鎖適合對數據結構讀的次數遠大於寫的情況下使用。
【2. 讀寫鎖函數操作:】
-
定義讀寫鎖
讀寫鎖一般也定義在全局:pthread_rwlock_t rwlock;//定義了名爲rwlock的讀寫鎖
-
初始化
對定義的讀寫鎖進行初始化,函數原型如下:# include<pthread.h> int pthread_rwlock_init(pthread_rwlock_t* mutex,const pthread_rwlockattr_t* attr) //成功返回0,失敗返回錯誤編號
參數:
(1)第一個參數:將定義的讀寫鎖地址
傳入即可。
(2)第二個參數:一般將attr設置爲NULL
,表示用默認的屬性初始化讀寫鎖。 -
加鎖
有兩種加鎖的方式:加讀鎖,加寫鎖。- 加讀鎖:
# include<pthread.h> int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);//可能會出現阻塞 int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);//不會阻塞,錯誤編號爲EBUSY //成功返回0,失敗返回錯誤編號
參數爲讀寫鎖地址,
在讀模式下鎖定讀寫鎖,可以允許多個線程進行加讀鎖讀取數據
。如果需要對讀鎖的數量進行限制,那麼需要檢查其返回值。- 加寫鎖:
# include<pthread.h> int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);//可能出現阻塞 int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock)//不會阻塞,錯誤編號爲EBUSY //成功返回0,失敗返回錯誤編號
參數爲讀寫鎖地址,
在寫模式下鎖定讀寫鎖,只允許一個線程加鎖,其他加鎖進行阻塞。
-
解鎖
# include<pthread.h> int pthread_rwlock_unlock(pthread_rwlock_t* rwlock) //成功返回0,失敗返回錯誤編號
-
銷燬鎖
# include<pthread.h> int pthread_rwlock_destroy(pthread_rwlock_t* rwlock) //成功返回0,失敗返回錯誤編號
2. 實例
實現: 主線程對buff寫入數據,創建的2個函數線程在主線程寫入後對buff進行打印。
思路: 主線程寫入時加寫鎖,兩個函數線程讀取時加讀鎖,這樣兩個函數線程可以同時讀取,就有:
主線程→寫鎖→將數據寫入buff→解鎖
函數線程fun→讀鎖→輸出buff數據→解讀鎖。
函數線程fun1→讀鎖→輸出buff數據→解讀鎖
注意:不要對buff進行memset清0,否則另一個函數線程讀不到了。
那麼代碼如下:
# include<unistd.h>
# include<stdio.h>
# include<string.h>
# include<time.h>
# include<assert.h>
# include<pthread.h>
pthread_rwlock_t rwlock;//創建讀寫鎖
char buff[128]={0};
void* fun(void* arg)//讀數據
{
while(1)
{
pthread_rwlock_rdlock(&rwlock);
if(strncmp(buff,"end",3)==0)
{
break;
}
printf("fun:%s\n",buff);
//memset(buff,0,128);
int n=rand()%3+1;
sleep(n);
pthread_rwlock_unlock(&rwlock);
n=rand()%3+1;
sleep(1);
}
}
void* fun1(void* arg)//讀數據
{
while(1)
{
pthread_rwlock_rdlock(&rwlock);
if(strncmp(buff,"end",3)==0)
{
break;
}
printf("fun1:%s\n",buff);
//memset(buff,0,128);
int n=rand()%3+1;
sleep(n);
pthread_rwlock_unlock(&rwlock);
n=rand()%3+1;
sleep(1);
}
}
int main()
{
srand((unsigned int)(time(NULL)*time(NULL)));
pthread_rwlock_init(&rwlock,NULL);//初始化
pthread_t id[2];
int res=pthread_create(&id[0],NULL,fun,NULL);//創建線程
int r=pthread_create(&id[1],NULL,fun1,NULL);
assert(res==0);
while(1)//寫數據
{
pthread_rwlock_wrlock(&rwlock);
printf("input:");
fgets(buff,127,stdin);
pthread_rwlock_unlock(&rwlock);
if(strncmp(buff,"end",3)==0)
{
break;
}
int n=rand()%3+1;
sleep(1);
}
pthread_join(id[0],NULL);
pthread_join(id[1],NULL);
pthread_rwlock_destroy(&rwlock);
}
可以看到fun1,fun加了讀鎖,可以同時讀取數據,主線程加了寫鎖,只能一個線程寫數據。
(三)信號量
1. 基本概念
在進程間通訊時我們講過信號量,它可以保護臨界資源,實現進程同步。線程的信號量和進程的信號量類似,但是注意:
- 進程對信號量P、V等操作需要我們用函數封裝,線程則是提供了一整套的方法,不需要我們去封裝P、V等操作,故
線程信號量操作比進程信號量操作簡單
。 - 線程信號量對於全局共享,但進程每個都有自己的地址空間,不能實現全局信號量進行進程間同步,故
進程的信號量可以在線程中使用,但線程的不可以在進程間使用
。
線程的信號量也是一個計數器,記錄臨界資源的個數,取值爲正整數,用信號量達到線程間的同步。
【1. 信號量的特點:】
- 信號量一般用於解決線程同步問題,即協調線程對於臨界資源的關係,就像一個紅綠燈一樣,但不代表不能解決互斥問題。
- 當信號量的初始值爲1時,表示只有一個臨界資源,可以看成互斥鎖。
- 信號量一般由一個線程釋放,另一個線程獲取,保證線程同步,這一點和鎖有很大的區別。
【2. 信號量函數操作:】
-
定義信號量
定義爲全局,多個線程可以共享# include<semaphore.h> sem_t sem1;//定義一個信號量
-
初始化、銷燬信號量
# include<semaphore.h> int sem_init(sem_t* sem,int shared,int val);//初始化 int sem_destroy(sem_t* sem);//銷燬 //成功返回0,出錯返回錯誤編號
初始化函數的參數
(1)sem:信號量指針。
(2)shared:是否在進程間共享標識,0爲不共享,1爲共享。Linux不支持,所以爲0。
(3)val:信號量初始值,爲正整數。 -
P操作
獲取資源,信號量值減一,函數原型爲:# include <semaphore.h> int sem_wait(sem_t* sem); //成功返回0,出錯返回錯誤編號
如果信號量爲0,那麼P操作會阻塞,只有信號量被釋放,阻塞纔會解除
-
V操作
釋放資源,信號量值加一,函數原型爲:# include <semaphore.h> int sem_post(sem_t* sem); //成功返回0,出錯返回錯誤編號
V操作永遠不會阻塞。
2. 實例
題目和互斥鎖一樣,我們嘗試用信號量來解決,經過上面的分析,我們不難發現這是一個:兩個線程對於buff臨界資源的協作關係,即主線程寫了,函數線程讀,函數線程讀了,主線程寫;並不是競爭關係。
那麼我們就設定兩個信號量分別來控制主線程和函數線程,讓它們達到一種協作關係:sem1控制函數線程,開始運行時函數線程阻塞等待主線程寫數據,所以初值爲0;sem2控制主線程,運行程序就開始對buff寫數據,所以初值爲1。那麼兩個線程的信號量操作如下:
- P(sem2),此時sem2=0 → 主線程運行向buff中寫數據 → 寫完數據,V(sem1),釋放信號,此時sem1=1,sem2=0,可以讀,不可以寫。
即兩個線程併發運行,只有函數線程可以運行讀取數據,因爲可以進行P操作,主線程不能運行,在P(sem2)操作時就會阻塞。
- P(sem1),此時sem1=0 → 函數線程運行讀取數據 → 讀取完畢,V(sem2),釋放信號,此時sem1=0,sem2=1,可以寫,不可以讀。
即兩個線程併發運行,只有主線程可以運行寫入數據,因爲可以進行P操作,函數線程不能運行,在P(sem1)操作時就會阻塞。
代碼如下:
# include<unistd.h>
# include<stdio.h>
# include<string.h>
# include<time.h>
# include<assert.h>
# include<pthread.h>
# include<semaphore.h>
sem_t sem1;
sem_t sem2;
char buff[128]={0};
void* fun(void* arg)
{
while(1)
{
sem_wait(&sem1);//P
if(strncmp(buff,"end",3)==0)
{
break;
}
printf("fun:%s\n",buff);
memset(buff,0,128);
int n=rand()%3+1;
sleep(n);
sem_post(&sem2);//V
n=rand()%3+1;
sleep(1);
}
}
int main()
{
srand((unsigned int)(time(NULL)*time(NULL)));
sem_init(&sem1,0,0);
sem_init(&sem2,0,1);
pthread_t id;
int res=pthread_create(&id,NULL,fun,NULL);
assert(res==0);
while(1)
{
sem_wait(&sem2);//P
printf("input:");
fgets(buff,127,stdin);
sem_post(&sem1);//V
if(strncmp(buff,"end",3)==0)
{
break;
}
int n=rand()%3+1;
sleep(1);
}
pthread_join(id,NULL);
sem_destroy(&sem1);
sem_destroy(&sem2);
}
可以看到實現了用信號量達到了線程間同步。
(四)條件變量
1. 基本概念
條件變量提供了一種線程間的通知機制,給多個線程提供了一個回合的場所
,條件變量自動阻塞一個進程,直到相應的某種特殊情況發生爲止,會喚醒等待這個條件的線程。這種方式提高了進程同步的合理性和高效性
。
條件本身是由互斥鎖保護的,和互斥鎖配合使用
,線程在改變條件狀態之前
必須鎖住互斥鎖
,如果一個條件
爲假
,則一個線程
自動阻塞
,並釋放
等待狀態改變的互斥鎖
。如果條件符合線程等待的條件
,則給狀態加鎖喚起線程
,之後解鎖
。
可以理解爲:一個線程修改條件,另一個線程等待條件,一旦等到自己需要的條件,就去運行。
【1. 條件變量的特點:】
- 需要和互斥鎖一起使用,纔可以起到線程間同步的作用。
- 允許線程以無競爭方式等待特定的條件發生,即條件改變這個信號會發送到所有等待這個條件發生的線程,不是說一個線程接收到這個消息其他線程就接收不到了。
【2. 條件變量函數操作:】
-
定義條件變量
定義爲全局,多個線程可以共享# include<pthread.h> pthread_cond_t cond ;//定義一個條件變量
-
初始化、銷燬條件變量
# include<pthread.h> int pthread_cond_init(pthread_cond_t* cond,pthread_condattr_t* attr)//初始化 int pthread_cond_destroy(pthread_cond_t* cond)//銷燬 //成功返回0,出錯返回錯誤編號
attr設置爲NULL。
-
等待條件爲真函數
函數原型爲:# include<pthread.h> int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex); //成功返回0,失敗返回錯誤編號
傳遞給此函數的互斥量對條件進行保護,調用者把
鎖住的互斥量傳給函數
,函數調用線程放到
等待條件的線程列表
上,然後對互斥量解鎖
,這兩步是原子操作
。等待函數返回
時,互斥量再次被鎖住
。所以使用此函數的代碼爲:
pthread_mutex_lock(&mutex); pthread_cond_wait(&cond,&mutex);//自動阻塞,直到條件發生 pthread_mutex_unlock(&mutex);
可以用一張圖來描述一下:
-
條件成立喚醒線程
# include<pthread.h> int pthread_cond_signal(pthread_cond_t* cond);//喚醒等待該條件的某個線程 int pthread_cond_broadcast(pthread_cond_t* cond);//喚醒等待該條件的所有進程 //成功返回0,失敗返回錯誤編號
使用時也應該先加鎖,再喚醒,再解鎖。
2. 實例
實現: 主線程給buff中寫入數據,其他兩個函數線程等待buff中有數據時纔可以進行數據打印,一旦輸入end結束符,則線程結束。
分析:
- 創建線程,通過傳參,創建出兩個線程。
- 函數線程先pthread_cond_wait()阻塞,等待buff數據寫入後,主線程喚醒。
- 主線程向buff中寫入數據後,根據寫入的數據進行線程喚醒,如果不是結束符,則喚起任意一個等待的線程輸出數據,如果是結束符end,則喚起所有等待的線程,所有線程break結束了線程。
那麼就可以寫出代碼:
# include<unistd.h>
# include<stdio.h>
# include<string.h>
# include<time.h>
# include<assert.h>
# include<pthread.h>
pthread_cond_t cond;//條件變量
pthread_mutex_t mutex;//互斥鎖
char buff[128]={0};
void* fun(void* arg)
{
char* s=(char*)arg;
while(1)
{
pthread_mutex_lock(&mutex);//加鎖
pthread_cond_wait(&cond,&mutex);//阻塞,等待有條件發生
pthread_mutex_unlock(&mutex);//解鎖
printf("%s:%s",s,buff);//
if(strncmp(buff,"end",3)==0)
{
break;
}
}
}
int main()
{
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_t id[2];
int res=pthread_create(&id[0],NULL,fun,"thread1");//通過傳參數thread1創建thread1線程
assert(res==0);
res=pthread_create(&id[1],NULL,fun,"thread2");
assert(res==0);
while(1)
{
printf("input:");
fgets(buff,127,stdin);
if(strncmp(buff,"end",3)==0)//喚醒在條件變量cond上等待的所有線程
{
pthread_mutex_lock(&mutex);
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);
break;
}
else//喚醒滿足條件的一個線程
{
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
sleep(1);
}
pthread_join(id[0],NULL);
pthread_join(id[1],NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
}
可以看到如果輸入數據,會喚起任意一個線程,進行數據的輸出;如果輸入end,會喚起所有線程。