互斥锁、条件变量、自旋锁、读写锁
一、互斥鎖
機制:一次只能一個線程擁有互斥鎖,其他線程只有等待。
互斥鎖是在搶鎖失敗的情況下主動放棄CPU,進入睡眠狀態直到鎖的狀態改變時再喚醒,而操作系統負責線程調度,為了實現鎖的狀態發生改變時能喚醒阻塞的線程或者進程,需要把鎖交給操作系統管理,所以互斥鎖在加鎖操作時涉及上下文的切換。
互斥鎖實際的效率還是可以讓人接受的,加鎖的時間大概100ns左右,而實際上互斥鎖的一種可能的實現是先自旋一段時間,當自旋的時間超過閥值之后再將線程投入睡眠中,因此在并發運算中使用互斥鎖(每次占用鎖的時間很短)的效果可能不亞于使用自旋鎖。
互斥鎖屬于sleep-waiting類型的鎖。例如在一個雙核的機器上有兩個線程A和B,它們分別運行在core 0和core 1上。假設線程A想要通過pthread_mutex_lock操作去得到一個臨界區的鎖,而此時這個鎖正被線程B所持有,那么線程A就會被阻塞,此時會通過上下文切換將線程A置于等待隊列中,此時core 0就可以運行其他的任務(如線程C)。
互斥鎖存在優先喚醒的問題。
接口:
1、初始化鎖
int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr);參數1:鎖的地址
參數2:設置鎖的屬性,NULL默認缺省屬性
互斥鎖的屬性在創建鎖的時候指定,當資源被某線程鎖住的時候,其它的線程在試圖加鎖時表現將不同。當前有四個值可供選擇:
1)PTHREAD_MUTEX_TIMED_NP,這是缺省值,也就是普通鎖。當一個線程加鎖以后,其余請求鎖的線程將形成一個等待隊列,并在解鎖后按優先級獲得鎖。這種鎖策略保證了資源分配的公平性。
2)PTHREAD_MUTEX_RECURSIVE_NP,嵌套鎖,允許同一個線程對同一個鎖成功獲得多次,并通過多次unlock解鎖。
3)PTHREAD_MUTEX_ERRORCHECK_NP,檢錯鎖,如果同一個線程請求同一個鎖,則返回EDEADLK,否則與PTHREAD_MUTEX_TIMED_NP類型動作相同。
4)PTHREAD_MUTEX_ADAPTIVE_NP,適應鎖,動作最簡單的鎖類型,等待解鎖后重新競爭。
2、阻塞加鎖
如果是鎖是空閑狀態,本線程將獲得這個鎖;如果鎖已經被占據,本線程將排隊等待,直到成功的獲取鎖。
3、非阻塞加鎖
如果是鎖是空閑狀態,本線程將獲得這個鎖,若鎖已經被占據時立即返回 EBUSY,不是掛起等待。
4、解鎖
線程把自己持有的鎖釋放。
4、銷毀鎖,釋放資源(此時鎖必需unlock狀態,否則返回EBUSY)
int pthread_mutex_destroy(pthread_mutex *mutex);用法1:使用互斥量(鎖)解決多線程搶占資源的問題
/* ************************************************************************ > File Name: pthread_mutex.cpp > Author: 想名字多費事 > 微信公眾號: 還沒弄好 > Created Time: 2021年04月24日 星期六 18時09分54秒 > Description: ************************************************************************/ //使用互斥量(鎖)解決多線程搶占資源的問題 #include<iostream> #include<cstdio> #include<cstdlib> #include<unistd.h> #include<pthread.h> #include<cstring> using namespace std;char buffer[1024];//全局共享的buffer//聲明互斥鎖方式1 pthread_mutex_t mutex;void *pthfun(void *arg){for(int ii=0;ii<2;ii++){cout<<time(0)<<":"<<(long)arg<<">>lock..."<<endl;//第一步,加鎖pthread_mutex_lock(&mutex);cout<<time(0)<<":"<<(long)arg<<">>lock ok! 正在處理數據..."<<endl;//第二步,操作共享的全局變量sprintf(buffer,"%d:%ld,%d",time(0),pthread_self(),ii);sleep(5);//第三步,解鎖pthread_mutex_unlock(&mutex);cout<<time(0)<<":"<<(long)arg<<">>unlock...ok!"<<endl;usleep(100);//互斥鎖存在優先喚醒的問題} }int main() { //初始化鎖pthread_mutex_init(&mutex,NULL);//NULL表示當一個線程加鎖以后,其余請求鎖的線程將形成一個等待隊列,并在解鎖后按優先級獲得鎖//創建兩個線程pthread_t pthid1,pthid2;pthread_create(&pthid1,NULL,pthfun,(void*)1);pthread_create(&pthid2,NULL,pthfun,(void*)2);//等待線程退出pthread_join(pthid1,NULL);pthread_join(pthid2,NULL);//銷毀鎖pthread_mutex_destroy(&mutex);return 0; }運行結果
1619265078:2>>lock... 1619265078:2>>lock ok! 正在處理數據... 1619265078:1>>lock... 1619265083:2>>unlock...ok! 1619265083:1>>lock ok! 正在處理數據... 1619265083:2>>lock... 1619265088:1>>unlock...ok! 1619265088:2>>lock ok! 正在處理數據... 1619265088:1>>lock... 1619265093:2>>unlock...ok! 1619265093:1>>lock ok! 正在處理數據... 1619265098:1>>unlock...ok!用法2:使用互斥量(鎖)實現數據庫連接池的服務端程序
(此程序涉及數據庫環境設置和部分框架的加入,不一定會運行成功,感興趣可到github里面下載完整代碼https://github.com/GpsLypy)
客戶端
#include "../_freecplus.h"int main(int argc,char *argv[]){printf("pid=%d\n",getpid());CTcpClient TcpClient; // 創建客戶端的對象。if (TcpClient.ConnectToServer("172.21.0.3",5858)==false) // 向服務端發起連接請求。{printf("TcpClient.ConnectToServer(\"172.21.0.3\",5858) failed.\n"); return -1;}char strbuffer[1024]; // 存放數據的緩沖區。for (int ii=0;ii<50;ii++) // 利用循環,與服務端進行5次交互。{memset(strbuffer,0,sizeof(strbuffer));snprintf(strbuffer,50,"(%d)這是第%d個超級女生,編號%03d。",getpid(),ii+1,ii+1);printf("發送:%s\n",strbuffer);if (TcpClient.Write(strbuffer)==false) break; // 向服務端發送請求報文。memset(strbuffer,0,sizeof(strbuffer));if (TcpClient.Read(strbuffer,20)==false) break; // 接收服務端的回應報文。 printf("接收:%s\n",strbuffer);sleep(5);}// 程序直接退出,析構函數會釋放資源。 }二、條件變量
互斥鎖一個明顯的缺點是只有兩種狀態:鎖定和非鎖定。
而條件變量通過允許線程阻塞和等待另一個線程發送信號的方法彌補了互斥鎖的不足,他常和互斥鎖一起使用,以免出現競爭條件。當條件不滿足時,線程往往解開相應的互斥鎖并阻塞線程然后等待條件發生變化。一旦其他的某個線程改變了條件變量,他將通知相應的條件變量喚醒一個或多個正被此條件變量阻塞的線程。總的來說互斥鎖是線程間互斥的機制,條件變量則是同步機制。
相關接口:
1、初始化條件變量
參數2:條件變量屬性,通常為默認值,傳NULL即可
也可以使用靜態初始化的方法,初始化條件變量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;2、阻塞等待一個條件變量
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);函數作用:
1、釋放互斥鎖。
2、等待條件。
3、條件被觸發。
4、給互斥鎖加鎖
3、4步為原子操作
3、限時等待一個條件變量
int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);參3: 參看man sem_timedwait函數,查看struct timespec結構體。
struct timespec {
time_t tv_sec; /* seconds / 秒
long tv_nsec; / nanosecondes*/ 納秒
}
形參abstime:絕對時間。
如:time(NULL)返回的就是絕對時間。而alarm(1)是相對時間,相對當前時間定時1秒鐘。
struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t); 只能定時到 1970年1月1日 00:00:01秒(早已經過去)
正確用法:
time_t cur = time(NULL); 獲取當前時間。
struct timespec t; 定義timespec 結構體變量t
t.tv_sec = cur+1; 定時1秒
pthread_cond_timedwait (&cond, &mutex, &t);
4、喚醒至少一個阻塞在條件變量上的線程
int pthread_cond_signal(pthread_cond_t *cond);5、喚醒全部阻塞在條件變量上的線程
int pthread_cond_broadcast(pthread_cond_t *cond);6、銷毀一個條件變量
int pthread_cond_destroy(pthread_cond_t *cond);1、條件變量與互斥鎖結合
#include<iostream> #include<unistd.h> #include<pthread.h> #include<signal.h> using namespace std;//聲明互斥鎖和條件變量pthread_mutex_t mutex;pthread_cond_t cond;void *thread1(void* arg){while(1){pthread_mutex_lock(&mutex);cout<<"線程一開始等待條件"<<endl;pthread_cond_wait(&cond,&mutex);cout<<"線程一被喚醒"<<endl;pthread_mutex_unlock(&mutex);} }void *thread2(void* arg){while(1){pthread_mutex_lock(&mutex);cout<<"線程二開始等待條件"<<endl;pthread_cond_wait(&cond,&mutex);cout<<"線程二被喚醒"<<endl;pthread_mutex_unlock(&mutex);} }void func(int sig){pthread_cond_signal(&cond);//pthread_cond_broadcast(&cond);//廣播 } int main() {signal(15,func);//初始化鎖pthread_mutex_init(&mutex,NULL);//NULL表示當一個線程加鎖以后,其余請求鎖的線程將形成一個等待隊列,并在解鎖后按優先級獲得鎖//初始化條件變量pthread_cond_init(&cond,NULL);//創建兩個線程pthread_t pthid1,pthid2;pthread_create(&pthid1,NULL,thread1,NULL);pthread_create(&pthid2,NULL,thread2,NULL);//等待線程退出pthread_join(pthid1,NULL);pthread_join(pthid2,NULL);//銷毀鎖pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);return 0; }2、條件變量與互斥鎖實現(生產-消費者模型)高速緩存
1、
運行結果:
[root@localhost mutex]# g++ -o cache cache.cpp -lpthread [root@localhost mutex]# ./cache phid=140295864997632,mesgid=1 phid=140295856604928,mesgid=2 phid=140295848212224,mesgid=3 phid=140295856604928,mesgid=4 phid=140295864997632,mesgid=52、
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h>//節點結構體,鏈表作為共享數據(數據緩存),需被互斥量保護/ struct ListNode{int num; //數據區struct ListNode *next; //鏈表區 };struct ListNode *head = NULL;//頭指針 struct ListNode *node = NULL; //節點指針 //利用宏定義的方式初始化全局的互斥鎖和條件變量 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;void *producter(void *arg){while (1) {//node =(struct ListNode*)malloc(sizeof(struct ListNode));//node->num = rand() % 400 + 1;//printf("---producted---%d\n", node->num);pthread_mutex_lock(&mutex);//訪問共享區域必須加鎖//node =(struct ListNode*)malloc(sizeof(struct ListNode));node->num = rand() % 400 + 1;printf("---producted---%d\n", node->num);node->next = head;head = node;//pthread_mutex_unlock(&mutex);pthread_cond_signal(&has_product);//通知消費者來消費sleep(rand() % 3);}return NULL; }void *consumer(void *arg){while (1){pthread_mutex_lock(&mutex);//訪問共享區域必須加鎖///while (head == NULL){//如果共享區域沒有數據,則解鎖并等待條件變量pthread_cond_wait(&has_product, &mutex);}node = head;head = node->next;printf("------------------consumer--%d\n", mp->num);free(node); //釋放被刪除的節點內存node = NULL;//并將刪除的節點指針指向NULL,防止野指針///pthread_mutex_unlock(&mutex);//printf("------------------consumer--%d\n", mp->num);//free(node); //釋放被刪除的節點內存//node = NULL;//并將刪除的節點指針指向NULL,防止野指針sleep(rand() % 3);}return NULL; }int main(void){pthread_t ptid, ctid;//創建生產者和消費者線程pthread_create(&ptid, NULL, producter, NULL);pthread_create(&ctid, NULL, consumer, NULL);//主線程回收兩個子線程pthread_join(ptid, NULL);pthread_join(ctid, NULL);return 0; }運行結果:
[root@localhost mutex]# g++ -o cache1 cache1.cpp -lpthread [root@localhost mutex]# ./cache1 ---producted---184 ------------------consumer--184 ---producted---116 ------------------consumer--116 ---producted---187 ---producted---250 ------------------consumer--250 ---producted---28 ------------------consumer--28 ---producted---164 ------------------consumer--164 ------------------consumer--187 ---producted---373 ------------------consumer--373 ---producted---169 ---producted---30 ------------------consumer--30 ---producted---63條件變量的優點:
相較于mutex而言,條件變量可以減少競爭。
如直接使用mutex,除了生產者、消費者之間要競爭互斥量以外,消費者之間也需要競爭互斥量,但如果匯聚(鏈表)中沒有數據,消費者之間競爭互斥鎖是無意義的。有了條件變量機制以后,只有生產者完成生產,才會引起消費者之間的競爭。提高了程序效率。
三、自旋鎖
如果進線程無法取得鎖,進線程不會立刻放棄CPU時間片,而是一直循環嘗試獲取鎖,直到獲取為止。
如果別的線程長時期占有鎖,那么自旋就是在浪費CPU做無用功,但是自旋鎖一般應用于加鎖時間很短的場景,這個時候效率比較高。
自旋鎖屬于busy-waiting類型的鎖,如果線程A是使用pthread_spin_lock操作去請求鎖,如果自旋鎖已經被線程B所持有,那么線程A就會一直在core 0上進行忙等待并不停的進行鎖請求,檢查該自旋鎖是否已經被線程B釋放,直到得到這個鎖為止。
因為自旋鎖不會引起調用者睡眠,所以自旋鎖的效率遠高于互斥鎖。
雖然它的效率比互斥鎖高,但是它也有些不足之處:
自旋鎖一直占用CPU,在未獲得鎖的情況下,一直進行自旋,所以占用著CPU,如果不能在很短的時間內獲得鎖,無疑會使CPU效率降低。
在用自旋鎖時有可能造成死鎖,當遞歸調用時有可能造成死鎖。
自旋鎖只有在內核可搶占式或SMP的情況下才真正需要,在單CPU且不可搶占式的內核下,自旋鎖的操作為空操作。自旋鎖適用于鎖使用者保持鎖時間比較短的情況下。
相關API:
1、初始化鎖
int pthread_spin_init(pthread_spinlock_t* lock,int pshared);2、阻塞加鎖
int pthread_spin_lock(pthread_spinlock_t* lock);3、非阻塞加鎖
int pthread_spin_trylock(pthread_spinlock_t* lock);4、解鎖
int pthread_spin_unlock(pthread_spinlock_t* lock);5、銷毀鎖
int pthread_spin_destorya(pthread_spinlock_t* lock); #include<iostream> #include<cstdio> #include<cstdlib> #include<unistd.h> #include<pthread.h> #include<cstring> using namespace std;char buffer[1024];//全局共享的bufferpthread_spinlock_t spin;//聲明自旋鎖void *pthfun(void *arg){for(int ii=0;ii<2;ii++){cout<<time(0)<<":"<<(long)arg<<">>lock..."<<endl;//第一步,加鎖pthread_spin_lock(&spin);cout<<time(0)<<":"<<(long)arg<<">>lock ok! 正在處理數據..."<<endl;//第二步,操作共享的全局變量sprintf(buffer,"%d:%ld,%d",time(0),pthread_self(),ii);sleep(5);//第三步,解鎖pthread_spin_unlock(&spin);cout<<time(0)<<":"<<(long)arg<<">>unlock...ok!"<<endl;usleep(100);} }int main() { //初始化鎖pthread_spin_init(&spin,PTHREAD_PROCESS_PRIVATE);//當一個線程加鎖以后,其余請求鎖的線程將形成一個等待隊列,并在解鎖后按優先級獲得鎖//創建兩個線程pthread_t pthid1,pthid2;pthread_create(&pthid1,NULL,pthfun,(void*)1);pthread_create(&pthid2,NULL,pthfun,(void*)2);//等待線程退出pthread_join(pthid1,NULL);pthread_join(pthid2,NULL);//銷毀鎖pthread_spin_destroy(&spin);return 0; }四、讀寫鎖
多個讀者可以同時進行讀
寫者必須互斥(只允許一個寫者寫,也不能讀者寫者同時進行)
寫者優先于讀者(一旦有寫者,則后續讀者必須等待,喚醒時優先考慮寫者)
相關API:
1、初始化讀寫鎖
int pthread_rwlock_init(pthread);2、申請讀鎖
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);3、申請寫鎖
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);4、嘗試申請寫鎖,若有線程持有讀鎖或寫鎖,返回失敗,否則成功
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);5、解鎖
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);6、銷毀讀寫鎖
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);總結
以上是生活随笔為你收集整理的互斥锁、条件变量、自旋锁、读写锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux多线程简介
- 下一篇: linux信号量简介