使用POSIX Threads進行多線程編程(二) ——使用互斥量同步線程

說明:

  1. 本文是翻譯自《MultiThreaded-Programming-With-POSIX》,作者Guy Kerens。
  2. 本文預計翻譯三章,主要涉及pthread基本知識互斥量(鎖)條件變量,一是因爲這已經能夠引導讀者入門,二是因爲本人在工作之餘翻譯,實在時間捉急。
  3. 翻譯:張小川,轉載請保留原作者

使用互斥量同步線程

當在同一個內存空間跑幾個線程時的一個基本問題就是保證它們不會同時操作同一內存(step on each other's toes).這一點,我們指的是使用兩個不同線程的數據結構問題。

例如,考慮這樣一個例子:兩個線程嘗試更新兩個變量。一個線程將兩個值都設爲0,另一個線程將兩個值都設爲1。如果兩個線程同時想要訪問這兩個數據,我們可能會得到一個0一個1.這是因爲在一個線程將第一個線程置零後可能會發生一個上下文切換(contex-switch),切換之後第二個線程將兩個變量都置爲1,而當再次切換到第一個線程後,它僅將第二個變量置零,這樣我們就得到了一個0一個1.

什麼是互斥量

互斥量(或稱爲互斥鎖,以下在不影響閱讀的情況下不加以區分)是pthread 庫爲解決這個問題提供的一個基本的機制。互斥量是一個鎖,它保證如下三件事情:

  1. 原子性-鎖住一個互斥量是一個院子操作,表明操作系統(或者線程庫)保證如果你鎖了一個互斥量,那麼在同一時刻就不會有其他線程能夠鎖住這個互斥量;
  2. 奇異性-如果一個線程鎖住了一個互斥量,那麼可以保證的是在該線程釋放這個鎖之前沒有其他線程可以鎖住這個互斥量;
  3. 非忙等待-如果一個線程(線程1)嘗試去鎖住一個由線程2鎖住的鎖,線程1會掛起(suspend)並且不會消耗任何CPU資源,直到線程2釋放了這個鎖。這時,線程1會喚醒並繼續執行,鎖住這個互斥量。

從這三點可以看到一個互斥鎖是如何保證對變量(或一般的關鍵代碼段)的互斥訪問的。下面給出了上節討論的如何更新兩個變 量的僞代碼。
第一個線程使用:

lock mutex 'X1'
set 1st variable to '0'
set 2nd variable to '0'
unlock mutex 'X1'

第二個線程使用:

lock mutex 'X1'
set 1st variable to '1'
set 2nd variable to '1'
unlock mutex 'X1'

假定兩個線程使用相同的互斥量,那麼可以保證的是在兩個線程都跑過這段代碼之後,兩個變量要麼全是0要麼全1.你應該注意到這需要程序員來做一些工作——如果第三個線程通過不適用這個互斥量的其他代碼來修改這些變量,仍然可能弄亂這兩個變量的值。因此,將訪問這兩個變量的所有代碼都放入一個小的函數集合中,並且只通過這些函數來訪問這兩個變量就變得至關重要。

創建和初始化互斥量

在使用互斥量之前需要聲明一個pthread_mutex_t類型的變量,然後對其初始化。最簡單的方法就是用常量PTHREAD_MUTEX_INITIALIZER對其賦值。所以我們的代碼會是如下形式:

pthread_mutex_t a_mutex = PTHREAD_MUTEX_INITIALIZER;

這兒需要注意一點的是,這種初始化方法創建的互斥量稱爲快互斥量(‘fast mutex’)。這表明如果一個線程鎖住了這個互斥鎖,然後嘗試再次對其上鎖,它會卡住——它就處於一個死鎖狀態。

另一種類型的鎖稱爲‘遞歸鎖’(’recursive mutex’),它允許鎖住這個互斥量的線程鎖住它多次而不被卡住(但是其他嘗試鎖住這個鎖的線程會被卡住)。如果該線程解鎖了這個互斥量,它仍然是鎖住的狀態,直到它解鎖的次數與鎖住的次數是相同的。這與現代的鎖是一樣的——如果你順時針方向轉兩次鎖住了鎖,那麼你開鎖的時候就需要逆時針方向轉兩次。這種類型的互斥量用常量PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP對其賦值。

對一個互斥量上鎖和解鎖

使用函數pthread_mutex_lock()函數來鎖住一個互斥量。這個函數嘗試鎖住一個互斥量,如果其他線程已經鎖住了這個互斥量那麼它會掛起。這種情況下,當那個線程解鎖了這個互斥量後,你的線程纔會鎖住這個鎖。如下給出了鎖住一個互斥量的示例(假定他已經初始化了):

int rc = pthread_mutex_lock(&a_mutex);
if (rc)
{ 
    /* an error has occurred */
    perror("pthread_mutex_lock");
    pthread_exit(NULL);
}
/* mutex is now locked - do your stuff. */
.
.

一個線程做完它的事情(改變變量或者數據結構的值,處理文件或者其他它要做的事情)之後,它應該解鎖該互斥量,使用函數pthread_mutex_unlock()

rc = pthread_mutex_unlock(&a_mutex);
if (rc)
{
    perror("pthread_mutex_unlock");
    pthread_exit(NULL);
}

銷燬一個互斥量

在使用完一個互斥量之後,需要銷燬它。用完表示沒有線程再使用它了。如果僅有一個線程用完,那麼仍然要保持它的有效,因爲其他線程可能會使用它。一旦所有線程都使用完畢,那麼最後一個線程可以使用pthread_mutex_destroy()函數銷燬它:

rc = pthread_mutex_destroy(&a_mutex);

在調用這個函數之後,這個互斥量就不能再用了,除非再次進行初始化。因此,如果一個線程過早地銷燬一個互斥量,其它線程嘗試訪問(鎖住或解鎖)它的時候,這個線程的上鎖或解鎖函數就會返回一個EINVAL錯誤。

使用互斥量——一個完整例子

在瞭解了一個互斥量的所有生命週期之後,那麼來看一個使用互斥量的例子。這個程序描述了兩個員工競選“今日之星”的頭銜,和其所代表的榮譽。爲了快速模擬,我們使用了三個線程:一個提拔Danny爲“今日之星”,一個提拔Moshe爲“今日之星”,第三個線程保證“今日之星”員工的內容是一致的(例如:包含一個員工的數據)。
我們一共提供了兩份代碼,一份使用了互斥量,一份沒有。嘗試一下這兩份代碼來看一下不同之處,這樣就會確信互斥量在多線程環境中是至關重要的。
這兩份代碼分別是employee-withmutex.cemployee-without-mutex.c(代碼請參考原文)


參考:《MultiThreaded-Programming-With-POSIX》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章