1. 程式人生 > >自旋鎖、互斥體和訊號量

自旋鎖、互斥體和訊號量

自旋鎖

Linux核心中最常見的鎖是自旋鎖(spin lock)。自旋鎖最多隻能被一個可執行執行緒持有。如果一個執行執行緒試圖獲得一個被已經持有的自旋鎖,那麼該執行緒就會一直進行忙迴圈——旋轉——等待鎖重新可用。要是鎖未被爭用,請求鎖的執行執行緒便能立刻得到它,繼續執行。在任意時間,自旋鎖都可以防止多於一個的執行執行緒同時進入臨界區。同一個鎖可以用在多個位置。例如,對於給定資料的所有訪問都可以得到保護和同步。

自旋鎖相當於上廁所時,在門外等待的過程。如果你到在廁所門外,發現裡面沒有人,就可以推開門進入廁所。如果你到了廁所門口發現門是關著的(裡面有人),就必須在門口等待(此時你很著急),不斷地檢查廁所是否為空。當廁所為空時,你就可以進入了。正是因為有了門(相當於自旋鎖),才允許一次只有一個人(相當於執行執行緒)進入廁所裡(相當於臨界區)。

一個被爭用的自旋鎖使得請求它的執行緒在等待鎖重新可用時自旋(特別浪費處理器時間),這種行為是自旋鎖的要點。所以自旋鎖不應該被長時間的持有。事實上,這點正是使用自旋鎖的初衷:在段期間內進行輕量級加鎖。還可以採取另外的方式來處理對鎖的爭用:讓請求執行緒睡眠,直到鎖重新可用時再喚醒它。這樣處理器就不必迴圈等待,可以去執行其他程式碼。這也會帶來一定的開銷——這裡有兩次明顯的上下文切換,被阻塞的執行緒要換出和換入,與實現自旋鎖的少數幾行程式碼相比,上下文切換當然有較多的程式碼。因此,持有自旋鎖的時間最好小於完成兩次上下文切換的耗時。當然我們大多數人都不會無聊到去測量上下文切換的耗時,所以我們讓持有自旋鎖的時間應儘可能的短就可以了。

自旋鎖的實現和體系結構密切相關,程式碼往往通過彙編實現。這些與體系結構相關的程式碼定義在檔案<asm/spinlock.h>中,實際需要用到的介面定義在檔案<linux/spinlock.h>中。本文參考的書籍是Linux核心設計與實現,其討論的是2.6.34核心版本。自旋鎖的基本使用方式如下:

DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/*臨界區...*/
spin_unlock(&mr_lock);

因為自旋鎖在同一時刻最多被一個執行執行緒持有,所以一個時刻只能有一個執行緒位於臨界區內,這就為多處理器機器提供了防止併發訪問所需的保護機制。注意:在單處理器機器上,編譯的時候並不會加入自旋鎖。它僅僅被當做一個設定核心搶佔機制是否被啟用的開關。

如果禁止核心搶佔,那麼在編譯時自旋鎖會被完全剔除出核心。

注意:自旋鎖是不可遞迴的

Linux核心實現的自旋鎖是不可遞迴的,這點不同於自旋鎖在其他作業系統中的實現。所以如果你試圖得到一個你正持有的鎖,你必須自旋,等待你自己釋放這個鎖。由於你處於自旋忙等待,所以你永遠沒有機會釋放鎖,於是你被自己鎖死了。

自旋鎖可能帶來的問題

(1)死鎖。試圖遞迴地獲得自旋鎖必然會引起死鎖:例如遞迴程式的持有例項在第二個例項迴圈,以試圖獲得相同自旋鎖時,就不會釋放此自旋鎖。所以,在遞迴程式中使用自旋鎖應遵守下列策略:遞迴程式決不能在持有自旋鎖時呼叫它自己,也決不能在遞迴呼叫時試圖獲得相同的自旋鎖。此外如果一個程序已經將資源鎖定,那麼,即使其它申請這個資源的程序不停地瘋狂“自旋”,也無法獲得資源,從而進入死迴圈。
(2)過多佔用CPU資源。如果不加限制,由於申請者一直在迴圈等待,因此自旋鎖在鎖定的時候,如果不成功,不會睡眠,會持續的嘗試,單cpu的時候自旋鎖會讓其它process動不了。因此,一般自旋鎖實現會有一個引數限定最多持續嘗試次數。超出後,自旋鎖放棄當前time slice,等下一次機會。

自旋鎖的操作

spin_lock_init():可以使用該方法來初始化動態建立的自旋鎖(此時你只有一個指向spinlock_t型別的指標,沒有它的實體)。

spin_try_lock():試圖獲得某個特定的自旋鎖,如果該鎖已經被爭用,那麼該方法會立即返回一個非0值,而不會自旋等待鎖被釋放;如果成功地獲得了這個自旋鎖,該函式返回0.同理,spin_is_locked()方法用於檢查特定的鎖當前是否已被佔用,如果被佔用,返回非0值;否則返回0。該方法只做判斷,並不實際佔用。

標準的自旋鎖操作的完整列表:

spin_lock_init(lock)

初始化自旋鎖,將自旋鎖設定為1,表示有一個資源可用。

spin_is_locked(lock)

如果自旋鎖被置為1(未鎖),返回0,否則返回1。

spin_unlock_wait(lock)

等待直到自旋鎖解鎖(為1),返回0;否則返回1。

spin_trylock(lock)

嘗試鎖上自旋鎖(置0),如果原來鎖的值為1,返回1,否則返回0。

spin_lock(lock)

迴圈等待直到自旋鎖解鎖(置為1),然後,將自旋鎖鎖上(置為0)。

spin_unlock(lock)

將自旋鎖解鎖(置為1)。

spin_lock_irqsave(lock, flags)

迴圈等待直到自旋鎖解鎖(置為1),然後,將自旋鎖鎖上(置為0)。關中斷,將狀態暫存器值存入flags。

spin_unlock_irqrestore(lock, flags)

將自旋鎖解鎖(置為1)。開中斷,將狀態暫存器值從flags存入狀態暫存器。

spin_lock_irq(lock)

迴圈等待直到自旋鎖解鎖(置為1),然後,將自旋鎖鎖上(置為0)。關中斷。

spin_unlock_irq(lock)

將自旋鎖解鎖(置為1)。開中斷。

spin_unlock_bh(lock)

將自旋鎖解鎖(置為1)。開啟底半部的執行。

spin_lock_bh(lock)

迴圈等待直到自旋鎖解鎖(置為1),然後,將自旋鎖鎖上(置為0)。阻止軟中斷的底半部的執行。

spin_lock和spin_lock_irq的區別

(1)spin_lock
spin_lock 的實現關係為:spin_lock -> raw_spin_lock -> _raw_spin_lock -> __raw_spin_lock ,而__raw_spin_lock 的實現為:

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    preempt_disable();
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

(2)spin_lock_irq

spin_lock_irq 的實現關係為:spin_lock_irq -> raw_spin_lock_irq -> _raw_spin_lock_irq -> __raw_spin_lock_irq,而__raw_spin_lock_irq 的實現為:

static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
    local_irq_disable();
    preempt_disable();
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

注意“preempt_disable()”,這個呼叫的功能是“關搶佔”(在spin_unlock中會重新開啟搶佔功能)。從中可以看出,使用自旋鎖保護的區域是工作在非搶佔的狀態;即使獲取不到鎖,在“自旋”狀態也是禁止搶佔的。瞭解到這,我想咱們應該能夠理解為何自旋鎖保護 的程式碼不能睡眠了。試想一下,如果在自旋鎖保護的程式碼中間睡眠,此時發生程序排程,則可能另外一個程序會再次呼叫spinlock保護的這段程式碼。而我們 現在知道了即使在獲取不到鎖的“自旋”狀態,也是禁止搶佔的,而“自旋”又是動態的,不會再睡眠了,也就是說在這個處理器上不會再有程序排程發生了,那麼死鎖自然就發生了。

由此可見,這兩者之間只有一個差別:是否呼叫local_irq_disable()函式,即是否禁止本地中斷。這兩者的區別可以總結為:在任何情況下使用spin_lock_irq都是安全的。因為它既禁止本地中斷,又禁止核心搶佔。spin_lock比spin_lock_irq速度快,但是它並不是任何情況下都是安全的。

舉例來說明:程序A中呼叫了spin_lock(&lock)然後進入臨界區,此時來了一箇中斷(interrupt),該中斷也執行在和程序A相同的CPU上,並且在該中斷處理程式中恰巧也會spin_lock(&lock)試圖獲取同一個鎖。由於是在同一個CPU上被中斷,程序A會被設定為TASK_INTERRUPT狀態,中斷處理程式無法獲得鎖,會不停的忙等,由於程序A被設定為中斷狀態,schedule()程序排程就無法再排程程序A執行,這樣就導致了死鎖!但是如果該中斷處理程式執行在不同的CPU上就不會觸發死鎖。因為在不同的CPU上出現中斷不會導致程序A的狀態被設為TASK_INTERRUPT,只是換出。當中斷處理程式忙等被換出後,程序A還是有機會獲得CPU,執行並退出臨界區。所以在使用spin_lock時要明確知道該鎖不會在中斷處理程式中使用。

自旋鎖可以使用在中斷處理程式中(此處不能使用訊號量,因為它們會導致睡眠)。在中斷處理程式中使用自旋鎖時,一定要在獲取鎖之前,先禁止本地中斷(在當前處理器上的中斷請求),否則,中斷處理程式會打斷正在持有的鎖的核心程式碼,有可能會試圖去爭用這個已經被持有的自旋鎖。這樣一來,中斷處理程式就會自旋,等待該鎖重新可用,但是鎖的持有者在這個中斷處理程式執行完畢前不可能執行(雙重請求死鎖)。注意,需要關閉的只是當前處理器上的中斷。如果中斷髮生在不同的處理器上,即使中斷處理程式在同一鎖上自旋,也不會妨礙鎖的持有者(在不同處理器上)最終釋放鎖。

核心提供的禁止中斷同時請求鎖的介面spin_lock_irqsave的實現關係spin_lock_irqsave------>__raw_spin_lock_irqsave

static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
{
    unsigned long flags;
    local_irq_save(flags);
    preempt_disable();
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    /*
     * On lockdep we dont want the hand-coded irq-enable of
     * do_raw_spin_lock_flags() code, because lockdep assumes
     * that interrupts are not re-enabled during lock-acquire:
     */
#ifdef CONFIG_LOCKDEP
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
#else
    do_raw_spin_lock_flags(lock, &flags);
#endif
    return flags;
}

使用方法:

DEFINE_SPINLOCK(mr_lock);
unsigned long flags;
spin_lock_irqsave(&mr_lock, flags);
/*臨界區*/
spin_unlock_irqrestore(&mr_lock, flags);

函式spin_lock_irqsave()儲存中斷的當前狀態,並禁止本地中斷,所以再去獲取指定的鎖。反過來spin_unlock_irqrestore()對指定的鎖解鎖,然後讓中斷恢復到加鎖前的狀態。所以即使中斷最初是被禁止的,程式碼也不會錯誤地啟用它們,相反,會繼續讓它們禁止。注意,flags變數看起來像是由數值傳遞的,這是因為這些鎖函式有些部分是通過巨集的方式實現的。在單處理器系統上,雖然在編譯時拋棄掉了鎖機制,但在上面的例子中仍需要關閉中斷,以禁止中斷處理程式訪問共享資料。加鎖和解鎖分別可以禁止和允許核心搶佔。

由此可見,自旋鎖比較適用於鎖使用者保持鎖時間比較短的情況。正是由於自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高於互斥鎖。訊號量和讀寫訊號量適合於保持時間較長的情況,它們會導致呼叫者睡眠,因此只能在程序上下文使用,而自旋鎖適合於保持時間非常短的情況,它可以在任何上下文使用。

自旋鎖為什麼廣泛用於核心

自旋鎖是一種輕量級的互斥鎖,可以更高效的對互斥資源進行保護。自旋鎖本來就只是一個很簡單的同步機制,在SMP之前根本就沒這個東西,一切都是Event之類的同步機制,這類同步機制都有一個共性就是:一旦資源被佔用都會產生任務切換,任務切換涉及很多東西的(儲存原來的上下文,按排程演算法選擇新的任務,恢復新任務的上下文,還有就是要修改cr3暫存器會導致cache失效)這些都是需要大量時間的,因此用Event之類來同步一旦涉及到阻塞代價是十分昂貴的,而自旋鎖的效率就遠高於互斥鎖。

總結自旋鎖在不同CPU下工作的特點:

(1)單CPU非搶佔核心下:自旋鎖會在編譯時被忽略(因為單CPU且非搶佔模式情況下,不可能發生程序切換,時鐘只有一個程序處於臨界區(自旋鎖實際沒什麼用了)。

(2)單CPU搶佔核心下:自選鎖僅僅當作一個設定搶佔的開關(因為單CPU不可能有併發訪問臨界區的情況,禁止搶佔就可以保證臨街區唯一被擁有)。

(3)多CPU下:此時才能完全發揮自旋鎖的作用,自旋鎖在核心中主要用來防止多處理器中併發訪問臨界區,防止核心搶佔造成的競爭。

 

POSIX提供的與自旋鎖相關的函式

使用自旋鎖時要注意:由於自旋時不釋放CPU,因而持有自旋鎖的執行緒應該儘快釋放自旋鎖,否則等待該自旋鎖的執行緒會一直在哪裡自旋,這就會浪費CPU時間。

持有自旋鎖的執行緒在sleep之前應該釋放自旋鎖以便其他咸亨可以獲得該自旋鎖。核心程式設計中,如果持有自旋鎖的程式碼sleep了就可能導致整個系統掛起。(下面會解釋)使用任何鎖都需要消耗系統資源(記憶體資源和CPU時間),這種資源消耗可以分為兩類:1.建立鎖所需要的資源2.當執行緒被阻塞時所需要的資源

int pthread_spin_init(pthread_spinlock_t*lock,int pshared);

初始化spin lock,當執行緒使用該函式初始化一個未初始化或者被destroy過的spin lock有效。該函式會為spin lock申請資源並且初始化spin lock為unlocked狀態。有關第二個選項是這麼說的:If the Thread Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_SHARED,the implementation shall permit the spin lock to be operated upon by any thread that has access to the memory where the spin lock is allocated,even if it is allocated in memory that is shared by multiple processes.If the Thread Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_PRIVATE,or if the option is not supported,the spin lock shall only be operated upon by threads created within the same process as the thread that initialized the spin lock.If threads of differing processes attempt to operate on such a spin lock,the behav‐ior is undefined.

所以,如果初始化spin lock的執行緒設定第二個引數為PTHREAD_PROCESS_SHARED,那麼該spin lock不僅被初始化執行緒所在的程序中所有執行緒看到,而且可以被其他程序中的執行緒看到,PTHREAD_PROESS_PRIVATE則只被同一程序中執行緒看到。如果不設定該引數,預設為後者。

int pthread_spin_destroy(pthread_spinlock_t*lock); 銷燬spin lock,作用和mutex的相關函式類似。
int pthread_spin_lock(pthread_spinlock_t*lock); 加鎖函式,不過這麼一點值得注意:EBUSY A thread currently holds the lock。These functions shall not return an error code of[EINTR].
int pthread_spin_trylock(pthread_spinlock_t*lock); 試圖獲取指定的鎖
int pthread_spin_unlock(pthread_spinlock_t*lock); 解鎖函式。不是持有鎖的執行緒呼叫或者解鎖一個沒有lock的spin lock這樣的行為都是undefined的。

 

訊號量

Linux中的訊號量是一種睡眠鎖。如果有一個任務試圖獲得一個不可用(已經被佔用)的訊號量時,訊號量會將其推進一個等待佇列,然後讓其睡眠。這時處理器能重獲自由,從而去執行其他程式碼。當持有的訊號量可用(被釋放)後,處於等待佇列中的那個任務將被喚醒,並獲得該訊號量。例如:當某個人到了門前(鑰匙在門外,進去房間的人持有鑰匙),此時房間(臨界區)裡沒有人,於是他就進入房間並關上了門。最大的差異在於當另外一個人想進入房間,但無法進入時,這傢伙不是在徘徊,而是把自己的名字寫在一個列表中,然後去打盹。當裡面的人離開房間(釋放鑰匙)時,就在門口檢視一下列表。如果列表上有名字,他就對第一個名字仔細檢查,並叫醒那個人讓他進入房間,在這種方式中,鑰匙(相當於訊號量)確保一次只有一個人(相當於執行執行緒)進入房間(臨界區)。這就比自旋鎖提供了更好的處理器利用率,因為沒有把時間花費在忙等待上,但是,訊號量比自旋鎖有更大的開銷。可以從訊號量的睡眠特性中得出以下結論:

(1)由於爭用訊號量的程序在等待鎖重新變為可用時會睡眠,所以訊號量適用於鎖會被長時間持有的情況。 

(2)如果鎖被短時間持有時,此時不建議使用訊號量。因為睡眠、維護、等待佇列以及喚醒等操作,其所花費的開銷可能要比鎖持有的全部時間還要長。

(3)由於執行執行緒在鎖被爭用時會睡眠,所以只能在程序上下文中才能獲取訊號量鎖,因為在中斷上下文中是不能進行排程的。

(4)可以在持有訊號量時去睡眠,因為當其他程序試圖獲得同一訊號量時不會因此而死鎖(因為該程序也只是去睡眠而已,而前一個程序最終會繼續執行)

(5)佔用訊號量的同時不能佔用自旋鎖。因為在你等待訊號量時可能會睡眠,而在持有自旋鎖時是不允許睡眠的。

在使用訊號量的大多數時候,選擇餘地並不大。往往在需要和使用者空間同步時,當代碼需要睡眠,此時使用訊號量是唯一的選擇。由於不受睡眠的限制,使用訊號量通常來說更加容易一些。訊號量不同於自旋鎖,它不會禁止核心搶佔,所以持有訊號量的程式碼可以被搶佔。這意味著訊號量不會對排程的等待時間帶來負面影響。

使用者搶佔在以下情況下產生:
從系統呼叫返回使用者空間
從中斷處理程式返回使用者空間
核心搶佔會發生在:
當從中斷處理程式返回核心空間的時候,且當時核心具有可搶佔性
當核心程式碼再一次具有可搶佔性的時候(如:spin_unlock時)
如果核心中的任務顯示的呼叫schedule()

計數訊號量和二值訊號量

訊號量同時允許的持有者數量可以在宣告訊號量時指定。這個值成為使用者數量或簡單的叫數量。通常情況下,訊號量和自旋鎖一樣,在一個時刻僅允許有一個鎖的持有者。這時計數等於1,這樣的訊號量被稱為二值訊號量(因為它或者由一個任務特有,或者根本沒有任務持有它)或者稱為互斥訊號量(因為它強制進行互斥)。另一方面,初始化時也可以把數量設定為大於1的非0值。這種情況,訊號量被稱為計數訊號量,它允許在一個時刻最多有count個鎖持有者。計數訊號量不能用來進行強制互斥,因為它允許多個執行執行緒同時訪問臨界區。相反,這種訊號量用來對特定程式碼加以限制,核心中使用它的機會不多。在使用訊號量時,基本上用到的都是互斥訊號量(計數等於1的訊號量)。

訊號量是一種常見的鎖機制,它支援兩個原子操作P()和V(),後來的系統把這兩個操作分別叫做down()和up(),Linux也遵從這種叫法。down()操作通過對訊號量計數減1來請求獲得一個訊號量。如果結果是0或者大於0,獲得訊號量鎖,任務就可以進入臨界區。如果結果是負數,任務會被放入等待佇列,處理器執行其他任務。該函式如同一個動詞,降低(down)一個訊號量就等於獲取該訊號量。相反,當臨界區中的操作完成後,up()操作用來釋放訊號量,該操作也被稱作提升訊號量,因為它會增加訊號量的計數值。如果在該訊號量上的等待佇列不為空,那麼處於佇列中等待的任務在被喚醒的同時會獲得該訊號量。

建立訊號量和初始化訊號量

訊號量的實現是與體系結構相關的,具體實現定義在檔案<asm/semaphore.h>中。struct semaphore型別用來表示訊號量。可以通過以下方式靜態地宣告訊號量——其中name是訊號量變數名,count是訊號量的使用數量:

struct semaphore name;
sema_init(&name, count);

建立更為普通的互斥訊號量可以使用以下快捷方式:

static DECLARE_MUTEX(name);//Linux 2.6.36以後,將#define DECLARE_MUTEX(name)改成了#define DEFINE_SEMAPHORE(name)

更常見的情況是,訊號量作為一個數據結構的一部分動態建立。此時,只有指向該動態建立的訊號量的間接指標,可以使用如下函式來對其進行初始化:

sema_init(sem, count);   //sem是指標,count是訊號量的使用者數量。

與前面情況類似,初始化一個動態建立的互斥訊號量時,使用如下函式:

init_MUTEX(sem);

 

互斥體(互斥鎖)

核心中唯一允許睡眠的鎖是訊號量。多數使用者使用訊號量只使用計數1,說白了是把其作為一個互斥的排他鎖使用。訊號量的用途更通用,沒有多少使用限制。這點使得訊號量適合用於那些較為複雜的、未明情況下的互斥訪問,比如核心與使用者空間複雜的互動行為。但這也意味著簡單的鎖定而使用訊號量並不方便,並且訊號量也缺乏強制的規則來行使任何形式的自動除錯,即便受限的除錯也不可能。為了找到一個更簡單睡眠鎖,核心開發者們引入了互斥體(mutex)。互斥體這個稱謂所指的是任何可以睡眠的強制互斥鎖,比如使用計數是1的訊號量。但是在Linux核心2.6.34中,互斥體這個稱謂也用於一種實現互斥的特定睡眠鎖。也就是說,互斥體是一種互斥訊號。

mutex在核心中對應資料結構mutex,其行為和使用計數為1的訊號量類似,但操作介面更簡單,實現也更為高效,而且使用限制更強。靜態定義mutex,你需要做:

DEFINE_MUTEX(name);
動態初始化mutex:
mutex_init(&mutex);

對互斥鎖加鎖和解鎖:

mutex_lock(&mutex);
/*臨界區*/
mutex_unlock(&mutex);

Mutex方法:

mutex_lock(struct mutex*)       為指定的mutex上鎖,如果鎖不可用則睡眠
mutex_unlock(struct mutex*)     為指定的mutex解鎖
mutex_trylock(struct mutex*)    試圖獲取指定的mutex,成功返回1;否則,返回0
mutex_is_lock(struct mutex*)    如果鎖已被爭用,則返回1;否則返回0

C語言的多執行緒程式設計中,互斥鎖的初始化 

標頭檔案:#include <pthread.h>

函式原型:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_init() 函式是以動態方式建立互斥鎖的,引數attr指定了新建互斥鎖的屬性。如果引數attr為空,則使用預設的互斥鎖屬性,預設屬性為快速互斥鎖 。互斥鎖的屬性在建立鎖的時候指定,在LinuxThreads實現中僅有一個鎖型別屬性,不同的鎖型別在試圖對一個已經被鎖定的互斥鎖加鎖時表現不同。pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經被佔據時返回EBUSY而不是掛起等待。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  初始化一個快速鎖的巨集定義
pthread_mutex_lock(&mutex);
/*中間程式碼*/
pthread_mutex_unlock(&mutex);
函式成功執行後,互斥鎖被初始化為未鎖住態。

互斥鎖例子:

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>

/*全域性變數*/
int sum = 0;
/*互斥量 */
pthread_mutex_t mutex;
/*宣告執行緒執行服務程式*/
void* pthread_function1 (void*);
void* pthread_function2 (void*);

int main (void)
{
    /*執行緒的識別符號*/
    pthread_t pt_1 = 0;
    pthread_t pt_2 = 0;
    int ret = 0;
    /*互斥初始化*/
    pthread_mutex_init (&mutex, NULL);
    /*分別建立執行緒1、2*/
    ret = pthread_create( &pt_1,                  //執行緒識別符號指標
                           NULL,                  //預設屬性
                           pthread_function1,     //執行函式
                           NULL);                 //無引數
    if (ret != 0)
    {
        perror ("pthread_1_create");
    }

    ret = pthread_create( &pt_2,                  //執行緒識別符號指標
                          NULL,                   //預設屬性
                          pthread_function2,      //執行函式
                          NULL);                  //無引數
    if (ret != 0)
    {
        perror ("pthread_2_create");
    }
    /*等待執行緒1、2的結束*/
    pthread_join (pt_1, NULL);
    pthread_join (pt_2, NULL);

    printf ("main programme exit!\n");
    return 0;
}

/*執行緒1的服務程式*/
void* pthread_function1 (void*a)
{
    int i = 0;
    printf ("This is pthread_1!\n");
    for( i=0; i<3; i++ )
    {
        pthread_mutex_lock(&mutex); /*獲取互斥鎖*/
        /*注意,這裡以防執行緒的搶佔,以造成一個執行緒在另一個執行緒sleep時多次訪問互斥資源,所以sleep要在得到互斥鎖後呼叫*/
        sleep (1);
        /*臨界資源*/
        sum++;
        printf ("Thread_1 add one to num:%d\n",sum);
        pthread_mutex_unlock(&mutex); /*釋放互斥鎖*/
    }
    pthread_exit ( NULL );
}

/*執行緒2的服務程式*/
void* pthread_function2 (void*a)
{
    int i = 0;
    printf ("This is pthread_2!\n");
    for( i=0; i<5; i++ )
    {
        pthread_mutex_lock(&mutex); /*獲取互斥鎖*/
        /*注意,這裡以防執行緒的搶佔,以造成一個執行緒在另一個執行緒sleep時多次訪問互斥資源,所以sleep要在得到互斥鎖後呼叫*/
        sleep (1);
        /*臨界資源*/
        sum++;
        printf ("Thread_2 add one to num:%d\n",sum);
        pthread_mutex_unlock(&mutex); /*釋放互斥鎖*/
    }
    pthread_exit ( NULL );
}

mutex的簡潔性和高效性源於相比使用訊號量更多的受限性。它不同於訊號量,其使用場景相對而言更嚴格、更定向了。

(1)任何時刻中只有一個任務可以持有mutex,也就是說,mutex的使用計數永遠是1。

(2)給mutex上鎖者必須負責給其再解鎖——你不能再上下文中鎖定一個mutex,而在另一個上下文中給它解鎖。這個限制使得mutex不適合核心同用戶空間複雜的同步場景。最常使用的方式是:在同一個上下文中上鎖和解鎖。

(3)遞迴地上鎖和解鎖是不允許的。也就是說,你不能遞迴地持有同一個鎖,同樣你也不能再去解一個已經解開的mutex。

(4)當持有一個mutex時,程序不可以退出。

(5)mutex不能再中斷或者下半部中使用,即使使用mutex_trylock()也不行。整個中斷處理流程被分為兩個部分,中斷處理程式為上半部。而下半部的任務是執行與中斷處理密切相關但中斷處理程式本身不執行的工作。

(6)mutex只能通過官方API管理:它不可被拷貝、手動初始化或者重複初始化。

從實現原理上來講,Mutex屬於sleep-waiting型別的鎖。例如在一個雙核的機器上有兩個執行緒(執行緒A和執行緒B),它們分別執行在Core0和Core1上。假設執行緒A想要通過 pthread_mutex_lock操作去得到一個臨界區的鎖,而此時這個鎖正被執行緒B所持有,那麼執行緒A就會被阻塞(blocking),Core0 會在此時進行上下文切換(Context Switch)將執行緒A置於等待佇列中,此時Core0就可以執行其他的任務(例如另一個執行緒C)而不必進行忙等待。而Spin lock則不然,它屬於busy-waiting型別的鎖,如果執行緒A是使用pthread_spin_lock操作去請求鎖,那麼執行緒A就會一直在 Core0上進行忙等待並不停的進行鎖請求,直到得到這個鎖為止。

 

訊號量和互斥體

互斥體和訊號量很相似,核心中兩者共存會令人混淆。所幸,它們的標準使用方式都有簡單的規範:除非mutex的某個約束妨礙你使用,否則相比訊號量要優先使用mutex。如果你所寫的是很底層的程式碼,才會需要使用訊號量。如果發現不能滿足其約束條件,且沒有其他別的選擇時,再考慮選擇訊號量。

自旋鎖和互斥體

對於自旋鎖來說,它只需要消耗很少的資源來建立鎖;隨後當執行緒被阻塞時,它就會一直重複檢檢視鎖是否可用了,也就是說當自旋鎖處於等待狀態時它會一直消耗CPU時間。
對於互斥鎖來說,與自旋鎖相比它需要消耗大量的系統資源來建立鎖;隨後當執行緒被阻塞時,執行緒的排程狀態被修改,並且執行緒被加入等待執行緒佇列;最後當鎖可用 時,在獲取鎖之前,執行緒會被從等待佇列取出並更改其排程狀態;但是線上程被阻塞期間,它不消耗CPU資源。
因此自旋鎖和互斥鎖適用於不同的場景。自旋鎖適用於那些僅需要阻塞很短時間的場景,而互斥鎖適用於那些可能會阻塞很長時間的場景。還有一點是在中斷上下文中只能使用自旋鎖,而在任務睡眠時只能使用互斥體。

需求                          建議的加鎖方式
低開銷加鎖                     優先使用自旋鎖
短期鎖定                       優先使用自旋鎖
長期加鎖                       優先使用互斥體
中斷上下文中加鎖                使用自旋鎖
持有鎖需要睡眠                  使用互斥體

 

參考:

《Linux核心設計與實現》

https://www.cnblogs.com/kuliuheng/p/4064680.html

https://www.cnblogs.com/aaronLinux/p/5890924.html

https://blog.csdn.net/wh_19910525/article/details/11536279

https://blog.csdn.net/freeelinux/article/details/53695111