執行緒同步及執行緒鎖

chaocai發表於2018-01-01

1 資源競爭與執行緒同步

競爭態條件下,多個執行緒對同一競態資源的搶奪會引發執行緒安全問題。競態資源是對多個執行緒可見的共享資源,主要包括全域性(非const)變數、靜態(區域性)變數、堆變數、資原始檔等。

執行緒之間的競爭,可能帶來一些列問題:

  • 執行緒在操作某個共享資源的過程中被其他執行緒所打斷,時間片耗盡而被迫切換到其他執行緒
  • 共享資源被其他執行緒修改後的不到告知,造成執行緒間資料不一致
  • 由於編譯器優化等原因,若干操作指令的執行順序被打亂,造成結果的不可預期

1.1 原子操作

原子操作,即不可分割開的操作;該操作一定是在同一個cpu時間片中完成,這樣即使執行緒被切換,多個執行緒也不會看到同一塊記憶體中不完整的資料。

原子表示不可分割的最小單元,具體來說是指在所處尺度空間或者層(layer)中不能觀測到更為具體的內部實現與結構。對於計算機程式執行的最小單位是單條指令。我們可以通過參考各種cpu的指令操作手冊,用其彙編指令編寫原子操作。而這種方式太過於低效。

某些簡單的表示式可以算作現代程式語言的最小執行單元 某些簡單的表示式,其實編譯之後的得到的彙編指令,不止一條,所以他們並不是真正意義原子的。以加法指令操作實現 x += n為例 ,gcc編譯出來的彙編形式上如下:

...
movl 0xc(%ebp), %eax
addl $n, %eax
movl %eax, 0xc(%ebp)
...
複製程式碼

而將它放在所執行緒環境之中,顯然也是不安全的:

dispatch_group_t group = dispatch_group_create();
    __block int  i = 1;
for (int k = 0; k < 300; k++) {
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        ++i;
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        --i;
        dispatch_group_leave(group);
    });
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"----result=%d  i=%d",self.pro1,i);
});
複製程式碼

上述例子中,全域性變數i理論上應該最後得到1,而實際上卻機率性得到0,-1,2,-2,1。

為了避免錯誤,很多作業系統或編譯器都提供了一些常用原子化操作的內建函式或API,包括把一些實際是多條指令的常用表示式。上述操作中,將i++/i--,替換為 OSAtomicIncrement32(&i) / OSAtomicDecrement32(&i) ,將得到預期的結果1

下邊列舉了不同平臺上原子操作API的部分例子

windows API macOS/iOS API gcc內建函式 作用
InterlockExchange OSAtomicAdd32 AO_SWAP 原子的交換兩個值
InterlockDecrement OSAtomicDecrement32 AO_DEC 原子的減少一個值
InterlockIncrement OSAtomicIncrement32 AO_INC 原子的增加一個值
InterlockXor OSAtomicXor32 AO_XOR 原子的進行異或

在OC中,屬性變數的atomoc修飾符,起到的作用跟上述API相似,編譯器會通過鎖定機制確保所修飾變數的原子性,而且它是預設情況下新增的。而在實際應用場景中,在操作屬性值時一般會包含三步(讀取、運算、寫入),即便寫操作是原子,也不能保證執行緒安全。而ios中同步鎖的開銷很大(macOS中沒有類似問題),所以一般會加上nonatomic修飾。

@property (nonatomic,assign)int pro1;
複製程式碼

在實際業務中,通常是給核心業務程式碼加同步鎖,使其整體變為原子的,而不是針對具體的屬性讀寫方法。

1.2 可重入與執行緒安全

函式被重入 一個程式被重入,表示這個函式沒有執行完成,由於外部因數或內部呼叫,又一次進入函式執行。函式被重入分兩種情況

  • 多個執行緒同時執行這個函式
  • 函式自身(可能是經過多層呼叫之後)呼叫自身

可重入 一個函式稱為可重入的,表明該函式被重入之後沒有產生任何不良後果。 可重入函式具備以下特點:

  • 不使用任何區域性(靜態)非const變數
  • 不使用任何區域性(靜態)或全域性的非const變數的指標
  • 僅依賴呼叫方法提供的引數
  • 不依賴任何單個資源提供的鎖(互斥鎖等)
  • 不呼叫任何不可重入的函式

可重入是併發的強力保障,一個可重入函式可以在多執行緒環境下放心使用。也就是說在處理多執行緒問題時,我們可以講程式拆分為若干可重入的函式,而把注意的焦點放在可重入函式之外的地方。

函數語言程式設計正規化中,由於整個系統不需要維護多餘資料變數,而是狀態流方式。所以可以認為全是由一些可重入的函式組成的。所以函數語言程式設計在高併發程式設計中有其先天的優勢。

1.3 CPU的過度優化

1.3.1 亂序優化與記憶體屏障

cpu有動態排程機制,在執行過程中可能因為執行效率交換指令的順序。而一些看似獨立的變數實際上是相互影響,這種編譯器優化會導致潛在不正確結果。

面對這種情況我們一般採用記憶體屏障(memory barrier)。其作用就相當於一個柵欄,迫使處理器來完成位於障礙前面的任何載入和儲存操作,才允許它執行位於屏障之後的載入和儲存操作。確保一個執行緒的記憶體操作總是按照預定的順序完成。為了使用一個記憶體屏障,你只要在你程式碼裡面需要的地方簡單的呼叫 OSMemoryBarrier() 函式。

class A {
    let lock = NSRecursiveLock()
    var _a : A? = nil
    var a : A? {
        lock.lock()
        if _a == nil {
            let temp = A()
            
            OSMemoryBarrier()
            
            _a = temp
        }
        lock.unlock()
        return _a
    }
}
複製程式碼

值得注意的是,大部分鎖型別都合併了記憶體屏障,來確保在進入臨界區之前它前面的載入和儲存指令都已經完成。

1.3.2 暫存器優化與volatile變數

在某些情況下編譯器會把某些變數載入進入暫存器,而如果這些變數對多個執行緒可見,那麼這種優化可能會阻止其他執行緒發現變數的任何變化,從而帶來執行緒同步問題。

在變數之前加上關鍵字volatile可以強制編譯器每次使用變數的時候都從記憶體裡面載入。如果一個變數的值隨時可能給編譯器無法檢測的外部源更改,那麼你可以把該變數宣告為volatile變數。在許多原子性操作API中,大量使用了volatile 識別符號修飾。譬如 在系統庫中,所有原子性變數都使用了

<libkern/OSAtomic.h>

int32_t	OSAtomicIncrement32( volatile int32_t *__theValue )
複製程式碼

##2.執行緒同步的主要方式--執行緒鎖 執行緒同步最常用的方法是使用(Lock)。鎖是一種非強制機制,每一個執行緒訪問資料或資源之前,首先試圖獲取(Acquireuytreewq)鎖,並在訪問結束之後釋放(release)。在鎖已經被佔用時獲取鎖,執行緒會等待,直到該鎖被釋放。

2.1 互斥鎖(Mutex)

2.1.1 基本概念

互斥鎖 是在很多平臺上都比較常用的一種鎖。它屬於sleep-waiting型別的鎖。即當鎖處於佔用狀態時,其他執行緒會掛起,當鎖被釋放時,所有等待的執行緒都將被喚醒,再次對鎖進行競爭。在掛起與釋放過程中,涉及使用者態與核心態之間的context切換,而這種切換是比較消耗效能的。

互斥鎖和二元訊號量很相似,唯一不同是隻能由獲取鎖的執行緒釋放而不能假手於人。在某些平臺中,他是用二元訊號量實現的。關於訊號量,我們將在2.3中詳細介紹。

互斥鎖可以是多程式共享的,也可以是程式內執行緒可見的。它可以分為分為普通鎖、檢錯鎖、遞迴鎖。讓我們通過pthread中的pthread_mutex,來詳細瞭解互斥鎖的一些用法及注意事項。

2.1.2 pthread_mutex

pthread_mutex 是pthread中的互斥鎖,具有跨平臺性質。pthread是POSIX執行緒(POSIX threads)的簡稱,是執行緒的POSIX標準(可移植作業系統介面 Portable Operation System Interface)。POSIX是unix的api設計標準,相容各大主流平臺。所以pthread_mutex是比較低層的,可以跨平臺的互斥鎖實現。

我們先來看看最常規的呼叫方式:

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
block();
pthread_mutex_unlock(&mutex);
複製程式碼

pthread_mutex可以定義它的作用範圍,是多程式共享,還是隻是程式內可見。預設是後者

/**
 PTHREAD_PROCESS_SHARE:該程式與其他程式的同步
 PTHREAD_PROCESS_PRIVATE:同一程式內不同的執行緒之間的同步
**/
pthread_mutexattr_setpshared(&mattr,PTHREAD_PROCESS_PRIVATE);
複製程式碼

pthread_mutex又可分為普通鎖、檢錯鎖、遞迴鎖。可以通過屬性,實現相應的功能。

/*
互斥鎖的型別:有以下幾個取值空間:
PTHREAD_MUTEX_NORMAL 0: 普通鎖(預設)。不提供死鎖檢測。嘗試重新鎖定互斥鎖會導致死鎖。如果某個執行緒嘗試解除鎖定的互斥鎖不是由該執行緒鎖定或未鎖定,則將產生不確定的行為。
 
PTHREAD_MUTEX_ERRORCHECK 1: 檢錯鎖,會提供錯誤檢查。如果某個執行緒嘗試重新鎖定的互斥鎖已經由該執行緒鎖定,則將返回錯誤。如果某個執行緒嘗試解除鎖定的互斥鎖不是由該執行緒鎖定或者未鎖定,則將返回錯誤。
 
PTHREAD_MUTEX_RECURSIVE 2: 巢狀鎖/遞迴鎖,該互斥鎖會保留鎖定計數這一概念。執行緒首次成功獲取互斥鎖時,鎖定計數會設定為 1。執行緒每重新鎖定該互斥鎖一次,鎖定計數就增加 1。執行緒每解除鎖定該互斥鎖一次,鎖定計數就減小 1。 鎖定計數達到 0 時,該互斥鎖即可供其他執行緒獲取。如果某個執行緒嘗試解除鎖定的互斥鎖不是由該執行緒鎖定或者未鎖定,則將返回錯誤。
 
*/
pthread_mutexattr_settype(&mattr ,PTHREAD_MUTEX_NORMAL);
複製程式碼

pthread_mutex還有一種簡便的呼叫方式,使用的是全域性唯一互斥鎖。實驗表明,該鎖是所有屬性都是預設的,程式內可見,型別是普通鎖

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
block();
pthread_mutex_unlock(&mutex);
複製程式碼

同時它還提供了一種非阻塞版本pthread_mutex_trylock。若嘗試獲取鎖時發現互斥鎖已經被鎖定,或則超出了遞迴鎖定的最大次數,則立即返回,不會掛起。只有在鎖未被佔用時才能成功加鎖。

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int res = pthread_mutex_trylock(&mutex);
if(res == 0){
    block();
    pthread_mutex_unlock(&mutex);
}else if(res == EBUSY){
    printf("由於 mutex 所指向的互斥鎖已鎖定,因此無法獲取該互斥鎖。");
}else if (res == EAGAIN){
    printf("由於已超出了 mutex 的遞迴鎖定最大次數,因此無法獲取該互斥鎖。");
}
複製程式碼

2.1.3 NSLock與NSRecursiveLock

NSLock是iOS中最常用的一種鎖,對應著普通型別的互斥鎖。另外一個可遞迴的子類為NSRecursiveLock; 我們先來看看它的官方文件:

An NSLock object can be used to mediate access to an application’s global data or to protect a critical section of code, allowing it to run atomically.

Warning

The NSLock class uses POSIX threads to implement its locking behavior. When sending an unlock message to an NSLock object, you must be sure that message is sent from the same thread that sent the initial lock message. Unlocking a lock from a different thread can result in undefined behavior.
You should not use this class to implement a recursive lock. Calling the lock method twice on the same thread will lock up your thread permanently. Use the NSRecursiveLock class to implement recursive locks instead.

Unlocking a lock that is not locked is considered a programmer error and should be fixed in your code. The NSLock class reports such errors by printing an error message to the console when they occur.
複製程式碼

從文件中我們可以知道:

  • 其實現是基於phthread的
  • 誰持有誰釋放,試圖釋放由其他執行緒持有的鎖是不合法的
  • 如果用在需要遞迴巢狀加鎖的場景時,需要使用其子類NSRecursiveLock。不是所有情況下都會引發遞迴呼叫,而NSLock在效能上要優於NSRecursiveLock。而當我們使用NSLock不小心造成死鎖時,可以嘗試將其替換為NSRecursiveLock。
  • lock與unlock是一一對應的,如果試圖釋放一個沒有加鎖的鎖,會發生異常崩潰。而lock始終等不到對應的unlock會進入飢餓狀態,讓當前執行緒一直掛起

2.1.4 @synchronized

@synchronized(self){
	// your code hear        
};
複製程式碼

@synchronized在執行時會在程式碼塊前面加上objc_sync_enter,程式碼塊最後插入objc_sync_exit。下面是這兩個函式宣告檔案。

/** 
 * Begin synchronizing on 'obj'.  
 * Allocates recursive pthread_mutex associated with 'obj' if needed.
 * 
 * @param obj The object to begin synchronizing on.
 * 
 * @return OBJC_SYNC_SUCCESS once lock is acquired.  
 */
OBJC_EXPORT int
objc_sync_enter(id _Nonnull obj)
    OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);

/** 
 * End synchronizing on 'obj'. 
 * 
 * @param obj The object to end synchronizing on.
 * 
 * @return OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
 */
OBJC_EXPORT int
objc_sync_exit(id _Nonnull obj)
    OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這兩個函式位於runtime/objc-sync.mm中,而且是開源的,我們可以 這裡看到具體的原始碼實現。原始碼中 當你呼叫 objc_sync_enter(obj) 時,它用 obj 記憶體地址的雜湊值查詢合適的 SyncData,然後將其上鎖。當你呼叫 objc_sync_exit(obj) 時,它查詢合適的 SyncData 並將其解鎖。 SyncData其實是資料連結串列的一個節點,其資料結構如下:

typedef struct SyncData {
    struct SyncData* nextData;
    id               object;
    int              threadCount;  // number of THREADS using this block
    recursive_mutex_t        mutex;
} SyncData;

typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;
複製程式碼

加鎖程式碼如下:

/ Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");
	
        result = recursive_mutex_lock(&data->mutex);
        require_noerr_string(result, done, "mutex_lock failed");
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

done: 
    return result;
}
// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR, "id2data failed");
        
        result = recursive_mutex_unlock(&data->mutex);
        require_noerr_string(result, done, "mutex_unlock failed");
    } else {
        // @synchronized(nil) does nothing
    }
	
done:
    if ( result == RECURSIVE_MUTEX_NOT_LOCKED )
         result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;

    return result;
}

複製程式碼

可以看到,其核心邏輯是recursive_mutex_lock和recursive_mutex_unlock。這兩個函式在蘋果私有庫當中,具體實現無從而知。但是從文件中得知是基於遞迴型別的pthread_mutex的,這個前文中我們已經討論過。

需要注意的是,所傳入的obj物件主要作用是生成連結串列節點的雜湊索引。該物件的生命週期對程式碼塊及加鎖過程無任何影響。也就是說在傳入之後,如論何時將物件釋放或則置為nil,都是安全的。但是如果傳入一個空物件,將不進行任何的加鎖解鎖操作。

2.2 自旋鎖

自旋鎖 與互斥鎖有點類似,只是自旋鎖被某執行緒佔用時,其他執行緒不會進入睡眠(掛起)狀態,而是一直執行(自旋/空轉)直到鎖被釋放。由於不涉及使用者態與核心態之間的切換,它的效率遠遠高於互斥鎖。

雖然它的效率比互斥鎖高,但是它也有些不足之處:

  • 自旋鎖一直佔用CPU,他在未獲得鎖的情況下,一直執行(自旋),所以佔用著CPU,如果不能在很短的時間內獲得鎖,這無疑會使CPU效率降低。在高併發執行(衝突概率大,競爭激烈)的時候,又或者程式碼片段比較耗時(比如涉及核心執行檔案io、socket、thread等),就容易引發CPU佔有率暴漲的風險
  • 在用自旋鎖時有可能造成死鎖,當遞迴呼叫時有可能造成死鎖。
  • 自旋鎖可能會引起優先順序反轉問題。具體來說,如果一個低優先順序的執行緒獲得鎖並訪問共享資源,這時一個高優先順序的執行緒也嘗試獲得這個鎖,自旋鎖會處於忙等狀態從而佔用大量 CPU。此時低優先順序執行緒無法與高優先順序執行緒爭奪 CPU 時間,從而導致任務遲遲完不成、無法釋放 lock。自旋鎖OSSpinLock由於上述優先順序反轉問題,在新版iOS已經不在保證安全,除非開發者能保證訪問鎖的執行緒全部都處於同一優先順序,否則 iOS 系統中所有型別的自旋鎖都不能再使用了。在ios10中建議替換為os_unfair_lock

因此我們要慎重使用自旋鎖,自旋鎖只有在核心可搶佔式或SMP的情況下才真正需要,在單CPU且不可搶佔式的核心下,自旋鎖的操作為空操作。自旋鎖適用於鎖使用者保持鎖時間比較短的情況下。

#import <libkern/OSAtomic.h>

OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
OSSpinLockUnlock(&lock);
複製程式碼

2.3 訊號量

訊號量(Semaphore),有時被稱為訊號燈,是在多執行緒環境下使用的一種設施, 它負責協調各個執行緒, 以保證它們能夠正確、合理的使用公共資源。

訊號量可以分為幾類:

  • 二進位制訊號量(binary semaphore) / 二元訊號量 :只允許訊號量取0或1值,,只有兩種狀態:佔用與非佔用,其同時只能被一個執行緒獲取。

  • 整型訊號量(integer semaphore):訊號量取值是整數,它可以被多個執行緒同時獲得,直到訊號量的值變為0。

  • 記錄型訊號量(record semaphore):每個訊號量s除一個整數值value(計數)外,還有一個等待佇列List,其中是阻塞在該訊號量的各個執行緒的標識。當訊號量被釋放一個,值被加一後,系統自動從等待佇列中喚醒一個等待中的執行緒,讓其獲得訊號量,同時訊號量再減一。

訊號量通過一個計數器控制對共享資源的訪問,訊號量的值是一個非負整數,所有通過它的執行緒都會將該整數減一。如果計數器大於0,則訪問被允許,計數器減1;如果為0,則訪問被禁止,所有試圖通過它的執行緒都將處於等待狀態。

2.3.1 pthread中的sem_t

他的具體呼叫方式如下:

#include <semaphore.h>

// 初始化訊號量:
// pshared 0程式內所有執行緒可用 1程式間可見
// val     訊號量初始值
// 呼叫成功時返回0,失敗返回-1
int sem_init(sem_t *sem, int pshared, unsigned int val);
        
// 訊號量減1:
// 該函式申請一個訊號量,當前無可用訊號量則等待,有可用訊號量時佔用一個訊號量,對訊號量的值減1。
int sem_wait(sem_t *sem);
        
// 訊號量加1:該函式釋放一個訊號量,訊號量的值加1。
int sem_post(sem_t *sem);
        
// 銷燬訊號量:
int sem_destory(sem_t *sem);

複製程式碼

值得注意的是:上述初始化方法,已經被Apple棄用。在呼叫時基本返回的都是-1,呼叫失敗。其後所有操作也是無效的。搜尋了一下原因,iOS不支援建立無名的訊號量所至,解決方案是造建有名的訊號量。。換成下屬方式,建立一個有名訊號量,訊號量初值為2。使用結束時,呼叫與之對應的unlick方法。

sem_t *semt = sem_open("sem name", O_CREAT,0664,2);


sem_unlink(semt);

複製程式碼

下面我們來看一個簡單的例子。結果很明顯可以看出,某一時刻,只有兩個執行緒在輸出了waite,其他執行緒都被掛起了,當1s後這兩個執行緒都post之後。另外兩個執行緒才被喚醒,繼續執行。

func testSem_t(name:String){
    let semt = sem_open(name, O_CREAT,0664,2)
    if semt != SEM_FAILED {
        for i in 0...5 {
            DispatchQueue.global().async {
            	   sem_wait(semt)
                print("waite \(i)")
                sleep(1)
                sem_post(semt)
                print("post \(i)")
            }
        }
        sem_unlink(name)
    }else{
        if errno == EEXIST {
            print("Semaphore with name \(name) already exists.\n")
        }else{
            print( "Unhandled error: \(errno). name=\(name) \n")
        }
        let newName = name + "\(arc4random()%500)"
        print("new name = \(newName)")
        testSem_t(name: newName)
    }
}
複製程式碼

值得注意的是:當反覆建立同一名字的訊號量時,會返回錯誤。及時重新執行,也會機率性得到錯誤。因此,一方面我們儘量保證每次建立的訊號量名字的唯一性,另一方面在重名返回錯誤時,也應該做相應的處理。本例中處理方式比較簡單,只作為參考。(其中errno為全域性變數,是核心<errno.h>返回的錯誤碼)

2.3.2 dispatch_semaphore

dispatch_semaphore是GCD用於控制多執行緒併發的訊號量,允許通過wait/signal的訊號事件控制併發執行的最大執行緒數,當最大執行緒數降級為1的時候則可當作同步鎖使用,注意該訊號量並不支援遞迴;

2.3.1中的例子用dispatch_semaphore實現,程式碼如下:

let semt = DispatchSemaphore(value: 7)
for i in 0...20 {
    DispatchQueue.global().async {
        print(" \(i)")
        semt.wait()
        print("waite \(i)")
        sleep(1)
        semt.signal()
        print("post \(i) ")
    }
}
複製程式碼

2.3.2 訊號量的用途

  • 二元訊號量相當於互斥鎖,也就是說當訊號量初值為1時,wait相當於lock,signal相當於unlock。而它允許在一個執行緒加鎖在另任一執行緒解鎖,使用更加靈活,而帶來的不確定性則相應增加。

下述程式碼中,執行緒A將等執行緒B呼叫之後再逐一執行。如果換成NSLock理論上由其他執行緒是不允許的,但執行結果一切正常。而換成NSRecursiveLock遞迴鎖,所有加鎖操作將失效,執行緒不會掛起。用pthread_mutex,也是在設定屬性為可遞迴時,加鎖才會失效。(普通互斥鎖可能是由訊號量實現的,具體原因不明,但不建議這樣使用。)

let semt = DispatchSemaphore(value: 1)
let q1 = DispatchQueue(label:"A")
let q2 = DispatchQueue(label:"B")
for i in 0...20 {
   q1.async {
        print(" \(i)")
        semt.wait()
        print("waite \(i)")
    }
    q2.asyncAfter(deadline: .now() + .seconds(i * 1)){
        semt.signal()
        print("post \(i) ")
    }
}
複製程式碼
  • 控制某個程式碼塊的最大併發數。通過設定訊號量的初值,很容易實現某一段程式碼片段的執行的併發數。或者說控制某個資源最大同時訪問量。

  • 當訊號量的值為0,而waite/signal分屬不同執行緒時,可以適用於經典的生產者-消費者模型。即一種一對一的觀測監聽方式。當生產者完成生產後,立刻通知消費者購買。而沒有產品時,消費者只能等待。

var a : Int32 = 0
let semt = DispatchSemaphore(value:0)
for i in 0..<303 {
    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {
        print("task start \(i)  a= \(   OSAtomicAdd32(1, &a)     )")
        semt.signal()
    }
}
for i in 0..<5 {
    DispatchQueue.global().async {
        var count : Int32 = 0
        while(true){
            semt.wait()
            print("obsever \(i) finish a=\( OSAtomicAdd32(-1, &a)  )  一共搶到\( OSAtomicAdd32(1, &count) )")
        }
    }
}
複製程式碼

上述例子中,訊號量的值相當於庫存量。初始庫存為0。生產者一共生產了303件商品,每生產一件都會及時對外銷售。一共有5位消費者(或者經銷商),每當有商品生產出來,都會不同的搶購。從結果中可以看出,由於併發比較高,最大庫存存在波動,但是最終庫存量是0。5位消費者搶購總數等於生產量。而且搶到的總數是一樣的。由於餘數是3,頭三位多搶了一件。

上述生產者,消費者模型更加適合用條件變數來實現。下面讓我們來仔細看看。

2.4 條件變數

條件變數 (Condition Variable) 作為一種同步手段類似於柵欄,允許執行緒以一種無競爭的方式等待某個條件的發生。當該條件沒有發生時,執行緒會一直處於休眠狀態。當被其它執行緒通知條件已經發生時,執行緒才會被喚醒從而繼續向下執行。條件變數是比較底層的同步原語,直接使用的情況不多,往往用於實現高層之間的執行緒同步。使用條件變數的一個經典的例子就是執行緒池(Thread Pool)了。

NSCondition是條件變數在iOS上的一種實現,他是一種特殊型別的鎖,通過它可以實現不同執行緒的排程。一個執行緒被某一個條件所阻塞,直到另一個執行緒滿足該條件從而傳送訊號給該執行緒使得該執行緒可以正確的執行。比如說,你可以開啟一個執行緒下載圖片,一個執行緒處理圖片。這樣的話,需要處理圖片的執行緒由於沒有圖片會阻塞,當下載執行緒下載完成之後,則滿足了需要處理圖片的執行緒的需求,這樣可以給定一個訊號,讓處理圖片的執行緒恢復執行。

func consumer() {
        DispatchQueue.global().async {
            print("start to track")
            while(true){
                self.conditionLock.wait()
                print("in  \(Thread.current)")
            }
        }
    }
    
func producer(){
    let queue1 = DispatchQueue.global()
    for i in 0...5 {
        queue1.asyncAfter(deadline: .now() + .milliseconds(i*300), execute: {
            print(i)
            self.conditionLock.signal()
        })
    }
}
複製程式碼
輸出結果
start to track
0
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
1
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
2
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
3
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
4
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
5
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
複製程式碼

與lock和unlock一一對應相同的是,NSCondition中wait()與signal()也需要一一對應。多個執行緒waite()時,按順序解鎖。多出的wait()執行緒,如果一直等不到signal(),會造成死鎖。同理同一時刻多個執行緒signal(),多餘的將得不到處理。上述例子中,當時間延遲為0時,每次將只會執行一次,因為同一時間只有一把鎖,多餘的鑰匙將被丟棄。

NSConditionLock 是另一種條件變數,唯一不同的是,它可以傳入一個整型數,從而確定具體的條件。也就是具有處理多種條件的能力。與其他鎖一樣,**lock(whenCondition:)unlock(withCondition:)**是一一對應的,並且只有condition值相同時,才可以順利解鎖。由於繼承NSLock,兩者如lock()/unlock()類似,唯一不同是是否指定或修改condition值

let conditionLock = NSConditionLock()
let queue1 = DispatchQueue.global()
for i in 1...5 {
    queue1.asyncAfter(deadline: .now() + .milliseconds(0), execute: {
        conditionLock.lock()
        print("dosomthing thread1 cordition=\(i) ")
        if i == 3 {
            conditionLock.unlock(withCondition:3)
        }
        conditionLock.unlock()
    })
    DispatchQueue.global().async {
        conditionLock.lock(whenCondition:3)
        print("in \(Thread.current)")
        conditionLock.unlock()
    }
}
複製程式碼

上述程式碼機率性得到結果如下

dosomthing thread1 cordition=1 
dosomthing thread1 cordition=2 
dosomthing thread1 cordition=3 
in <NSThread: 0x604000663600>{number = 4, name = (null)}
in <NSThread: 0x604000663700>{number = 5, name = (null)}
dosomthing thread1 cordition=5 
in <NSThread: 0x6040006635c0>{number = 6, name = (null)}
in <NSThread: 0x60000026f340>{number = 3, name = (null)}
dosomthing thread1 cordition=4 
in <NSThread: 0x600000275780>{number = 7, name = (null)} 
複製程式碼

上述程式碼中,多個執行緒等到condition=3後才等以執行。

###2.4 讀寫鎖 讀寫鎖 從廣義的邏輯上講,也可以認為是一種共享版的互斥鎖。如果對一個臨界區大部分是讀操作而只有少量的寫操作,讀寫鎖在一定程度上能夠降低執行緒互斥產生的代價。

對於同一個鎖,讀寫鎖有兩種獲取鎖的方式:共享(share)方式,獨佔(Exclusive)方式。寫操作獨佔,讀操作共享

讀寫鎖狀態 以共享方式獲取 以獨佔方式獲取
自由 成功 成功
共享 成功 等待
獨佔 等待 等待
NSString *path = [[NSBundle mainBundle] pathForResource:@"t.txt" ofType:nil];
    dispatch_group_t group = dispatch_group_create();
    __block double start = CFAbsoluteTimeGetCurrent();
    for (int k = 0; k <= 3000; k++) {
        dispatch_group_enter(group);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self readBookWithPath:path];
            dispatch_group_leave(group);
        });
        dispatch_group_enter(group);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self writeBook:path string:[NSString stringWithFormat:@"--i=%d--",k]];
            dispatch_group_leave(group);
        });
    }
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"----result=%@ time=%f",[self readBookWithPath:path],CFAbsoluteTimeGetCurrent()-start);
    });
複製程式碼
- (NSString *)readBookWithPath:(NSString *)path {
    pthread_rwlock_rdlock(&rwLock);
    NSLog(@"start read ---- ");
    NSString *contentString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   read ---- %@",contentString);
    pthread_rwlock_unlock(&rwLock);
    return contentString;
}
- (void)writeBook:(NSString *)path string:(NSString *)string {
    pthread_rwlock_wrlock(&rwLock);
    NSLog(@"start wirte ---- ");
    [string writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   wirte ---- %@",string);
    pthread_rwlock_unlock(&rwLock);
}
複製程式碼
輸出結果:
......
2017-12-24 17:24:20.506522+0800 lock[8591:299152] start wirte ----
2017-12-24 17:24:20.507522+0800 lock[8591:299152] end   wirte ---- --i=2998--
2017-12-24 17:24:20.507685+0800 lock[8591:299162] start read ----
2017-12-24 17:24:20.507828+0800 lock[8591:299162] end   read ---- --i=2998--
2017-12-24 17:24:20.507943+0800 lock[8591:299154] start wirte ----
2017-12-24 17:24:20.508872+0800 lock[8591:299154] end   wirte ---- --i=2999--
2017-12-24 17:24:20.509065+0800 lock[8591:299161] start read ----
2017-12-24 17:24:20.509240+0800 lock[8591:299161] end   read ---- --i=2999--
2017-12-24 17:24:20.509358+0800 lock[8591:299157] start wirte ----
2017-12-24 17:24:20.510294+0800 lock[8591:299157] end   wirte ---- --i=3000--
2017-12-24 17:24:20.510443+0800 lock[8591:298979] start read ----
2017-12-24 17:24:20.510582+0800 lock[8591:298979] end   read ---- --i=3000--
2017-12-24 17:24:20.510686+0800 lock[8591:298979] ----result=--i=3000-- time=5.968375
複製程式碼

2.4 臨界區

臨界區 (Critical Section)是相較於互斥鎖更為嚴格的同步手段。只對本程式可見,其他程式試圖獲取是非法的(訊號量和互斥量可以)。獲取鎖被稱為進入臨界區,釋放鎖叫做離開臨界區。除此之外,它具有和互斥鎖相同的性質。

相關文章