A. 如何對讀寫鎖進行處理
信號量強調的是線程(或進程)間的同步:「信號量用在多線程多任務同步的,一個線程完成了某一個動作就通過信號量告訴別的線程,別的線程再進行某些動作(大家都 在sem_wait的時候,就阻塞在那裡)。當信號量為單值信號量是,也可以完成一個資源的互斥訪問。
有名信號量:可以用於不同進程間或多線程間的互斥與同步
創建打開有名信號量
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
成功返回信號量指針;失敗返回SEM_FAILED,設置errnoname是文件路徑名,但不能寫成/tmp/a.sem這樣的形式,因為在linux下,sem都是在/dev/shm目錄下,可寫成"/mysem"或"mysem",創建出來的文件都 是"/dev/shm/sem.mysem",mode設置為0666,value設置為信號量的初始值.所需信號燈等已存在條件下指定O_CREAT|O_EXCL卻是個錯誤。
關閉信號量,進程終止時,會自動調用它
int sem_close(sem_t *sem);
成功返回0;失敗返回-1,設置errno
刪除信號量,立即刪除信號量名字,當其他進程都關閉它時,銷毀它
int sem_unlink(const char *name);
等待信號量,測試信號量的值,如果其值小於或等於0,那麼就等待(阻塞);一旦其值變為大於0就將它減1,並返回
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
成功返回0;失敗返回-1,設置errno
當信號量的值為0時,sem_trywait立即返回,設置errno為EAGAIN。如果被某個信號中斷,sem_wait會過早地返回,設置errno為EINTR
發出信號量,給它的值加1,然後喚醒正在等待該信號量的進程或線程
int sem_post(sem_t *sem);
成功返回0;失敗返回-1,不會改變它的值,設置errno,該函數是非同步信號安全的,可以在信號處理程序里調用它無名信號量,用於進程體內各線程間的互斥和同步,使用如下API(無名信號量,基於內存的信號量)
(1)、sem_init
功能:用於創建一個信號量,並初始化信號量的值。
頭文件:
函數原型: int sem_init (sem_t* sem, int pshared, unsigned int value);
函數傳入值: sem:信號量。pshared:決定信號量能否在幾個進程間共享。由於目前LINUX還沒有實現進程間共享信息量,所以這個值只能取0。
(2)其他函數。
int sem_wait (sem_t* sem);
int sem_trywait (sem_t* sem);
int sem_post (sem_t* sem);
int sem_getvalue (sem_t* sem);
int sem_destroy (sem_t* sem);
功能:sem_wait和sem_trywait相當於P操作,它們都能將信號量的值減一,兩者的區別在於若信號量的值小於零時,sem_wait將會阻塞進程,而sem_trywait則會立即返回。sem_post相當於V操作,它將信號量的值加一,同時發出喚醒的信號給等待的進程(或線程)。
sem_getvalue 得到信號量的值。
sem_destroy 摧毀信號量。
如果某個基於內存的信號燈是在不同進程間同步的,該信號燈必須存放在共享內存區中,這要只要該共享內存區存在,該信號燈就存在。
互斥鎖(又名互斥量)強調的是資源的訪問互斥:互斥鎖是用在多線程多任務互斥的,一個線程佔用了某一個資源,那麼別的線程就無法訪問,直到這個線程unlock,其他的線程才開始可以利用這個資源。比如對全局變數的訪問,有時要加鎖,操作完了,在解鎖。有的時候鎖和信號量會同時使用的」
也就是說,信號量不一定是鎖定某一個資源,而是流程上的概念,比如:有A,B兩個線程,B線程要等A線程完成某一任務以後再進行自己下面的步驟,這個任務並不一定是鎖定某一資源,還可以是進行一些計算或者數據處理之類。而線程互斥量則是「鎖住某一資源」的概念,在鎖定期間內,其他線程無法對被保護的數據進行操作。在有些情況下兩者可以互換。
在linux下, 線程的互斥量數據類型是pthread_mutex_t. 在使用前, 要對它進行初始化:
對於靜態分配的互斥量, 可以把它設置為PTHREAD_MUTEX_INITIALIZER, 或者調用pthread_mutex_init.
對於動態分配的互斥量, 在申請內存(malloc)之後, 通過pthread_mutex_init進行初始化, 並且在釋放內存(free)前需要調用pthread_mutex_destroy.
原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restric attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
頭文件:
返回值: 成功則返回0, 出錯則返回錯誤編號.
說明: 如果使用默認的屬性初始化互斥量, 只需把attr設為NULL. 其他值在以後講解.
首先說一下加鎖函數:
頭文件:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
返回值: 成功則返回0, 出錯則返回錯誤編號.
說 明: 具體說一下trylock函數, 這個函數是非阻塞調用模式, 也就是說, 如果互斥量沒被鎖住, trylock函數將把互斥量加鎖, 並獲得對共享資源的訪問許可權; 如果互斥量 被鎖住了, trylock函數將不會阻塞等待而直接返回EBUSY, 表示共享資源處於忙狀態.
再說一下解所函數:
頭文件:
原型: int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值: 成功則返回0, 出錯則返回錯誤編號.
條件變數常與互斥鎖同時使用,達到線程同步的目的:條件變數通過允許線程阻塞和等待另一個線程發送信號的方法彌補了互斥鎖的不足。在發 送信號時,如果沒有線程 等待在該條件變數上,那麼信號將丟失;而信號量有計數值,每次信號量post操作都會被記錄
互斥鎖必須是誰上鎖就由誰來解鎖,而信號量的wait和post操作不必由同一個線程執行。
2. 互斥鎖要麼被鎖住,要麼被解開,和二值信號量類似
3. sem_post是各種同步技巧中,唯一一個能在信號處理程序中安全調用的函數
4. 互斥鎖是為上鎖而優化的;條件變數是為等待而優化的; 信號量既可用於上鎖,也可用於等待,因此會有更多的開銷和更高的復雜性
5. 互斥鎖,條件變數都只用於同一個進程的各線程間,而信號量(有名信號量)可用於不同進程間的同步。當信號量用於進程間同步時,要求信號量建立在共享內存區。
6. 信號量有計數值,每次信號量post操作都會被記錄,而條件變數在發送信號時,如果沒有線程在等待該條件變數,那麼信號將丟失。
讀寫鎖
讀寫鎖與互斥量類似,不過讀寫鎖允許更高的並行性。互斥量要麼是鎖住狀態要麼是不加鎖狀態,而且一次只有一個線程可以對其加鎖。
讀寫鎖可以由三種狀態:讀模式下加鎖狀態、寫模式下加鎖狀態、不加鎖狀態。一次只有一個線程可以佔有寫模式的讀寫鎖,但是多個線程可以同時佔有讀模式的讀寫
鎖。
在讀寫鎖是寫加鎖狀態時,在這個鎖被解鎖之前,所有試圖對這個鎖加鎖的線程都會被阻塞。當讀寫鎖在讀加鎖狀態時,所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權,但是如果線程希望以寫模式對此鎖進行加鎖,它必須阻塞直到所有的線程釋放讀鎖。雖然讀寫鎖的實現各不相同,但當讀寫鎖處於讀模式鎖住狀態時,如果有另外的線程試圖以寫模式加鎖,讀寫鎖通常會阻塞隨後的讀模式鎖請求。這樣可以避免讀模式鎖長期佔用,而等待的寫模式鎖請求一直得不到滿足。
讀寫鎖非常適合於對數據結構讀的次數遠大於寫的情況。當讀寫鎖在寫模式下時,它所保護的數據結構就可以被安全地修改,因為當前只有一個線程可以在寫模式下擁 有這個鎖。當讀寫鎖在讀狀態下時,只要線程獲取了讀模式下的讀寫鎖,該鎖所保護的數據結構可以被多個獲得讀模式鎖的線程讀取。
讀寫鎖也叫做共享-獨占鎖,當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當他以寫模式鎖住時,它是以獨占模式鎖住的。
初始化和銷毀:
#include
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
成功則返回0, 出錯則返回錯誤編號.
同互斥量以上, 在釋放讀寫鎖佔用的內存之前, 需要先通過thread_rwlock_destroy對讀寫鎖進行清理工作, 釋放由init分配的資源.
讀和寫:
#include
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
成功則返回0, 出錯則返回錯誤編號.
這3個函數分別實現獲取讀鎖, 獲取寫鎖和釋放鎖的操作. 獲取鎖的兩個函數是阻塞操作, 同樣, 非阻塞的函數為:
#include
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
成功則返回0, 出錯則返回錯誤編號.
非阻塞的獲取鎖操作, 如果可以獲取則返回0, 否則返回錯誤的EBUSY.
雖然讀寫鎖提高了並行性,但是就速度而言並不比互斥量快.
可能這也是即使有讀寫鎖存在還會使用互斥量的原因,因為他在速度方面略勝一籌。這就需要我們在寫程序的時候綜合考慮速度和並行性並找到一個折中。
比如: 假設使用互斥量需要0.5秒,使用讀寫鎖需要0.8秒。在類似學生管理系統這類中,可能百分之九十的時間都是查詢操作,那麼假如現在突然來個個20個請求,如果使用的是互斥量,那麼最後的那個查詢請求被滿足需要10後。這樣,估計沒人能受得了。而使用讀寫鎖,應為 讀鎖能夠多次獲得。所以所有的20個請求,每個請求都能在1秒左右得到滿足。
也就是說,在一些寫操作比較多或是本身需要同步的地方並不多的程序中我們應該使用互斥量,而在讀操作遠大於寫操作的一些程序中我們應該使用讀寫鎖來進行同步
條件變數(condition)
條件變數與互斥量一起使用時,允許線程以無競爭的方式等待特定的條件發生。
條件本身是由互斥量保護的。線程在改變條件狀態前必須首先鎖住互斥量,其它線程在獲得互斥量之前不會察覺到這種改變,因此必須鎖定互斥量以後才能計算條件。
條件的檢測是在互斥鎖的保護下進行的。如果一個條件為假,一個線程自動阻塞,並釋放等待狀態改變的互斥鎖。如果另一個線程改變了條件,它發信號給關聯的條件
變數,喚醒一個或多個等待它的線程,重新獲得互斥鎖,重新評價條件。如果兩進程共享可讀寫的內存,條件變數可以被用來實現這兩進程間的線程同步。
1. 初始化:
條件變數採用的數據類型是pthread_cond_t, 在使用之前必須要進行初始化, 這包括兩種方式:
靜態: 可以把常量PTHREAD_COND_INITIALIZER給靜態分配的條件變數.
動態: pthread_cond_init函數, 是釋放動態條件變數的內存空間之前, 要用pthread_cond_destroy對其進行清理.
#include
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
成功則返回0, 出錯則返回錯誤編號.
注意:條件變數佔用的空間並未被釋放。
當pthread_cond_init的attr參數為NULL時, 會創建一個默認屬性的條件變數; 非默認情況以後討論.
2. 等待條件:
#include
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restric mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);
成功則返回0, 出錯則返回錯誤編號.
這兩個函數分別是阻塞等待和超時等待.
等待條件函數等待條件變為真, 傳遞給pthread_cond_wait的互斥量對條件進行保護, 調用者把鎖住的互斥量傳遞給函數. 函數把調用線程放到等待條件的線程列表上, 然後對互斥量解鎖, 這兩個操作是原子的. 這樣 便關閉了條件檢查和線程進入休眠狀態等待條件改變這兩個操作之間的時間通道, 這樣線程就不會錯過條件的任何變化.
當pthread_cond_wait返回時, 互斥量再次被鎖住.
pthread_cond_wait函數的返回並不意味著條件的值一定發生了變化,必須重新檢查條件的值。
pthread_cond_wait函數返回時,相應的互斥鎖將被當前線程鎖定,即使是函數出錯返回。
阻塞在條件變數上的線程被喚醒以後,直到pthread_cond_wait()函數返回之前條件的值都有可能發生變化。所以函數返回以後,在鎖定相應的互斥鎖之前,必須重新測試條 件值。最好的測試方法是循環調用pthread_cond_wait函數,並把滿足條件的表達式置為循環的終止條件。如:
pthread_mutex_lock();
while (condition_is_false)
pthread_cond_wait();
pthread_mutex_unlock();
阻塞在同一個條件變數上的不同線程被釋放的次序是不一定的。
注意:pthread_cond_wait()函數是退出點,如果在調用這個函數時,已有一個掛起的退出請求,且線程允許退出,這個線程將被終止並開始執行善後處理函數,而這時和條 件變數相關的互斥鎖仍將處在鎖定狀態。
pthread_cond_timedwait函數到了一定的時間,即使條件未發生也會解除阻塞。這個時間由參數abstime指定。函數返回時,相應的互斥鎖往往是鎖定的,即使是函數出錯返回。
注意:pthread_cond_timedwait函數也是退出點。
超時時間參數是指一天中的某個時刻。使用舉例:
pthread_timestruc_t to;
to.tv_sec = time(NULL) + TIMEOUT;
to.tv_nsec = 0;
超時返回的錯誤碼是ETIMEDOUT。
3. 通知條件:
#include
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
成功則返回0, 出錯則返回錯誤編號.
這兩個函數用於通知線程條件已經滿足. 調用這兩個函數, 也稱向線程或條件發送信號. 必須注意, 一定要在改變條件狀態以後再給線程發送信號.
B. linux 多線程環境下的幾種鎖機制
NO1
互斥量(Mutex)
互斥量是實現最簡單的鎖類型,因此有一些教科書一般以互斥量為例對鎖原語進行描述。互斥量的釋放並不僅僅依賴於釋放操作,還可以引入一個定皮返時器屬性。如果在釋放操作執行前發生定時器超時,則互斥量也會釋放代碼塊或共享存儲區供其他線程訪問。當有異常發生時,可使用try-finally語句來確保互斥量被釋放。定時器狀態或try-finally語句的使用可以避免產生死鎖。
遞歸鎖(Recursive
Lock)
遞歸鎖是指可以被當前持有該鎖的線程重復獲取,而不會導致該線程產生死鎖的鎖類型。對遞歸鎖而言,只有在當前持有線程的獲取鎖操作都有一個釋放操作與之對應時,其他線程才可以獲取該鎖。因此,在使用遞歸鎖時,必須要用足夠的釋放鎖操作來平衡獲取鎖操作,實現這一目標的最佳方式是在單入口單出口代碼塊的兩頭一一對應地使用獲取、釋放操作,做法和在普通鎖中一樣。遞歸鎖在遞歸函數中最有用。但是,總的來說,遞歸鎖比非遞歸鎖速度要慢。需要注意的是:調用線程獲得幾次遞歸鎖必須釋放幾次遞歸鎖。
以下為一個遞歸鎖的示例:
[cpp] view plain
Recursive_Lock L
void recursiveFunction (int count) {
L->acquire()
if (count > 0) {
count = count - 1;
recursiveFunction(count);
}
L->release();
}
讀寫鎖(Read-Write
lock) 讀寫鎖又稱為共享獨占鎖(shared-exclusive
lock)、多讀單寫鎖(multiple-read/single-write lock)或者非互斥信號量(non-mutual
exclusion
semaphore)。讀寫鎖允許多個線程同時進行讀訪問,但是在某一時刻卻最多隻能由一個線程執行寫操作。對於多個線程需要同時讀共享數據卻並不一定進行寫操作的應用來說,讀寫鎖是一種高效的同步機制。對於較長的共享數據,只為其設置一個讀寫鎖會導致較長的訪問時間,最好將其劃分為多個小段並設置多個讀寫鎖以進行同步。
這個讀寫鎖我們在學習資料庫的時候應該很熟悉的喲!
旋轉鎖(Spin
Lock)
旋轉鎖是一種非阻塞鎖,由某個線程獨占。腔指采伍握配用旋轉鎖時,等待線程並不靜態地阻塞在同步點,而是必須「旋轉」,不斷嘗試直到最終獲得該鎖。旋轉鎖多用於多處理器系統中。這是因為,如果在單核處理器中採用旋轉鎖,當一個線程正在「旋轉」時,將沒有執行資源可供另一釋放鎖的線程使用。旋轉鎖適合於任何鎖持有時間少於將一個線程阻塞和喚醒所需時間的場合。線程式控制制的變更,包括線程上下文的切換和線程數據結構的更新,可能比旋轉鎖需要更多的指令周期。旋轉鎖的持有時間應該限制在線程上下文切換時間的50%到100%之間(Kleiman,1996年)。在線程調用其他子系統時,線程不應持有旋轉鎖。對旋轉鎖的不當使用可能會導致線程餓死,因此需謹慎使用這種鎖機制。旋轉鎖導致的餓死問題可使用排隊技術來解決,即每個等待線程按照先進先出的順序或者隊列結構在一個獨立的局部標識上進行旋轉。
學習了這些,果然受益匪淺,在今後的coding中,我得挨個試試咯。
C. linux 互斥鎖和讀寫鎖的區別與聯系
信號量與互斥鎖之間的區別:
1. 互斥量用於線程的互斥,信號量用於線程的同步。
這是互斥量和信號量的根本區別,也就是互斥和同步之間的區別。
互斥:是指某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的。
同步:是指在互斥的基礎上(大多數情況),通過其它機制實現訪問者對資源的有序訪問。在大多數情況下,同步已經實現了互斥,特別是所有寫入資源的情況必定是互斥的。少數情況是指可以允許多個訪問者同時訪問資源
2. 互斥量值只能為0/1,信號量值可以為非負整數。
也就是說,一個互斥量只能用於一個資源的互斥訪問,它不能實現多個資源的多線程互斥問題。信號量可以實現多個同類資源的多線程互斥和同步。當信號量為單值信號量是,也可以完成一個資源的互斥訪問。
3. 互斥量的加鎖和解鎖必須由同一線程分別對應使用,信號量可以由一個線程釋放,另一個線程得到。
D. linux有沒有多進程間的讀寫鎖
Linux共享內存可以不用加鎖,不過需要一種機制來標記共享內存的讀寫狀態;也就是說要讓兩個進程知道:1)負責寫入的進程,必須知道當前共享內存是否可以寫入,上一次的寫入內容是否有被負責讀取的進程讀走;2)負責讀取的進程,必須知道當前共享內存是否需要讀取,防止重復讀取。一般的這種標記機制是通過以下方式來簡單實現:1)通過讀寫鎖來控制;2)共享內存上設置一個地方,專門存放當前共享內存的讀寫狀態;
E. linux rcu鎖問題怎麼查
眾所周知,為了保護共享數據,需要一些同步機制,如自旋鎖(spinlock),讀寫鎖(rwlock),它們使用起來非常簡單,而且是一種很有效的同步機制,在UNIX系統和Linux系統中得到了廣泛的使用。但是隨著計算機硬體的快速發展,獲得這種鎖的開銷相對於CPU的速度在成倍地增加,原因很簡單,CPU的速度與訪問內存的速度差距越來越大,而這種鎖使用了原子操作指令,它需要原子地訪問內存,也就說獲得鎖的開銷與訪存速度相關,另外在大部分非x86架構上獲取鎖使用了內存柵(Memory Barrier),這會導致處理器流水線停滯或刷新,因此它的開銷相對於CPU速度而言就越來越大。
在操作系統中,數據一致性訪問是一個非常重要的部分,通常我們可以採用鎖機制實現數據的一致性訪問。例如,semaphore、spinlock機制,在訪問共享數據時,首先訪問鎖資源,在獲取鎖資源的前提下才能實現數據的訪問。這種原理很簡單,根本的思想就是在訪問臨界資源時,首先訪問一個全局的變數(鎖),通過全局變數的狀態來控制線程對臨界資源的訪問。但是,這種思想是需要硬體支持的,硬體需要配合實現全局變數(鎖)的讀-修改-寫,現代CPU都會提供這樣的原子化指令。採用鎖機制實現數據訪問的一致性存在如下兩個問題:
1、 效率問題。鎖機制的實現需要對內存的原子化訪問,這種訪問操作會破壞流水線操作,降低了流水線效率。這是影響性能的一個因素。另外,在採用讀寫鎖機制的情況下,寫鎖是排他鎖,無法實現寫鎖與讀鎖的並發操作,在某些應用下回降低性能。
2、 擴展性問題。當系統中CPU數量增多的時候,採用鎖機制實現數據的同步訪問效率偏低。並且隨著CPU數量的增多,效率降低,由此可見鎖機制實現的數據一致性訪問擴展性差。
為了解決上述問題,Linux中引進了RCU機制。該機制在多CPU的平台上比較適用,對於讀多寫少的應用尤其適用。RCU的思路實際上很簡單,下面對其進行描述:
1、 對於讀操作,可以直接對共享資源進行訪問,但是前提是需要CPU支持訪存操作的原子化,現代CPU對這一點都做了保證。但是RCU的讀操作上下文是不可搶占的(這一點在下面解釋),所以讀訪問共享資源時可以採用read_rcu_lock(),該函數的工作是停止搶占。
2、 對於寫操作,其需要將原來的老數據作一次備份(),然後對備份數據進行修改,修改完畢之後再用新數據更新老數據,更新老數據時採用了rcu_assign_pointer()宏,在該函數中首先屏障一下memory,然後修改老數據。這個操作完成之後,需要進行老數據資源的回收。操作線程向系統注冊回收方法,等待回收。採用數據備份的方法可以實現讀者與寫者之間的並發操作,但是不能解決多個寫著之間的同步,所以當存在多個寫者時,需要通過鎖機制對其進行互斥,也就是在同一時刻只能存在一個寫者。
3、 在RCU機制中存在一個垃圾回收的daemon,當共享資源被update之後,可以採用該daemon實現老數據資源的回收。回收時間點就是在update之前的所有的讀者全部退出。由此可見寫者在update之後是需要睡眠等待的,需要等待讀者完成操作,如果在這個時刻讀者被搶占或者睡眠,那麼很可能會導致系統死鎖。因為此時寫者在等待讀者,讀者被搶占或者睡眠,如果正在運行的線程需要訪問讀者和寫者已經佔用的資源,那麼死鎖的條件就很有可能形成了。
F. Linux進程間通信(互斥鎖、條件變數、讀寫鎖、文件鎖、信號燈)
為了能夠有效的控制多個進程之間的溝通過程,保證溝通過程的有序和和諧,OS必須提供一定的同步機制保證進程之間不會自說自話而是有效的協同工作。比如在 共享內存的通信方式中,兩個或者多個進程都要對共享的內存進行數據寫入,那麼怎麼才能保證一個進程在寫入的過程中不被其它的進程打斷,保證數據的完整性 呢?又怎麼保證讀取進程在讀取數據的過程中數據不會變動,保證讀取出的數據是完整有效的呢?
常用的同步方式有: 互斥鎖、條件變數、讀寫鎖、記錄鎖(文件鎖)和信號燈.
互斥鎖:
顧名思義,鎖是用來鎖住某種東西的,鎖住之後只有有鑰匙的人才能對鎖住的東西擁有控制權(把鎖砸了,把東西偷走的小偷不在我們的討論范圍了)。所謂互斥, 從字面上理解就是互相排斥。因此互斥鎖從字面上理解就是一點進程擁有了這個鎖,它將排斥其它所有的進程訪問被鎖住的東西,其它的進程如果需要鎖就只能等待,等待擁有鎖的進程把鎖打開後才能繼續運行。 在實現中,鎖並不是與某個具體的變數進行關聯,它本身是一個獨立的對象。進(線)程在有需要的時候獲得此對象,用完不需要時就釋放掉。
互斥鎖的主要特點是互斥鎖的釋放必須由上鎖的進(線)程釋放,如果擁有鎖的進(線)程不釋放,那麼其它的進(線)程永遠也沒有機會獲得所需要的互斥鎖。
互斥鎖主要用於線程之間的同步。
條件變數:
上文中提到,對於互斥鎖而言,如果擁有鎖的進(線)程不釋放鎖,其它進(線)程永遠沒機會獲得鎖,也就永遠沒有機會繼續執行後續的邏輯。在實際環境下,一 個線程A需要改變一個共享變數X的值,為了保證在修改的過程中X不會被其它的線程修改,線程A必須首先獲得對X的鎖。現在假如A已經獲得鎖了,由於業務邏 輯的需要,只有當X的值小於0時,線程A才能執行後續的邏輯,於是線程A必須把互斥鎖釋放掉,然後繼續「忙等」。如下面的偽代碼所示:
1.// get x lock
2.while(x
G. Linux下各種鎖的理解和使用及總結解決epoll驚群問題(面試常考)-
鎖出現的原因
臨界資源是什麼: 多線程執行流所共享的資源
鎖的作用是什麼, 可以做原子操作, 在多線程中針對臨界資源的互斥訪問... 保證一個時刻只有一個線程可以持有鎖對於臨界資源做修改操作...
任何一個線程如果需要修改,向臨界資源做寫入操作都必須持有鎖,沒有持有鎖就不能對於臨界資源做寫入操作.
鎖 : 保證同一時刻只能有一個線程對於臨界資源做寫入操作 (鎖地功能)
再一個直觀地代碼引出問題,再從指令集的角度去看問題
上述一個及其奇怪的結果,這個結果每一次運行都可能是不一樣的,Why ? 按照我們本來的想法是每一個線程 + 20000000 結果肯定應該是60000000呀,可以就是達不到這個值
為何? (深入匯編指令來看) 一定將過程放置到匯編指令上去看就可以理解這個過程了.
a++; 或者 a += 1; 這些操作的匯編操作是幾個步驟?
其實是三個步驟:
正常情況下,數據少,操作的線程少,問題倒是不大,想一想要是這樣的情況下,操作次數大,對齊操作的線程多,有些線程從中間切入進來了,在運算之後還沒寫回內存就另外一個線程切入進來同時對於之前的數據進行++ 再寫回內存, 啥效果,多次++ 操作之後結果確實一次加加操作後的結果。 這樣的操作 (術語叫做函數的重入) 我覺得其實就是重入到了匯編指令中間了,還沒將上一次運算的結果寫回內存就重新對這個內存讀取再運算寫入,結果肯定和正常的邏輯後的結果不一樣呀
來一幅圖片解釋一下
咋辦? 其實問題很清楚,我們只需要處理的是多條匯編指令不能讓它中間被插入其他的線程運算. (要想自己在執行匯編指令的時候別人不插入進來) 將多條匯編指令綁定成為一條指令不就OK了嘛。
也就是原子操作!!!
不會原子操作?操作系統給咱提供了線程的 綁定方式工具呀:mutex 互斥鎖(互斥量), 自旋鎖(spinlock), 讀寫鎖(readers-writer lock) 他們也稱作悲觀鎖. 作用都是一個樣,將多個匯編指令鎖成為一條原子操作 (此處的匯編指令也相當於如下的臨界資源)
悲觀鎖:鎖如其名,每次都悲觀地認為其他線程也會來修改數據,進行寫入操作,所以會在取數據前先加鎖保護,當其他線程想要訪問數據時,被阻塞掛起
樂觀鎖:每次取數據的時候,總是樂觀地認為數據不會被其他線程修改,因此不上鎖。但是在更新數據前, 會判斷其他數據在更新前有沒有對數據進行修改。
互斥鎖
最為常見使用地鎖就是互斥鎖, 也稱互斥量. mutex
特徵,當其他線程持有互斥鎖對臨界資源做寫入操作地時候,當前線程只能掛起等待,讓出CPU,存在線程間切換工作
解釋一下存在線程間切換工作 : 當線程試圖去獲取鎖對臨界資源做寫入操作時候,如果鎖被別的線程正在持有,該線程會保存上下文直接掛起,讓出CPU,等到鎖被釋放出來再進行線程間切換,從新持有CPU執行寫入操作
互斥鎖需要進行線程間切換,相比自旋鎖而言性能會差上許多,因為自旋鎖不會讓出CPU, 也就不需要進行線程間切換的步驟,具體原理下一點詳述
加互斥量(互斥鎖)確實可以達到要求,但是會發現運行時間非常的長,因為線程間不斷地切換也需要時間, 線程間切換的代價比較大.
相關視頻推薦
你繞不開的組件—鎖,4個方面手撕鎖的多種實現
「驚群」原理、鎖的設計方案及繞不開的「死鎖」問題
學習地址:C/C++Linux伺服器開發/後台架構師【零聲教育】-學習視頻教程-騰訊課堂
需要C/C++ Linux伺服器架構師學習資料加qun812855908獲取(資料包括 C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg 等),免費分享
自旋鎖
spinlock.自旋鎖.
對比互斥量(互斥鎖)而言,獲取自旋鎖不需要進行線程間切換,如果自旋鎖正在被別的線程佔用,該線程也不會放棄CPU進行掛起休眠,而是恰如其名的在哪裡不斷地循環地查看自旋鎖保持者(持有者)是否將自旋鎖資源釋放出來... (自旋地原來就是如此)
口語解釋自旋:持有自旋鎖的線程不釋放自旋鎖,那也沒有關系呀,我就在這里不斷地一遍又一遍地查詢自旋鎖是否釋放出來,一旦釋放出來我立馬就可以直接使用 (因為我並沒有掛起等待,不需要像互斥鎖還需要進行線程間切換,重新獲取CPU,保存恢復上下文等等操作)
哪正是因為上述這些特點,線程嘗試獲取自旋鎖,獲取不到不會採取休眠掛起地方式,而是原地自旋(一遍又一遍查詢自旋鎖是否可以獲取)效率是遠高於互斥鎖了. 那我們是不是所有情況都使用自旋鎖就行了呢,互斥鎖就可以放棄使用了嗎????
解釋自旋鎖地弊端:如果每一個線程都僅僅只是需要短時間獲取這個鎖,那我自旋占據CPU等待是沒啥問題地。要是線程需要長時間地使用占據(鎖)。。。 會造成過多地無端占據CPU資源,俗稱站著茅坑不拉屎... 但是要是僅僅是短時間地自旋,平衡CPU利用率 + 程序運行效率 (自旋鎖確實是在有些時候更加合適)
自旋鎖需要場景:內核可搶占或者SMP(多處理器)情況下才真正需求 (避免死鎖陷入死循環,瘋狂地自旋,比如遞歸獲取自旋鎖. 你獲取了還要獲取,但是又沒法釋放)
自旋鎖的使用函數其實和互斥鎖幾乎是一摸一樣地,僅僅只是需要將所有的mutex換成spin即可
僅僅只是在init存在些許不同
何為驚群,池塘一堆, 我瞄準一條插過去,但是好似所有的都像是覺著自己正在被插一樣的四處逃竄。 這個就是驚群的生活一點的理解
驚群現象其實一點也不少,比如說 accept pthread_cond_broadcast 還有多個線程共享epoll監視一個listenfd 然後此刻 listenfd 說來 SYN了,放在了SYN隊列中,然後完成了三次握手放在了 accept隊列中了, 現在問題是這個connect我應該交付給哪一個線程處理呢.
多個epoll監視准備工作的線程 就是這群 (),然後connet就是魚叉,這一叉下去肯定是所有的 epoll線程都會被驚醒 (多線程共享listenfd引發的epoll驚群)
同樣如果將上述的多個線程換成多個進程共享監視 同一個 listenfd 就是(多進程的epoll驚群現象)
咱再畫一個草圖再來理解一下這個驚群:
如果是多進程道理是一樣滴,僅僅只是將所有的線程換成進程就OK了
終是來到了今天的正題了: epoll驚群問題地解決上面了...
首先 先說說accept的驚群問題,沒想到吧accept 平時大家寫它的多線程地時候,多個線程同時accept同一個listensock地時候也是會存在驚群問題地,但是accept地驚群問題已經被Linux內核處理了: 當有新的連接進入到accept隊列的時候,內核喚醒且僅喚醒一個進程來處理
但是對於epoll的驚群問題,內核卻沒有直接進行處理。哪既然內核沒有直接幫我們處理,我們應該如何針對這種現象做出一定的措施呢?
驚群效應帶來的弊端: 驚群現象會造成epoll的偽喚醒,本來epoll是阻塞掛起等待著地,這個時候因為掛起等待是不會佔用CPU地。。。 但是一旦喚醒就會佔用CPU去處理發生地IO事件, 但是其實是一個偽喚醒,這個就是對於線程或者進程的無效調度。然而進程或者線程地調取是需要花費代價地,需要上下文切換。需要進行進程(線程)間的不斷切換... 本來多核CPU是用來支持高並發地,但是現在卻被用來無效地喚醒,對於多核CPU簡直就是一種浪費 (浪費系統資源) 還會影響系統的性能.
解決方式(一般是兩種)
Nginx的解決方式:
加鎖:驚群問題發生的前提是多個進程(線程)監聽同一個套接字(listensock)上的事件,所以我們只讓一個進程(線程)去處理監聽套接字就可以了。
畫兩張圖來理解一下:
上述還沒有進行一個每一個進程都對應一個listensock 而是多線程共享一個listensock 運行結果如下
所有的線程同時被喚醒了,但是實際上會處理連接的僅僅只是一個線程,
咱僅僅只是將主線程做如上這樣一個簡單的修改,每一個線程對應一個listensock;每一個線程一個獨有的監視窗口,將問題拋給內核去處理,讓內核去負載均衡 : 結果如下
僅僅喚醒一個線程來進行處理連接,解決了驚群問題
本文通過介紹兩種鎖入手,以及為什麼需要鎖,鎖本質就是為了保護,持有鎖你就有權力有能力操作寫入一定的臨界保護資源,沒有鎖你就不行需要等待,本質其實是將多條匯編指令綁定成原子操作
然後介紹了驚群現象,通過一個巧妙地例子,扔一顆石子,只是瞄準一條魚扔過去了,但是整池魚都被驚醒了,
對應我們地實際問題就是, 多個線程或者進程共同監視同一個listensock。。。。然後IO連接事件到來地時候本來僅僅只是需要一個線程醒過來處理即可,但是卻會使得所有地線程(進程)全部醒過來,造成不必要地進程線程間切換,多核CPU被浪費喔,系統資源被浪費
處理方式 一。 Nginx 源碼加互斥鎖處理。。 二。設置SO_REUSEPORT, 使得多個進程線程可以同時連接同一個port , 為每一個進程線程搞一個listensock... 將問題拋給內核去處理,讓他去負載均衡地僅僅將IO連接事件分配給一個進程或線程
H. linux rcu原理
RCU, Read-Copy-Update,是Linux內核中的一種同步機制。RCU常被描述為讀寫鎖的替代品,它的特點是讀者並不需要直接與寫者進行同步,讀者與寫者也能並發的執行。
來一張圖片來描述下大體的操作吧羨薯:
多個讀者可以並發訪問臨界資源,同時使用rcu_read_lock/rcu_read_unlock來標定臨界區;
寫者(updater)在更新臨界資源的時候,拷貝一份副本作為基礎進行修改,當所有讀者離開臨界區後,把指向舊臨界資源的指針指向更新後的副本,並對舊資源進行回收處理;
圖中只顯兄羨者示一個寫者,當存在多個寫者的時候,需要在寫者之派模間進行互斥處理。
I. 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)