C語言中文網 目錄

C語言線程互斥和原子操作

如果多個線程訪問相同的數據,并且它們中至少有一個修改了數據,那么對共享數據的所有訪問必須同步以防止數據競爭。但是,一個正在讀取共享數據的線程可能中斷另一個正在修改相同共享數據的線程,因此,可能導致線程讀取到不一致的數據。

甚至,由于程序在每次執行時系統可能調度不同的線程,導致每次運行程序時錯誤消息只能間歇地反映當時情況,很難在測試中復現錯誤。如例 1 所示,哪怕是自增一個計數器這樣的簡單操作,都可能產生數據競爭。

【例1】沒有同步下的并行存儲訪問
#include <stdio.h>
#include <threads.h>
#define COUNT 10000000L
long counter = 0
void incFunc(void) { for (long i = 0; i < COUNT; ++i)  ++counter; }
void decFunc(void) { for (long i = 0; i < COUNT; ++I)  --counter; }

int main(void)
{
    clock_t cl = clock()
    thrd_t th1, th2;
    if (thrd_create(&th1, (thrd-start_t)incFunc, NULL) != thrd_success
      || thrd_create(&th2, (thrd_start_t)decFunc, NULL) != thrd_success)
    {
        fpintf(stderr,"Error creating thread/n"); return -1;
    }
    thrd_join(th1, NULL);
    thrd_join(th2, NULL);

    printf("Counter: %ld \t", counter);
    printf("CPU time: %ld ms\n", (clock()-cl)*1000L/CLOCKS_PER_SEC);
    return 0;
}

在程序結束時,計數器應為 0。然而,在沒有同步的情況下,結果則不同:程序每次運行時,最終獲得計數器值都是不同的。下面是一個典型的輸出示例:
Counter: -714573              CPU time: 59 ms

為了保障同步,C 標準庫提供了互斥操作(mutex operation)和原子操作(atomic operation)。

互斥

互相排斥(mutex exclusion)技術,簡稱為互斥(mutex),它用于防止多個線程同時訪問共享資源。互斥技術采用一個對象控制獨占訪問權限,該對象稱之為互斥。配合條件變量(condition variable),互斥可以實現廣泛的同步訪問控制。例如,它們允許程序員為數據訪問操作指定執行次序。

在 C 程序中,一個互斥采用類型為 mtx_t 的對象表示,它能在一段時間內被一個線程鎖定,而其他線程必須等待,直到它被解鎖。在頭文件 threads.h 中,包括了關于互斥操作的所有聲明。最重要的互斥函數有:
int mtx_init(mtx_t*mtx,int mutextype);
創建一個互斥,該互斥的屬性由 mutextype 指定。如果成功創建了一個新互斥,函數 mtx_init()會將新互斥寫入由參數 mtx 引用的對象,然后返回宏值 thrd_success。

參數 mutextype 的取值可以是以下 4 個:

mtx_plain
mtx_timed
mtx_plain | mtx_recursive
mtx_timed | mtx_recursive

mtx_plain 表示請求一個簡單的互斥,它既不支持超時也不支持遞歸,而其他 3 個值則表示支持超時和(或)遞歸。

void mtx_destroy(mtx_t*mtx);
銷毀 mtx 引用的互斥,并釋放它的所有資源。

int mtx_lock(mtx_t*mtx);
阻塞正在調用的線程,直到該線程獲得參數 mtx 引用的互斥。除該互斥支持遞歸的情況以外,正在調用的線程不能是已持有該互斥的線程。如果調用成功獲得互斥,則函數返回值 thrd_success,否則,返回值 thrd_error。

int mtx_unlock(mtx_t*mtx);
釋放參數 mtx 引用的互斥。在調用函數 mtx_unlock()之前,調用者必須持有該互斥。如果調用釋放互斥成功,則函數返回值 thrd_success,否則,返回值 thrd_error。

通常情況下,在代碼某個關鍵區間(critical section)的起始點調用函數 mtx_lock(),在其結束點調用函數 mtx_unlock(),在這段區間中只有一個線程執行。

函數 mtx_lock()還有兩個替代的選擇:一個選擇是函數 mtx_trylock(),如果該互斥恰好未被其他任何線程獲取,它則為當前線程獲得互斥,如果該互斥被其他線程獲取,它也不會阻塞當前線程;另一個選擇是函數 mtx_timedlock(),它僅在指定的時間內阻塞線程。所有這些函數都通過其返回值表明調用它們后,是否成功地獲得了互斥。

例 2 中的程序是例 1 的修改版本,它展示了如何使用互斥來消除對變量 counter 的數據競爭。

【例2】在例 1 的程序中添加一個互斥
#include <stdio.h>
#include <threads.h>

#define COUNT 10000000L

long counter = 0;
mtx_t mtx;                              // 為訪問counter而設立的互斥
void incFunc(void)
{
    for (long i = 0; i < COUNT; ++i)
    {  mtx_lock(&mtx); ++counter; mtx_unlock(&mtx); }
}
void decFunc(void)
{
    for (long i = 0; i < COUNT; ++i)
    {  mtx_lock(&mtx); --counter; mtx_unlock(&mtx); }
}
int main(void)
{
    if (mtx_init(&mtx, mtx_plain) != thrd_success)
    {
        fprintf(stderr, "Error initializing the mutex.\n");
        return -1;
    }
    // 如例14-2所示,啟動線程,等待它們完成,打印輸出
    mtx_destroy(&mtx);
    return 0;
}

函數 incFunc()和 decFunc()將不再并行地訪問 counter,因為一次只有其中一個可以鎖定互斥(為保障可讀性,省略錯誤檢查)。現在,在程序結束時,計數器具有正確的值:0。下面是一個典型的輸出示例:
Counter: 0             CPU time: 650 ms

實現同步性需要付出代價。較高的 CPU 時間表明:修改后的程序需要大約 10 倍于原來的時間來運行。其原因是,通過鎖定互斥實現同步性遠比自增和自減一個變量具有更為復雜的操作。在不需要互斥鎖定的情況下,使用原子對象可以獲得更好的性能。

原子對象

原子對象(atomic object)是一個可通過原子操作(atomic operation)被讀取或修改的對象。原子操作是指不能被并行線程中斷的操作。在C11標準下,可以使用類型限定符_Atomic聲明一個原子對象(如果實現版本定義了宏__STDC_NO_ATOMICS__,則表明該實現版本不支持原子操作,自然也不能聲明原子對象)。例如,在例14-2程序中的變量counter可以通過以下方式聲明它為原子對象:
_Atomic long counter = ATOMIC_VAR_INIT(0L);

上述聲明定義了原子化的 long 類型變量 counter,并將其值初始化為 0。在頭文件 stdatomic.h 中定義了宏 ATOMIC_VAR_INIT,以及其他所有用于原子對象的宏、類型和聲明。特別是,stdatomic.h 中還定義了對應于所有整數類型的原子類型縮寫。例如,類型 atomic_uchar 等效于 _Atomic unsigned char。

語法 _Atomic(T)也可用于為給定的非原子類型 T 指定其對應的原子類型。數組和函數類型不能為原子類型。然而,原子類型可以具有不同于其對應的非原子類型的空間大小和對齊方式。

原子操作

讀取或寫入一個原子對象是一個原子操作,也就是說它是不能被中斷的操作。這意味著:不同的線程可以同時訪問一個原子對象而不引起競態條件。對于每個原子對象,對象的所有修改以一個確定的全局化次序執行,這稱為該對象的修改次序(modification order)。

具有結構或聯合類型的原子對象只能被作為一個整體讀取或寫入:為了安全地訪問單個成員,原子結構或聯合應首先復制到等效的非原子對象中。

注意,無論是使用宏 ATOMIC_VAR_INIT,還是通過泛型函數 ATOMIC_INIT(),一個原子對象的初始化不是一個原子操作

原子操作通常用于進行讀-修改-寫操作。例如,后綴自增和自減運算符 ++ 和 --,當它們應用于原子對象時,是原子化的讀-修改-寫操作。同樣,復合賦值運算符,如 +=,當其原子化使用時,它們的左操作數是一個原子對象。

例 1 中的程序可以通過聲明變量 counter 作為原子對象,在不受任何其他影響下執行正確的計數,以最終獲得 0 值。該方案計時結果顯示,使用原子類型變量 counter 比例 2 所使用的互斥方法要快兩倍多。

除了已經提到的運算符,還有許多函數可以執行原子操作,包括 atomic_store()、atomic_exchange()和 atomic_compare_exchange_strong()

原子類型具有無鎖(lock-free)屬性,它表示不使用鎖定和解鎖操作實現對一個原子對象的原子訪問。該方式只需要使用類型 atomic_flag(它是一個結構類型)以確保實現無鎖,atomic_flag 有“設置”和“清除”兩種狀態。宏 ATOMIC_FLAG_INIT 將一個 atomic_flag 對象初始化為“清除”狀態,如以下示例聲明所示:
atomic_flag done = ATOMIC_FLAG_INIT;

C11 提供了函數 atomic_flag_test_and_set()和 atomic_flag_clear(),由此對一個 atomic_flag 對象執行狀態操作。整型原子類型通常也都是無鎖的。要確定一個給定的類型是否是無鎖的,程序可以檢查宏 ATOMIC_type_LOCK_FREE,其中 type 是一個指定整數類型的大寫縮寫,如 BOOL、INT,或 LLONG。

與指針類型對應的宏是 ATOMIC_POINTER_LOCK_FREE。所有這些宏的值可能為 0、1 或 2。值為 0,表示該類型不是無鎖的;值為 1,表示該類型對特定對象是無鎖的;值為 2,表示該類型始終是無鎖的。或者,可以調用泛型函數來確定一個給定的原子對象是否是無鎖的:
_Bool atomic_is_lock_free(const volatile A *obj);

在函數參數聲明中的占位符A代表任一原子類型。因此,參數 obj 為指針,它指向任一給定原子對象。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo