閱讀416 返回首頁    go 小米 go MIUI米柚


關於信號量與線程互斥鎖的區別與實現

 

https://dev.firnow.com/course/6_system/linux/Linuxjs/20090901/173322.html

之前一直沒有怎麼關注過這個問題,前些日子在麵試一家公司的時候,麵試官提到了pthread_cond_wait/pthread_cond_signal的實現,當時答的不是很好,回來就查了nptl的代碼。前天,水木上又有人問到了信號量和互斥鎖的問題,我想還是對它們的區別與實現總結一下

首先了解一些信號量和線程互斥鎖的語義上的區別:

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

援引CU上一篇帖子的內容:
“信號量用在多線程多任務同步的,一個線程完成了某一個動作就通過信號量告訴別的線程,別的線程再進行某些動作(大家都在sem_wait的時候,就阻塞在那裏)。而互斥鎖是用在多線程多任務互斥的,一個線程占用了某一個資源,那麼別的線程就無法訪問,直到這個線程unlock,其他的線程才開始可以利用這個資源。比如對全局變量的訪問,有時要加鎖,操作完了,在解鎖。有的時候鎖和信號量會同時使用的”
也就是說,信號量不一定是鎖定某一個資源,而是流程上的概念,比如:有A,B兩個線程,B線程要等A線程完成某一任務以後再進行自己下麵的步驟,這個任務並不一定是鎖定某一資源,還可以是進行一些計算或者數據處理之類。而線程互斥量則是“鎖住某一資源”的概念,在鎖定期間內,其他線程無法對被保護的數據進行操作。在有些情況下兩者可以互換。

兩者之間的區別:

作用域
信號量: 進程間或線程間(linux僅線程間)
互斥鎖: 線程間

上鎖時 
信號量: 隻要信號量的value大於0,其他線程就可以sem_wait成功,成功後信號量的value減一。若value值不大於0,則sem_wait阻塞,直到sem_post釋放後value值加一。一句話,信號量的value>=0
互斥鎖: 隻要被鎖住,其他任何線程都不可以訪問被保護的資源。如果沒有鎖,獲得資源成功,否則進行阻塞等待資源可用。一句話,線程互斥鎖的vlaue可以為負數

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

接下來,我們需要分析一下信號量和線程互斥鎖的實現機製。

在Linux下,信號量和線程互斥鎖的實現都是通過futex係統調用。

futex(快速用戶區互斥的簡稱)是一個在Linux上實現鎖定和構建高級抽象鎖如信號量和POSIX互斥的基本工具。它們第一次出現在內核開發的2.5.7版;其語義在2.5.40固定下來,然後在2.6.x係列穩定版內核中出現。

Futex 是fast userspace mutex的縮寫,意思是快速用戶空間互斥體。Linux內核把它們作為快速的用戶空間的鎖和信號量的預製構件提供給開發者。Futex非常基礎,借助其自身的優異性能,構建更高級別的鎖的抽象,如POSIX互斥體。大多數程序員並不需要直接使用Futex,它一般用來實現像NPTL這樣的係統庫。

Futex 由一塊能夠被多個進程共享的內存空間(一個對齊後的整型變量)組成;這個整型變量的值能夠通過匯編語言調用CPU提供的原子操作指令來增加或減少,並且一個進程可以等待直到那個值變成正數。Futex 的操作幾乎全部在應用程序空間完成;隻有當操作結果不一致從而需要仲裁時,才需要進入操作係統內核空間執行。這種機製允許使用 futex 的鎖定原語有非常高的執行效率:由於絕大多數的操作並不需要在多個進程之間進行仲裁,所以絕大多數操作都可以在應用程序空間執行,而不需要使用(相對高代價的)內核係統調用。

----------------------------------------------------------------
插播一段關於x86原子操作指令的說明:

cmpxchg 比較交換指令,其語義為:
int CompareAndExchange(int *ptr, int old, int new)
{
int actual = *ptr;
if (actual == old)
*ptr = new;
return actual;
}

Intel白皮書上的說明如下:
(* Accumulator = AL, AX, EAX, or RAX depending on whether a byte, word, doubleword, or
quadword comparison is being performed *)
IF accumulator = DEST
    THEN
        ZF ← 1;
        DEST ← SRC;
    ELSE
        ZF ← 0;
        accumulator ← DEST;
FI;

使用此原子操作可以實現自旋鎖,之前有一篇文章中描述了實現:
void lock(lock_t *lock) {
while (CompareAndExchange(&lock->flag, 0, 1) == 1)
; // spin
}
void unlock(lock_t *lock) {
lock->flag = 0;
}

關於smp下的原子操作的一些說明:
  原子操作是不可分割的,在執行完畢不會被任何其它任務或事件中斷。在單處理器係統(UniProcessor)中,能夠在單條指令中完成的操作都可以認為是" 原子操作",因為中斷隻能發生於指令之間。這也是某些CPU指令係統中引入了test_and_set、test_and_clear等指令用於臨界資源互斥的原因。在對稱多處理器(Symmetric Multi-Processor)結構中就不同了,由於係統中有多個處理器在獨立地運行,即使能在單條指令中完成的操作也有可能受到幹擾。
  在x86 平台上,CPU提供了在指令執行期間對總線加鎖的手段。CPU芯片上有一條引線#HLOCK pin,如果匯編語言的程序中在一條指令前麵加上前綴"LOCK",經過匯編以後的機器代碼就使CPU在執行這條指令的時候把#HLOCK pin的電位拉低,持續到這條指令結束時放開,從而把總線鎖住,這樣同一總線上別的CPU就暫時不能通過總線訪問內存了,保證了這條指令在多處理器環境中的原子性。
  當然,並不是所有的指令前麵都可以加lock前綴的,隻有ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG,DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, 和 XCHG指令前麵可以加lock指令,實現原子操作。
----------------------------------------------------------------

廣告回來了,我們繼續。

futex保存在用戶空間的共享內存中,並且通過原子操作進行操作。在大部分情況下,資源不存在爭用的情況下,進程或者線程可以立刻獲得資源成功,實際上就沒有必要調用係統調用,陷入內核了。實際上,futex的作用就在於減少係統調用的次數,來提高係統的性能。

線程互斥鎖pthread_mutex_t的實現原理:
pthread_mutex_lock:
atomic_dec(pthread_mutex_t.value);
if(pthread_mutex_t.value!=0)
   futex(WAIT)
else
   success

pthread_mutex_unlock:
atomic_inc(pthread_mutex_t.value);
if(pthread_mutex_t.value!=1)
   futex(WAKEUP)
else
   success
信號量sem_t的實現原理(直接從glibc/nptl/DESIGN-sem.txt中摘的):
sem_wait(sem_t *sem)
{
  for (;;) {

    if (atomic_decrement_if_positive(sem->count))
      break;

    futex_wait(&sem->count, 0)
  }
}

sem_post(sem_t *sem)
{
  n = atomic_increment(sem->count);
  // Pass the new value of sem->count
  futex_wake(&sem->count, n + 1);
}

對比,pthread_mutex_unlock()和sem_post()的實現,我們發現一個不同點,sem_post()無論如何都會調用futex_wake(),進行係統調用。但是pthread_mutex_unlock()卻符合futex的初衷,隻有在需要仲裁的時候才調用futex_wake()。那麼什麼是仲裁條件呢?

前麵說過信號量和線程互斥鎖語義上的區別在於信號量的value>=0,而線程互斥鎖的value可以為負數。
對於lock操作,這兩個倒是沒有多少差別。信號量隻要value>0就可以獲得資源,線程互斥鎖需要value=1。
但是對於unlock操作,這兩個就有一些差別了。信號量和線程互斥鎖,都會增加對應的value。如果加1後,value為1,對於線程互斥鎖來講,實際上表明資源可用,並且之前沒有其他的線程在等待這個資源;否則說明還有其他線程在等待這個資源,需要調用futex係統調用喚醒它們。但是對於信號量,由於value必須>=0。那麼加1後,即使value為1,也無法判定現在沒有其他的進程或線程正在等待資源,所以必須調用futex係統調用。例如:
#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>

sem_t sem_a;
void *task1();

int main(void)
{
 int ret=0;
 pthread_t thrd1;
 pthread_t thrd2;
 sem_init(&sem_a,0,1);
 ret=pthread_create(&thrd1,NULL,task1,NULL); //創建子線程
 ret=pthread_create(&thrd2,NULL,task1,NULL); //創建子線程
 pthread_join(thrd1,NULL); //等待子線程結束
 pthread_join(thrd2,NULL); //等待子線程結束
}

void *task1()
{
  int sval = 0;
  sem_wait(&sem_a); //持有信號量
  sleep(5); //do_nothing
  sem_getvalue(&sem_a,&sval);
  printf("sem value = %d/n",sval);
  sem_post(&sem_a); //釋放信號量
}
上麵sem的value初始化為1,但是有兩個線程爭用資源。那麼第一個線程獲得資源成功,當它unlock的時候,sem的value變為1。但是,這個時候,實際上還有一個線程在等待資源。因此,必須要進行futex_wake()係統調用,喚醒等待資源的線程。

感興趣的同學可以使用strace跟蹤一下,進行驗證。要注意忽略程序運行初始化的那個futex_wake ;-)

參考:
https://www.eetop.cn/blog/html/04/343504-14125.html
https://javadino.blog.sohu.com/99256728.html
https://javadino.blog.sohu.com/99256835.html
https://javadino.blog.sohu.com/99256921.html

最後更新:2017-04-02 06:51:26

  上一篇:go STL中的valarray
  下一篇:go socket編程:SO_REUSEADDR例解