三種專門用於線程同步的機制:POSIX信號量,互斥量和條件變數.
在Linux上信號量API有兩組,一組是System V IPC信號量,即PV操作,另外就是POSIX信號量,POSIX信號量的名字都是以sem_開頭.
phshared參數指定信號量的類型,若其值為0,就表示這個信號量是當前進程的局部信號量,否則該信號量可以在多個進程之間共享.value值指定信號量的初始值,一般與下面的sem_wait函數相對應.
其中比較重要的函數sem_wait函數會以原子操作的方式將信號量的值減一,如果信號量的值為零,則sem_wait將會阻塞,信號量的值可以在sem_init函數中的value初始化;sem_trywait函數是sem_wait的非阻塞版本;sem_post函數將以原子的操作對信號量加一,當信號量的值大於0時,其他正在調用sem_wait等待信號量的線程將被喚醒.
這些函數成功時返回0,失敗則返回-1並設置errno.
生產者消費者模型:
生產者對應一個信號量:sem_t procer;
消費者對應一個信號量:sem_t customer;
sem_init(&procer,2)----生產者擁有資源,可以工作;
sem_init(&customer,0)----消費者沒有資源,阻塞;
在訪問公共資源前對互斥量設置(加鎖),確保同一時間只有一個線程訪問數據,在訪問完成後再釋放(解鎖)互斥量.
互斥鎖的運行方式:串列訪問共享資源;
信號量的運行方式:並行訪問共享資源;
互斥量用pthread_mutex_t數據類型表示,在使用互斥量之前,必須使用pthread_mutex_init函數對它進行初始化,注意,使用完畢後需調用pthread_mutex_destroy.
pthread_mutex_init用於初始化互斥鎖,mutexattr用於指定互斥鎖的屬性,若為NULL,則表示默認屬性。除了用這個函數初始化互斥所外,還可以用如下方式初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER。
pthread_mutex_destroy用於銷毀互斥鎖,以釋放佔用的內核資源,銷毀一個已經加鎖的互斥鎖將導致不可預期的後果。
pthread_mutex_lock以原子操作給一個互斥鎖加鎖。如果目標互斥鎖已經被加鎖,則pthread_mutex_lock則被阻塞,直到該互斥鎖佔有者把它給解鎖.
pthread_mutex_trylock和pthread_mutex_lock類似,不過它始終立即返回,而不論被操作的互斥鎖是否加鎖,是pthread_mutex_lock的非阻塞版本.當目標互斥鎖未被加鎖時,pthread_mutex_trylock進行加鎖操作;否則將返回EBUSY錯誤碼。注意:這里討論的pthread_mutex_lock和pthread_mutex_trylock是針對普通鎖而言的,對於其他類型的鎖,這兩個加鎖函數會有不同的行為.
pthread_mutex_unlock以原子操作方式給一個互斥鎖進行解鎖操作。如果此時有其他線程正在等待這個互斥鎖,則這些線程中的一個將獲得它.
三個列印機輪流列印:
輸出結果:
如果說互斥鎖是用於同步線程對共享數據的訪問的話,那麼條件變數就是用於在線程之間同步共享數據的值.條件變數提供了一種線程之間通信的機制:當某個共享數據達到某個值時,喚醒等待這個共享數據的線程.
條件變數會在條件不滿足的情況下阻塞線程.且條件變數和互斥量一起使用,允許線程以無競爭的方式等待特定的條件發生.
其中pthread_cond_broadcast函數以廣播的形式喚醒所有等待目標條件變數的線程,pthread_cond_signal函數用於喚醒一個等待目標條件變數線程.但有時候我們可能需要喚醒一個固定的線程,可以通過間接的方法實現:定義一個能夠唯一標識目標線程的全局變數,在喚醒等待條件變數的線程前先設置該變數為目標線程,然後採用廣播的方式喚醒所有等待的線程,這些線程被喚醒之後都檢查該變數以判斷是否是自己.
採用條件變數+互斥鎖實現生產者消費者模型:
運行結果:
阻塞隊列+生產者消費者
運行結果:
2. linux的線程同步方式有哪些
三種同步方式:1、互斥鎖(mutex)、2、條件同步(cond)、3、信號量(semphore).
如果還想深入:可以參考http://blog.csdn.net/zsf8701/article/details/7844316。
3. Linux進程間通信(互斥鎖、條件變數、讀寫鎖、文件鎖、信號燈)
為了能夠有效的控制多個進程之間的溝通過程,保證溝通過程的有序和和諧,OS必須提供一定的同步機制保證進程之間不會自說自話而是有效的協同工作。比如在 共享內存的通信方式中,兩個或者多個進程都要對共享的內存進行數據寫入,那麼怎麼才能保證一個進程在寫入的過程中不被其它的進程打斷,保證數據的完整性 呢?又怎麼保證讀取進程在讀取數據的過程中數據不會變動,保證讀取出的數據是完整有效的呢?
常用的同步方式有: 互斥鎖、條件變數、讀寫鎖、記錄鎖(文件鎖)和信號燈.
互斥鎖:
顧名思義,鎖是用來鎖住某種東西的,鎖住之後只有有鑰匙的人才能對鎖住的東西擁有控制權(把鎖砸了,把東西偷走的小偷不在我們的討論范圍了)。所謂互斥, 從字面上理解就是互相排斥。因此互斥鎖從字面上理解就是一點進程擁有了這個鎖,它將排斥其它所有的進程訪問被鎖住的東西,其它的進程如果需要鎖就只能等待,等待擁有鎖的進程把鎖打開後才能繼續運行。 在實現中,鎖並不是與某個具體的變數進行關聯,它本身是一個獨立的對象。進(線)程在有需要的時候獲得此對象,用完不需要時就釋放掉。
互斥鎖的主要特點是互斥鎖的釋放必須由上鎖的進(線)程釋放,如果擁有鎖的進(線)程不釋放,那麼其它的進(線)程永遠也沒有機會獲得所需要的互斥鎖。
互斥鎖主要用於線程之間的同步。
條件變數:
上文中提到,對於互斥鎖而言,如果擁有鎖的進(線)程不釋放鎖,其它進(線)程永遠沒機會獲得鎖,也就永遠沒有機會繼續執行後續的邏輯。在實際環境下,一 個線程A需要改變一個共享變數X的值,為了保證在修改的過程中X不會被其它的線程修改,線程A必須首先獲得對X的鎖。現在假如A已經獲得鎖了,由於業務邏 輯的需要,只有當X的值小於0時,線程A才能執行後續的邏輯,於是線程A必須把互斥鎖釋放掉,然後繼續「忙等」。如下面的偽代碼所示:
1.// get x lock
2.while(x
4. Linux信號量
信號量是包含一個非負整數型的變數,並且帶有兩個原子操作wait和signal。Wait還可以被稱為down、P或lock,signal還可以被稱為up、V、unlock或post。在UNIX的API中(POSIX標准)用的是wait和post。
對於wait操作,如果信號量的非負整形變數S大於0,wait就將其減1,如果S等於0,wait就將調用線程阻塞;對於post操作,如果有線程在信號量上阻塞(此時S等於0),post就會解除對某個等待線程的阻塞,使其從wait中返回,如果沒有線程阻塞在信號量上,post就將S加1.
由此可見,S可以被理解為一種資源的數量,信號量即是通過控制這種資源的分配來實現互斥和同步的。如果把S設為1,那麼信號量即可使多線程並發運行。另外,信號量不僅允許使用者申請和釋放資源,而且還允許使用者創造資源,這就賦予了信號量實現同步的功能。可見信號量的功能要比互斥量豐富許多。
POSIX信號量是一個sem_t類型的變數,但POSIX有兩種信號量的實現機制: 無名信號量 和 命名信號量 。無名信號量只可以在共享內存的情況下,比如實現進程中各個線程之間的互斥和同步,因此無名信號量也被稱作基於內存的信號量;命名信號量通常用於不共享內存的情況下,比如進程間通信。
同時,在創建信號量時,根據信號量取值的不同,POSIX信號量還可以分為:
下面是POSIX信號量函數介面:
信號量的函數都以sem_開頭,線程中使用的基本信號函數有4個,他們都聲明在頭文件semaphore.h中,該頭文件定義了用於信號量操作的sem_t類型:
【sem_init函數】:
該函數用於創建信號量,原型如下:
該函數初始化由sem指向的信號對象,設置它的共享選項,並給它一個初始的整數值。pshared控制信號量的類型,如果其值為0,就表示信號量是當前進程的局部信號量,否則信號量就可以在多個進程間共享,value為sem的初始值。
該函數調用成功返回0,失敗返回-1。
【sem_destroy函數】:
該函數用於對用完的信號量進行清理,其原型如下:
成功返回0,失敗返回-1。
【sem_wait函數】:
該函數用於以原子操作的方式將信號量的值減1。原子操作就是,如果兩個線程企圖同時給一個信號量加1或減1,它們之間不會互相干擾。其原型如下:
sem指向的對象是sem_init調用初始化的信號量。調用成功返回0,失敗返回-1。
sem_trywait()則是sem_wait()的非阻塞版本,當條件不滿足時(信號量為0時),該函數直接返回EAGAIN錯誤而不會阻塞等待。
sem_timedwait()功能與sem_wait()類似,只是在指定的abs_timeout時間內等待,超過時間則直接返回ETIMEDOUT錯誤。
【sem_post函數】:
該函數用於以原子操作的方式將信號量的值加1,其原型如下:
與sem_wait一樣,sem指向的對象是由sem_init調用初始化的信號量。調用成功時返回0,失敗返回-1。
【sem_getvalue函數】:
該函數返回當前信號量的值,通過restrict輸出參數返回。如果當前信號量已經上鎖(即同步對象不可用),那麼返回值為0,或為負數,其絕對值就是等待該信號量解鎖的線程數。
【實例1】:
【實例2】:
之所以稱為命名信號量,是因為它有一個名字、一個用戶ID、一個組ID和許可權。這些是提供給不共享內存的那些進程使用命名信號量的介面。命名信號量的名字是一個遵守路徑名構造規則的字元串。
【sem_open函數】:
該函數用於創建或打開一個命名信號量,其原型如下:
參數name是一個標識信號量的字元串。參數oflag用來確定是創建信號量還是連接已有的信號量。
oflag的參數可以為0,O_CREAT或O_EXCL:如果為0,表示打開一個已存在的信號量;如果為O_CREAT,表示如果信號量不存在就創建一個信號量,如果存在則打開被返回,此時mode和value都需要指定;如果為O_CREAT|O_EXCL,表示如果信號量存在則返回錯誤。
mode參數用於創建信號量時指定信號量的許可權位,和open函數一樣,包括:S_IRUSR、S_IWUSR、S_IRGRP、S_IWGRP、S_IROTH、S_IWOTH。
value表示創建信號量時,信號量的初始值。
【sem_close函數】:
該函數用於關閉命名信號量:
單個程序可以用sem_close函數關閉命名信號量,但是這樣做並不能將信號量從系統中刪除,因為命名信號量在單個程序執行之外是具有持久性的。當進程調用_exit、exit、exec或從main返回時,進程打開的命名信號量同樣會被關閉。
【sem_unlink函數】:
sem_unlink函數用於在所有進程關閉了命名信號量之後,將信號量從系統中刪除:
【信號量操作函數】:
與無名信號量一樣,操作信號量的函數如下:
命名信號量是隨內核持續的。當命名信號量創建後,即使當前沒有進程打開某個信號量,它的值依然保持,直到內核重新自舉或調用sem_unlink()刪除該信號量。
無名信號量的持續性要根據信號量在內存中的位置確定:
很多時候信號量、互斥量和條件變數都可以在某種應用中使用,那這三者的差異有哪些呢?下面列出了這三者之間的差異:
5. Linux C++多線程同步的四種方式
From : https://blog.csdn.net/qq_39382769/article/details/96075346
1.同一個線程內部,指令按照先後順序執行;但不同線程之間的指令很難說清楚是哪一個先執行,在並發情況下,指令執行的先後順序由內核決定。
如果運行的結果依賴於不同線程執行的先後的話,那麼就會形成競爭條件,在這樣的情況下,計算的結果很難預知,所以應該盡量避免競爭條件的形成。
2.最常見的解決競爭條件的方法是:將原先分離的兩個指令構成一個不可分割的原子操作,而其他任務不能插入到原子操作中!
3.對多線程來說,同步指的是在一定時間內只允許某一個線程訪問某個資源,而在此時間內,不允許其他線程訪問該資源!
互斥鎖
條件變數
讀寫鎖
信號量
一種特殊的全局變數,擁有lock和unlock兩種狀態。
unlock的互斥鎖可以由某個線程獲得,一旦獲得,這個互斥鎖會鎖上變成lock狀態,此後只有該線程由權力打開該鎖,其他線程想要獲得互斥鎖,必須得到互斥鎖再次被打開之後。
1.互斥鎖的初始化, 分為靜態初始化和動態初始化.
2.互斥鎖的相關屬性及分類
(1) attr表示互斥鎖的屬性;
(2) pshared表示互斥鎖的共享屬性,由兩種取值:
1)PTHREAD_PROCESS_PRIVATE:鎖只能用於一個進程內部的兩個線程進行互斥(默認情況)
2)PTHREAD_PROCESS_SHARED:鎖可用於兩個不同進程中的線程進行互斥,使用時還需要在進程共享內存中分配互斥鎖,然後為該互斥鎖指定屬性就可以了。
互斥鎖存在缺點:
(1)某個線程正在等待共享數據內某個條件出現。
(2)重復對數據對象加鎖和解鎖(輪詢),但是這樣輪詢非常耗費時間和資源,而且效率非常低,所以互斥鎖不太適合這種情況。
當線程在等待滿足某些條件時,使線程進入睡眠狀態;一旦條件滿足,就換線因等待滿足特定條件而睡眠的線程。
程序的效率無疑會大大提高。
1)創建
靜態方式:pthread_cond_t cond PTHREAD_COND_INITIALIZER
動態方式:int pthread_cond_init(&cond,NULL)
Linux thread 實現的條件變數不支持屬性,所以NULL(cond_attr參數)
2)注銷
int pthread_cond_destory(&cond)
只有沒有線程在該條件變數上,該條件變數才能注銷,否則返回EBUSY
因為Linux實現的條件變數沒有分配什麼資源,所以注銷動作只包括檢查是否有等待線程!(請參考條件變數的底層實現)
3)等待
條件等待:int pthread_cond_wait(&cond,&mutex)
計時等待:int pthread_cond_timewait(&cond,&mutex,time)
1.其中計時等待如果在給定時刻前條件沒有被滿足,則返回ETIMEOUT,結束等待
2.無論那種等待方式,都必須有一個互斥鎖配合,以防止多個線程同時請求pthread_cond_wait形成競爭條件!
3.在調用pthread_cond_wait前必須由本線程加鎖
4)激發
激發一個等待線程:pthread_cond_signal(&cond)
激發所有等待線程:pthread_cond_broadcast(&cond)
重要的是,pthread_cond_signal不會存在驚群效應,也就是是它最多給一個等待線程發信號,不會給所有線程發信號喚醒,然後要求他們自己去爭搶資源!
pthread_cond_broadcast() 喚醒所有正在pthread_cond_wait()的同一個條件變數的線程。注意:如果等待的多個現場不使用同一個鎖,被喚醒的多個線程執行是並發的。
pthread_cond_broadcast & pthread_cond_signal
1.讀寫鎖比互斥鎖更加具有適用性和並行性
2.讀寫鎖最適用於對數據結構的讀操作讀操作次數多餘寫操作次數的場合!
3.鎖處於讀模式時可以線程共享,而鎖處於寫模式時只能獨占,所以讀寫鎖又叫做共享-獨占鎖。
4.讀寫鎖有兩種策略:強讀同步和強寫同步
強讀同步:
總是給讀者更高的優先權,只要寫者沒有進行寫操作,讀者就可以獲得訪問許可權
強寫同步:
總是給寫者更高的優先權,讀者只能等到所有正在等待或者執行的寫者完成後才能進行讀
1)初始化的銷毀讀寫鎖
靜態初始化:pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER
動態初始化:int pthread_rwlock_init(rwlock,NULL),NULL代表讀寫鎖採用默認屬性
銷毀讀寫鎖:int pthread_rwlock_destory(rwlock)
在釋放某個讀寫鎖的資源之前,需要先通過pthread_rwlock_destory函數對讀寫鎖進行清理。釋放由pthread_rwlock_init函數分配的資源
如果你想要讀寫鎖使用非默認屬性,則attr不能為NULL,得給attr賦值
int pthread_rwlockattr_init(attr),給attr初始化
int pthread_rwlockattr_destory(attr),銷毀attr
2)以寫的方式獲取鎖,以讀的方式獲取鎖,釋放讀寫鎖
int pthread_rwlock_rdlock(rwlock),以讀的方式獲取鎖
int pthread_rwlock_wrlock(rwlock),以寫的方式獲取鎖
int pthread_rwlock_unlock(rwlock),釋放鎖
上面兩個獲取鎖的方式都是阻塞的函數,也就是說獲取不到鎖的話,調用線程不是立即返回,而是阻塞執行,在需要進行寫操作的時候,這種阻塞式獲取鎖的方式是非常不好的,你想一下,我需要進行寫操作,不但沒有獲取到鎖,我還一直在這里等待,大大拖累效率
所以我們應該採用非阻塞的方式獲取鎖:
int pthread_rwlock_tryrdlock(rwlock)
int pthread_rwlock_trywrlock(rwlock)
互斥鎖只允許一個線程進入臨界區,而信號量允許多個線程進入臨界區。
1)信號量初始化
int sem_init(&sem,pshared, v)
pshared為0,表示這個信號量是當前進程的局部信號量。
pshared為1,表示這個信號量可以在多個進程之間共享。
v為信號量的初始值。
返回值:
成功:0,失敗:-1
2)信號量值的加減
int sem_wait(&sem):以原子操作的方式將信號量的值減去1
int sem_post(&sem):以原子操作的方式將信號量的值加上1
3)對信號量進行清理
int sem_destory(&sem)