iOS中的鎖——由屬性atomic想到的線程安全

本文不介紹各種鎖的高級用法,只是整理鎖相關的知識點,幫助理解。

鎖的作用

防止在多線程(多任務)的情況下對共享資源(臨界資源)的臟讀或者臟寫。

自旋鎖和互斥鎖

共同點:都能保證同一時刻只能有一個線程操作鎖住的代碼。都能保證線程安全。
不同點

  • 互斥鎖(mutex):當上一個線程的任務沒有執行完畢的時候(被鎖住),那么下一個線程會進入睡眠狀態等待任務執行完畢(sleep-waiting),當上一個線程的任務執行完畢,下一個線程會自動喚醒然后執行任務。
  • 自旋鎖(Spin lock):當上一個線程的任務沒有執行完畢的時候(被鎖住),那么下一個線程會一直等待(busy-waiting),當上一個線程的任務執行完畢,下一個線程會立即執行。
  • 由于自旋鎖不會引起調用者睡眠,所以自旋鎖的效率遠高于互斥鎖
  • 自旋鎖會一直占用CPU,也可能會造成死鎖

自旋鎖有bug! ibireme大神的文章《不再安全的 OSSpinLock》
不同優先級線程調度算法會有優先級反轉問題,比如低優先級獲鎖訪問資源,高優先級嘗試訪問時會等待,這時低優先級又沒法爭過高優先級導致任務無法完成lock釋放不了。

1. 原子操作

  • nonatomic:非原子屬性,非線程安全,適合小內存移動設備
  • atomic:原子屬性,default,線程安全(內部使用自旋鎖),消耗大量資源
    • 單寫多讀,只為setter方法加鎖,不影響getter

    • 相關代碼如下:

      static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) 
      {
         if (offset == 0) {
             object_setClass(self, newValue);
             return;
         }
      
         id oldValue;
         id *slot = (id*) ((char*)self + offset);
      
         if (copy) {
             newValue = [newValue copyWithZone:nil];
         } else if (mutableCopy) {
             newValue = [newValue mutableCopyWithZone:nil];
         } else {
             if (*slot == newValue) return;
             newValue = objc_retain(newValue);
         }
      
         if (!atomic) {
             oldValue = *slot;
             *slot = newValue;
         } else {
             spinlock_t& slotlock = PropertyLocks[slot];
             slotlock.lock();
             oldValue = *slot;
             *slot = newValue;        
             slotlock.unlock();
         }
      
         objc_release(oldValue);
      }
      
      void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
      {
         bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
         bool mutableCopy = (shouldCopy == MUTABLE_COPY);
         reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
      }
      

很容易理解的代碼,可變拷貝和不可變拷貝會開辟新的空間,兩者皆不是則持有(引用計數+1),相比nonatomic只是多了一步鎖操作。

2. synchronized 條件鎖

使用最簡單,性能也最差。

@synchronized(obj) {
    // 內部會添加異常處理,所以耗時
    NSLog(@"自動加鎖,自動解鎖,自動銷毀");
}

obj為該鎖的唯一標識,只有當標識相同時,才為滿足互斥

3. dispatch_semaphore 信號量

用于線程同步,有序訪問。不支持遞歸。

  • 創建信號:dispatch_semaphore_create(long value) 傳入值必須 >=0, 若傳入為 0 則阻塞線程并等待timeout,時間到后會執行其后的語句
  • 等待信號到達:dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) 使得signal-1
  • 發送信號,解除等待狀態:dispatch_semaphore_signal(dispatch_semaphore_t deem)使得signal+1

這里順便寫一下dispatch_barrier,一個dispatch_barrier允許在一個并發隊列(如果是串行隊列或全局隊列相當于dispatch_(a)sync)中創建一個同步點。當在并發隊列中遇到一個barrier,他會延遲執行barrier的block,等待所有在barrier之前提交的blocks執行結束。 這時,barrier block自己開始執行。 之后, 隊列繼續正常的執行操作。
dispatch_barrier_asyncdispatch_barrier_sync的區別在于是否會等待自己的block完成再執行后面的任務。

一個同步訪問網絡的例子
- (void)syncRequestWithUrl:(NSURL*)url {
    NSMutableURLRequest *req = [[NSMutableURLRequest alloc]initWithURL:url];
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        
    __block NSURLSessionTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (!error && data) {

        } else {
            NSLog(@"error: %@",[error description]);
        }
        dispatch_semaphore_signal(semaphore);
    }];
    [dataTask resume];
        
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

4. pthread_mutex 互斥鎖

Facebook的 KVOController 有使用到。(先使用的是OSSpinLock,由于自旋鎖的優先級反轉問題后改用pthread_mutex)

使用需導入頭文件:
#import <pthread.h>

  • 聲明:pthread_mutex_t _mutex;
  • 初始化:pthread_mutex_init(&_mutex, NULL);
  • 加鎖:pthread_mutex_lock(&_mutex);
  • 解鎖:pthread_mutex_unlock(&_mutex);
  • 銷毀:pthread_mutex_destroy(&_mutex);

5. pthread_mutex(recursive) 遞歸鎖

遞歸鎖允許同一個線程在未釋放其擁有的鎖時反復對該鎖進行加鎖操作。不會造成死鎖。

static pthread_mutex_t pLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr并且給它賦予默認
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //設置鎖類型,這邊是設置為遞歸鎖
pthread_mutex_init(&pLock, &attr); // 初始化的時候帶入參數
pthread_mutexattr_destroy(&attr); //銷毀一個屬性對象,在重新進行初始化之前該結構不能重新使用

6. OSSpinLock 自旋鎖

是性能最佳的鎖,但由于線程調度算法(高優先級線程始終會在低優先級線程前執行,一個線程不會受到比它更低優先級線程的干擾)優先級反轉的原因逐漸被其他鎖取代。不支持遞歸。

使用需導入頭文件
#import <libkern/OSAtomic.h>

  • OSSpinLock oslock = OS_SPINLOCK_INIT;:默認值為 0,在 locked 狀態時就會大于 0,unlocked狀態下為 0
  • OSSpinLockLock(&oslock);:上鎖
  • OSSpinLockUnlock(&oslock);:解鎖
  • OSSpinLockTry(&oslock):嘗試加鎖,可以加鎖則立即加鎖并返回YES,反之返回NO

提一下os_unfair_lock,蘋果解決優先級反轉的問題整出的。。
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
os_unfair_lock_unlock(unfairLock);

7. NSLock

Cocoa提供的最基本的鎖對象,實際是在內部封裝了pthread_mutex。對象鎖均實現了NSLocking協議。

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end
  • trylock:能加鎖返回YES并執行加鎖操作,相當于lock,反之返回NO
  • lockBeforeDate:表示會在傳入的時間內嘗試加鎖,若能加鎖則執行加鎖操作并返回YES,反之返回NO

8. NSRecursiveLock 遞歸鎖

NSLock的遞歸版本,解決在循環或遞歸時造成的死鎖問題。使用同上。

9. NSCondition

最基本的條件鎖,底層是通過條件變量(condition variable) pthread_cond_t 來實現,實際上封裝了一個互斥鎖和條件變量。手動控制線程waitsignal

  • wait:進入等待狀態
  • waitUntilDate::讓一個線程等待一定的時間
  • signal:喚醒一個等待的線程
  • broadcast:喚醒所有等待的線程

10. NSConditionLock 條件鎖

API名說明一切...

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end

性能相對較差,但使用NSConditionLock可以處理任務間的依賴關系。

Additional

11. pthread_rwlock 讀寫鎖

讀寫鎖是用來解決文件讀寫問題的,讀操作可以共享,寫操作是排他的,讀可以有多個在讀,寫只有唯一個在寫,同時寫的時候不允許讀。

  • 當讀寫鎖被一個線程以讀模式占用的時候,寫操作的其他線程會被阻塞,讀操作的其他線程還可以繼續進行
  • 當讀寫鎖被一個線程以寫模式占用的時候,寫操作的其他線程會被阻塞,讀操作的其他線程也被阻塞
// 初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 讀模式
pthread_rwlock_wrlock(&lock);
// 寫模式
pthread_rwlock_rdlock(&lock);
// 讀模式或者寫模式的解鎖
pthread_rwlock_unlock(&lock);

對于讀數據比修改數據頻繁的應用,用讀寫鎖代替互斥鎖可以提高效率。因為使用互斥鎖時,即使是讀出數據(相當于操作臨界區資源)都要上互斥鎖,而采用讀寫鎖,則可以在任一時刻允許多個讀出者存在,提高了更高的并發度,同時在某個寫入者修改數據期間保護該數據,以免任何其它讀出者或寫入者的干擾。

12. NSDistributedLock 分布式鎖

Mac開發使用,mark

引用

更詳細的資料參考:

iOS 開發中的八種鎖
iOS中保證線程安全的幾種方式與性能對比
深入理解iOS開發中的鎖

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容

  • 鎖是一種同步機制,用于多線程環境中對資源訪問的限制iOS中常見鎖的性能對比圖(摘自:ibireme): iOS鎖的...
    LiLS閱讀 1,535評論 0 6
  • 線程安全是怎么產生的 常見比如線程內操作了一個線程外的非線程安全變量,這個時候一定要考慮線程安全和同步。 - (v...
    幽城88閱讀 678評論 0 0
  • iOS線程安全的鎖與性能對比 一、鎖的基本使用方法 1.1、@synchronized 這是我們最熟悉的枷鎖方式,...
    Jacky_Yang閱讀 2,246評論 0 17
  • demo下載 建議一邊看文章,一邊看代碼。 聲明:關于性能的分析是基于我的測試代碼來的,我也看到和網上很多測試結果...
    炸街程序猿閱讀 803評論 0 2
  • 前言 iOS開發中由于各種第三方庫的高度封裝,對鎖的使用很少,剛好之前面試中被問到的關于并發編程鎖的問題,都是一知...
    喵渣渣閱讀 3,717評論 0 33