iOS 線程同步

多線程相關的概念

  • 時間片輪轉調度算法:是目前操做系統中大量使用的線程管理方式,大體就是操做系統會給每一個線程分配一段時間片(一般100ms左右),這些線程都被放在一個隊列中,CPU只須要維護這個隊列,當隊首的線程時間片耗盡就會被強制放到隊尾等待,而後提取下一個隊首線程執行
  • 原子操做:「原子」通常指最小粒度,不可分割;原子操做也就是不可分割,不可中斷的操做
  • 臨界區 :每一個進程中訪問臨界資源的那段代碼稱爲臨界區(Critical Section)(臨界資源是一次僅容許一個進程使用的共享資源)。每次只准許一個進程進入臨界區,進入後不容許其餘進程進入。不管是硬件臨界資源,仍是軟件臨界資源,多個進程必須互斥地對它進行訪問
  • 忙等(busy-waiting): 試圖進入臨界區的線程,佔着CPU而不釋放的狀態
  • 睡眠(sleep-waiting):試圖進入臨界區的線程,會進入睡眠狀態,主動讓出時間片,不會再佔着CPU而不釋放
  • 上下文切換(Context Switch):當線程進入睡眠(sleep-waiting)的時候,cpu的核心會進行上下文切換,將該線程置於等待隊列中,而其餘線程就會繼續執行任務,上下文切換須要花費時間
  • 鎖的擁有者(Lock Ownership):若是鎖沒有擁有者,則當它被某一條線程獲取時,其餘任意一條線程均可以對它進行解鎖;若是鎖只能有單一的擁有者,則當它被某一條線程獲取時,只有這條線程能夠對它進行解鎖;若是鎖能夠有多個擁有者,則它能夠同時被某多條線程獲取
  • 死鎖:指兩個或兩個以上的進程(線程)在運行過程當中因爭奪資源而形成的一種僵局(Deadly-Embrace) ) ,若無外力做用,這些進程(線程)都將沒法向前推動。通常在得到鎖的線程中再次進行加鎖就會發生死鎖
  • 飢餓(Starvation):指一個進程一直得不到資源

Lock Ownership

線程同步方案

要保證線程安全,就必需要線程同步,而在iOS中線程同步的方案有:html

  • 原子操做
  • 信號量
  • GCD串行隊列

原子操做

在iOS中,原子操做能夠保證屬性在單獨的setter或者getter方法中是線程安全的,可是不能保證多個線程對同一個屬性進行讀寫操做時,能夠獲得預期的值,也就是原子操做不保證線程安全,例如:ios

// 共享資源name
@property (copy, atomic) NSString *name;
// 初始化
self.name = @"A";

// 線程2進行寫操做,是原子操做,不能夠分割的
self.name = @"B";

// 線程3進行寫操做,是原子操做,不能夠分割的
self.name = @"C";

// 線程4進行讀操做,是原子操做,不能夠分割的,但這時候存在三種可能
self.name == @"A";
self.name == @"B";
self.name == @"C";
複製代碼

OC的原子操做

在OC中,能夠在設置屬性的時候,使用atomic來設置原子屬性,保證屬性settergetter的原子性操做,底層是在gettersetter內部使用os_unfair_lock加鎖objective-c

@property (copy, atomic) NSString *name;
複製代碼

Swift的原子操做

在Swift中,原生沒有提供原子操做,可使用DispatchQueue的同步函數來達到一樣的效果算法

class Person {
  // 建立一個隊列
  let queue = DispatchQueue(label: "Person")

  // 私有化須要原子操做的屬性
  private var _name: String = ""

  // 向外界暴露的屬性,把它的get和set方法都設置爲同步操做,其實是對_name進行操做,這樣就能夠間接的對name進行原子操做
  var name: String {
      get {
          return queue.sync {
              _name
          }
      }
      set {
          return queue.sync {
              _name = newValue
          }
      }
  }
}
複製代碼

信號量(Semaphore)

  • 信號量(semaphore)是非負整型變量,在初始化時設置一個值value,用來控制線程併發訪問的最大數量,當value == 1的時候,就能夠實現線程同步
  • 信號量有兩個原子操做:wait()signal()
    • wait():當 value > 0,就將 value 減 1 並立刻返回;當 value == 0,那當前線程就會睡眠,直到其餘線程調用signal()把 value 加 1,當前線程恢復,而後將value 減1並返回
    • signal() :將 value 加 1
    • 若是初始化的時候 value 爲 0, 那麼調用 wait() 方法就會立刻掛起當前線程,直到別的線程調用了 signal() 方法,纔會恢復
  • 被阻塞線程會進入睡眠狀態
  • 信號量不支持遞歸
  • 信號量沒有擁有者(Owner),意味着能夠在一條線程進行wait()操做,在另一條線程進行signal() 操做
  • 在iOS中用dispatch_semaphore來使用信號量,也是 GCD 用來同步的一種方式
// 初始化一個值爲5的信號量,能夠同時有5條線程訪問臨界區,其餘線程則進入睡眠狀態
dispatch_semaphore_t semaphore = dispatch_semaphore_create(5);


// wait
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

// 臨界區...

// signal
dispatch_semaphore_signal(semaphore);
複製代碼

GCD串行隊列

  • 使用GCD串行隊列也能夠達到同步的效果,配合sync函數就是在當前線程執行任務
  • GCD串行隊列有單一的擁有者,就是一個串行隊列有對應的線程
dispatch_queue_t queue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    // 臨界區...
});
複製代碼

OSSpinLock

  • OSSpinLock是一種"自旋鎖"。自旋鎖是一種特殊互斥鎖,當一個線程須要獲取自旋鎖時,若是該鎖已經被其餘線程佔用,那麼會一直去請求鎖,進入忙等(busy-waiting)狀態,因此會一直佔用CPU
  • 因爲自旋鎖在等待鎖的時候線程一直處於忙等狀態,而不用進入睡眠,因此不用進行上下文切換,自旋鎖的效率遠高於互斥鎖
  • 自旋鎖適用於
    • 預計線程等待鎖的時間很短
    • 臨界區常常訪問,但競爭狀況不多發生
  • 自旋鎖不安全,會出現優先級反轉問題:若是一個低優先級的線程得到鎖並訪問共享資源,這時一個高優先級的線程也嘗試得到這個鎖,它會處於忙等狀態從而佔用大量 CPU時間片。此時低優先級線程沒法與高優先級線程爭奪 CPU 時間片,從而致使完成任務而沒法釋放鎖
  • 在iOS 10及以上被廢棄
#import <libkern/OSAtomic.h>

OSSpinLock lock = OS_SPINLOCK_INIT;

// 加鎖
OSSpinLockLock(&lock);

// 臨界區...

// 解鎖
OSSpinLockUnlock(&lock);
複製代碼

os_unfair_lock

  • os_unfair_lock用於取代不安全的OSSpinLock ,iOS 10開始支持,當一條線程等待鎖的時候會進入睡眠,再也不消耗CPU時間,當其餘線程解鎖之後,操做系統會激活線程
  • os_unfair_lock有單一的擁有者
  • 這是一種不公平鎖。在公平鎖中,多個線程同時競爭這個鎖的時候, 會考慮公平性儘量的讓不一樣的線程得到鎖,這樣會頻繁進行上下文切換,犧牲性能。而在不公平鎖中,系統爲了減小上下文切換,當前擁有鎖的線程有可能會再次得到鎖,但這樣作可能會讓其餘線程等待更長時間,形成飢餓
#import <os/lock.h>

os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;


// 加鎖
os_unfair_lock_lock(&lock);

// 臨界區...

// 解鎖
os_unfair_lock_unlock(&lock);
複製代碼

互斥鎖

  • 互斥鎖是能夠看做是一種特殊的信號量,當一條線程等待鎖的時候會進入睡眠狀態
  • 互斥鎖阻塞的過程分兩個階段,第一階段是會先空轉,能夠理解成跑一個 while 循環,不斷地去申請加鎖,在空轉必定時間以後,線程會進入睡眠狀態,讓出時間片,此時線程就不佔用CPU時間片,等鎖可用的時候,這個線程會當即被喚醒

pthread_mutex

pthread 表示POSIX thread,是POSIX標準的unix多線程庫,定義了一組跨平臺的線程相關的API。pthread_mutex是一種用 C 語言實現的互斥鎖,有單一的擁有者swift

#import <pthread.h>

// 靜態初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;


// 動態初始化
// 初始化屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化鎖
pthread_mutex_t mutex;
pthread_mutex_init(mutex, &attr);
// 銷燬屬性
pthread_mutexattr_destroy(&attr);


// 加鎖
pthread_mutex_lock(&mutex);

// 臨界區...

// 解鎖
pthread_mutex_unlock(&mutex);

// 銷燬鎖
pthread_mutex_destroy(&_mutex);
複製代碼

NSLock

  • NSLock 是以OC對象的形式對pthread_mutex的封裝,屬性爲 PTHREAD_MUTEX_ERRORCHECK,它會損失必定性能換來錯誤提示
  • NSLockpthread_mutex略慢的緣由在於它須要通過方法調用,同時因爲緩存的存在,屢次方法調用不會對性能產生太大的影響
  • NSLock有單一的擁有者
NSLock *lock = [[NSLock alloc] init];

// 加鎖
[lock lock];

// 臨界區...

// 解鎖
[lock unlock];
複製代碼

遞歸鎖

遞歸鎖是一種特殊互斥鎖。遞歸鎖容許單個線程在釋放以前屢次獲取鎖,其餘線程保持睡眠狀態,直到鎖的全部者釋放鎖的次數與獲取它的次數相同。遞歸鎖主要在遞歸迭代中使用,但也可能在多個方法須要單獨獲取鎖的狀況下使用。緩存

pthread_mutex(Recursive)

pthread_mutex 支持遞歸鎖,只要把 attr 的類型改爲 PTHREAD_MUTEX_RECURSIVE 便可,它有單一的擁有者安全

#import <pthread.h>

// 初始化屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化鎖
pthread_mutex_t mutex;
pthread_mutex_init(mutex, &attr);
// 銷燬屬性
pthread_mutexattr_destroy(&attr);


// 加鎖
pthread_mutex_lock(&_mutex);


// 臨界區...
// 在同一個線程中能夠屢次獲取鎖

// 解鎖
pthread_mutex_unlock(&_mutex);


// 銷燬鎖
pthread_mutex_destroy(&_mutex);
複製代碼

NSRecursiveLock

NSRecursiveLock 是以OC對象的形式對pthread_mutex(Recursive)的封裝,它有單一的擁有者多線程

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

// 加鎖
[lock lock];

// 臨界區...
// 在同一個線程中能夠屢次獲取鎖

// 解鎖
[lock unlock];
複製代碼

@synchronized

  • @synchronized是對pthread_mutex(Recursive)的封裝,因此它支持遞歸加鎖
  • 須要傳入一個 OC 對象,能夠理解爲把這個對象當作鎖來使用
  • 實際上它是用objc_sync_enter(id obj)objc_sync_exit(id obj)來進行加鎖和解鎖
  • 底層實現:在底層存在一個全局用來存放鎖的哈希表(能夠理解爲鎖池),對傳入的對象地址的哈希值做爲key,去查找對應的遞歸鎖
  • @synchronized額外還會設置異常處理機制,性能消耗較大
  • @synchronized有單一的擁有者
@synchronized(lock) {
    // 臨界區...
}
複製代碼

條件鎖

條件鎖是一種特殊互斥鎖,須要條件變量(condition variable) 來配合。條件變量有點像信號量,提供了線程阻塞與信號機制,所以能夠用來阻塞某個線程,並等待某個數據就緒,隨後喚醒線程。條件鎖是爲了解決生產者-消費者模型併發

pthread_mutex – 條件鎖

pthread_mutex 配合 pthread_cond_t,能夠實現條件鎖,其中pthread_cond_t沒有擁有者app

#import <pthread.h>

// 初始化鎖
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &NULL);
// 銷燬屬性
pthread_mutexattr_destroy(&attr);

// 初始化條件變量
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);

// 消費者
- (void)remove {
    // 加鎖
    pthread_mutex_lock(&mutex);

    // 先判斷某個條件
    if (self.data.count == 0) {
        // 若是不知足條件,則等待,具體是釋放鎖,用條件變量來阻塞當前線程
        // 當條件知足的時候,條件變量喚醒線程,用鎖加鎖
        pthread_cond_wait(&cond, &mutex);
    }

    [self.data removeLastObject];


    // 解鎖
    pthread_mutex_unlock(&mutex);
}


// 生產者
- (void)add
{
    // 加鎖
    pthread_mutex_lock(&mutex);
    

    [self.data addObject:@"Test"];
    
    // 信號
    // 條件變量喚醒阻塞的線程,用鎖加鎖
    pthread_cond_signal(&cond);
    
    // 廣播
    // pthread_cond_broadcast(&cond);
    
 	// 解鎖
    pthread_mutex_unlock(&mutex);
}


// 銷燬
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
複製代碼

NSCondition

NSRecursiveLock 是以OC對象的形式對pthread_mutexpthread_cond_t進行了封裝,NSCondition沒有擁有者

NSCondition *condition = [[NSCondition alloc] init];

// 消費者
- (void)remove
{
    [condition lock];

    
    if (self.data.count == 0) {
        // 若是不知足條件,則等待,具體是釋放鎖,用條件變量來阻塞當前線程
        // 當條件知足的時候,條件變量喚醒線程,用鎖加鎖
        [condition wait];
    }
    
    [self.data removeLastObject];
    
    [condition unlock];
}


// 生產者
- (void)add
{
    [condition lock];
    
    
    [self.data addObject:@"Test"];
    
    // 信號
    // 條件變量喚醒阻塞的線程,用鎖加鎖
    [condition signal];
    
    
    [condition unlock];
}
複製代碼

NSConditionLock

NSConditionLock是對NSCondition的進一步封裝,能夠設置條件變量的值。經過改變條件變量的值,可使任務之間產生依賴關係,達到使任務按照必定的順序執行,它有單一的擁有者(不肯定)

// 初始化設置條件變量的爲1,若是不設置則默認爲0
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:1];


// 消費者
- (void)remove
{
    // 當條件變量爲2的時候加鎖,不然等待
    [lock lockWhenCondition:2];
    
    [self.data removeLastObject];
    
    // 直接解鎖
    [lock unlock];
}


// 生產者
- (void)add
{
    // 直接加鎖
    [lock lock];
    
    
    [self.data addObject:@"Test"];
    
    
    // 解鎖並讓條件變量爲2
    [lock unlockWithCondition:2];
}
複製代碼

讀寫鎖

讀寫鎖是一種特殊互斥鎖,提供"多讀單寫"的功能,多個線程能夠同時對共享資源進行讀取,可是同一時間只能有一條線程對共享資源進行寫入

pthread_rwlock

pthread_rwlock 有多個擁有者

#import <pthread.h>

// 初始化
pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;


// 讀操做
- (void)read {
    pthread_rwlock_rdlock(&lock);

    // 臨界區...
  
    pthread_rwlock_unlock(&lock);
}

// 寫操做
- (void)write
{
    pthread_rwlock_wrlock(&lock);
  
    // 臨界區...

    pthread_rwlock_unlock(&lock);
}

// 銷燬
- (void)dealloc
{
    pthread_rwlock_destroy(&lock);
}
複製代碼

GCD的Barrier函數

  • GCD的Barrier函數也能夠實現"多讀單寫"的功能
  • Barrier函數的做用是:等其餘任務執行完畢,纔會執行任務本身的任務;會執行完畢本身的任務,纔會繼續執行其餘任務
  • 這個函數傳入的併發隊列必須是本身經過dispatch_queue_cretate建立的,若是傳入的是一個串行或是一個全局的併發隊列,那這個函數便等同於dispatch_async函數的效果
dispatch_queue_t queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);


dispatch_async(queue, ^{
    // 讀
});

dispatch_async(queue, ^{
    // 讀
});


dispatch_barrier_async(queue, ^{
    // 寫
});

dispatch_async(queue, ^{
    // 讀
});
複製代碼

性能

性能從高到底分別是:

  • os_unfair_lock
  • OSSpinLock
  • dispatch_semaphore
  • pthread_mutex
  • GCD串行隊列
  • NSLock
  • NSCondition
  • pthread_mutex(recursive)
  • NSRecursiveLock
  • NSConditionLock
  • @synchronized

總結:

  • OSSpinLockos_unfair_lock性能很高,可是一個是已經廢棄,一個是低級鎖,蘋果不建議使用低級鎖
  • dispatch_semaphorepthread_mutex也具備不錯的性能,NSLockpthread_mutex的封裝,性能上接近
  • 我的建議在OC中直接使用面向對象的NSLock,而在Swift中使用GCD串行隊列

參考文章

蘋果官方文檔

白夜追兇,揭開iOS鎖的祕密

起底多線程同步鎖(iOS)

深刻理解 iOS 開發中的鎖

相關文章
相關標籤/搜索