自旋锁、互斥锁和信号量
自旋鎖
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?的實現為:
(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_LOCKDEPLOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); #elsedo_raw_spin_lock_flags(lock, &flags); #endifreturn 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;否則返回0C語言的多線程編程中,互斥鎖的初始化
頭文件:#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); /*獲取互斥鎖*//*臨界資源*/sum++;printf ("Thread_1 add one to num:%d\n",sum);pthread_mutex_unlock(&mutex); /*釋放互斥鎖*//*注意,這里以防線程的搶占,以造成一個線程在另一個線程sleep時多次訪問互斥資源,所以sleep要在得到互斥鎖后調用*/sleep (1);}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); /*獲取互斥鎖*//*臨界資源*/sum++;printf ("Thread_2 add one to num:%d\n",sum);pthread_mutex_unlock(&mutex); /*釋放互斥鎖*//*注意,這里以防線程的搶占,以造成一個線程在另一個線程sleep時多次訪問互斥資源,所以sleep要在得到互斥鎖后調用*/sleep (1);}pthread_exit ( NULL ); }Linux下編譯時需要加 -lpthread注意第一個字母是大寫,windows C語言中單位是毫秒(ms)。 Sleep (500); 就是到這里停半秒,然后繼續向下執行。 包含在#include <windows.h>頭文件在Linux C語言中 sleep的單位是秒(s) sleep(5);//停5秒 包含在 #include <unistd.h>頭文件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
總結
以上是生活随笔為你收集整理的自旋锁、互斥锁和信号量的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C/C++线程基本函数
- 下一篇: 可重入锁(递归锁) 互斥锁属性设置