線程同步及線程鎖

1 資源競爭與線程同步

競爭態條件下,多個線程對同一競態資源的搶奪會引起線程安全問題。競態資源是對多個線程可見的共享資源,主要包括全局(非const)變量、靜態(局部)變量、堆變量、資源文件等。ios

線程之間的競爭,可能帶來一些列問題:編程

  • 線程在操做某個共享資源的過程當中被其餘線程所打斷,時間片耗盡而被迫切換到其餘線程
  • 共享資源被其餘線程修改後的不到告知,形成線程間數據不一致
  • 因爲編譯器優化等緣由,若干操做指令的執行順序被打亂,形成結果的不可預期

1.1 原子操做

原子操做,即不可分割開的操做;該操做必定是在同一個cpu時間片中完成,這樣即便線程被切換,多個線程也不會看到同一塊內存中不完整的數據。windows

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

某些簡單的表達式能夠算做現代編程語言的最小執行單元 某些簡單的表達式,其實編譯以後的獲得的彙編指令,不止一條,因此他們並非真正意義原子的。以加法指令操做實現 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。bash

爲了不錯誤,不少操做系統或編譯器都提供了一些經常使用原子化操做的內建函數或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)是相較於互斥鎖更爲嚴格的同步手段。只對本進程可見,其餘進程試圖獲取是非法的(信號量和互斥量能夠)。獲取鎖被稱爲進入臨界區,釋放鎖叫作離開臨界區。除此以外,它具備和互斥鎖相同的性質。

相關文章
相關標籤/搜索