iOS線程安全

UIKit是線程不安全的

  • UIKit是線程不安全的,并且這是蘋果有意的設計,主要是為了提升性能。具體原因,下面這篇文章寫得很好:
    線程安全類的設計

這個章節的內容都是這篇文章的節選

  • 最容易犯的錯誤是在后臺線程中對property賦值,比如圖片,因為圖片是在后臺從網絡上獲取的。如果兩個線程同時設置圖片,很可能程序將直接崩潰,因為當前設置的圖片可能會被釋放兩次。由于這是和時機相關的,因此崩潰通常發生在客戶使用時,而并不是在開發過程中。
  • 在大多數情況下,UIKit類只應該在程序的主線程使用。無論是從UIResponder派生的類,還是那些涉及以任何方式操作你的應用程序的用戶界面。
  • 在異步塊操作里一貫使用__weak和不訪問ivars是推薦的方式。
  • 一般情況下,不可變類,像NSArray是線程安全的,而它們的可變的變體,像NSMutableArray則不是。好的做法是寫一些像return [array copy]來確保返回的對象實際上是不可變的。
  • 單獨使用原子屬性不會讓你的類線程安全的。它只會保護你在setter中免受競態條件(race conditions),但不會保護你的應用程序邏輯。
  • 在試圖做線程安全之前,認真考慮是否是必要的。請確保它不是過早的優化。如果它像是一個配置類,考慮線程安全是沒有意義的。更好的方法是拋出一些斷言來確保它的正確使用:
void PSPDFAssertIfNotMainThread(void) { 
    NSAssert(NSThread.isMainThread, 
      @"Error: Method needs to be called on the main thread. %@", 
      [NSThread callStackSymbols]); 
} 
  • 一個好的方法是使用一個并行dispatch_queue為讀/寫鎖,以最大限度地提高性能,并嘗試只鎖定那些真正需要的地方。
// header 
@property (nonatomic, strong) NSMutableSet *delegates; 
 
// in init 
_delegateQueue = dispatch_queue_create("com.PSPDFKit.cacheDelegateQueue", 
  DISPATCH_QUEUE_CONCURRENT); 
 
- (void)addDelegate:(id<PSPDFCacheDelegate>)delegate { 
    dispatch_barrier_async(_delegateQueue, ^{ 
        [self.delegates addObject:delegate]; 
    }); 
} 
 
- (void)removeAllDelegates { 
    dispatch_barrier_async(_delegateQueue, ^{ 
        self.delegates removeAllObjects]; 
    }); 
} 
 
- (void)callDelegateForX { 
    dispatch_sync(_delegateQueue, ^{ 
        [self.delegates enumerateObjectsUsingBlock:^(id<PSPDFCacheDelegate> delegate, NSUInteger idx, BOOL *stop) { 
            // Call delegate 
        }]; 
    }); 
} 

_delegateQueue的類型是 dispatch_queue_t,在其他地方定義,應該是個內部成員變量

  • 除非addDelegate:removeDelegate:每秒被調用上千次,否則下面是更簡潔的方法:
// header 
@property (atomic, copy) NSSet *delegates; 
 
- (void)addDelegate:(id<PSPDFCacheDelegate>)delegate { 
    @synchronized(self) { 
        self.delegates = [self.delegates setByAddingObject:delegate]; 
    } 
} 
 
- (void)removeAllDelegates { 
    self.delegates = nil; 
} 
 
- (void)callDelegateForX { 
    [self.delegates enumerateObjectsUsingBlock:^(id<PSPDFCacheDelegate> delegate, NSUInteger idx, BOOL *stop) { 
        // Call delegate 
    }]; 
} 

實際的例子

在weex的SDK中有線程安全的字典和數組,以字典為例:

  • 采用繼承現有的字典方式
/**
 *  @abstract Thread safe NSMutableDictionary
 */
@interface WXThreadSafeMutableDictionary<KeyType, ObjectType> : NSMutableDictionary

@end
  • 這個可以討論,可以直接從NSObject過來,不過要重新設計一下對外的接口。本人更傾向于這種組合模式,接口可以自定義,按照需求來,用特殊的名字,防止使用過度。更有定制化的味道。比如key規定為NSString類型
  • 用繼承的方式,直接重寫父類的方法。這種方式是接口更通用,而且不用自己想接口的名字和調用方式。使用起來,跟普通的字典沒什么兩樣。使用起來更方便。

  • 內部用了一個隊列,額外包含了一個字典

@interface WXThreadSafeMutableDictionary ()

@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSMutableDictionary* dict;

@end
  • 是并行隊列,用到了指針地址作為名字的一部分,保證唯一性
- (instancetype)initCommon {
    self = [super init];
    if (self) {
        NSString* uuid = [NSString stringWithFormat:@"com.taobao.weex.dictionary_%p", self];
        _queue = dispatch_queue_create([uuid UTF8String], DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

NSStringUTF8String是不一樣的,GCDc函數

  • 有些方法用同步執行的方式實現“線程安全”
- (id)objectForKey:(id)aKey {
    __block id obj;
    dispatch_sync(_queue, ^{
        obj = _dict[aKey];
    });
    return obj;
}
  • 有些方法用異步柵欄實現“線程安全”
- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey {
    aKey = [aKey copyWithZone:NULL];
    dispatch_barrier_async(_queue, ^{
        _dict[aKey] = anObject;
    });
}
  • copy給的是內部成員變量字典的副本,并不是真正自己的副本。這個就是本人不大喜歡繼承的原因。(很多時候,組合比繼承要好理解一點)
- (id)copy {
    __block id copyInstance;
    dispatch_sync(_queue, ^{
        copyInstance = [_dict copy];
    });
    return copyInstance;
}

同步機制

保證線程安全的措施。下面兩篇文章不錯:
iOS中保證線程安全的幾種方式與性能對比
iOS開發里的線程安全機制

Foundation對象

  • 使用@synchronized關鍵字
  • 使用NSLock
  • 使用NSRecursiveLock
  • 使用NSConditionLock
  • 使用NSCondition

GCD方式

  • dispatch_semaphore
  • pthread_mutex
  • OSSpinLock

小結

  • 單例的時候,就用dispatch_once,就不要用@synchronized關鍵字
  • 一般情況下,使用@synchronized(self)關鍵字,雖然性能耗一點,大多數情況下夠用了,簡單好用
  • 接下來,考慮的是NSOperationNSOperationQueue,并發數為1就是串行隊列,線程安全;或者設置下依賴,不用管同步異步,串行并行等事情
  • 再接下來,可以考慮用GCD的同步執行和柵欄,就像前面例子提到的那樣,weex中用的那樣,線程安全的字典或者數組,可以作為參考
  • 再接下來,可以考慮用NSLockNSRecursiveLockNSConditionLock,用到這些的,已經算比較復雜了,要注意死鎖問題,時序是否按照預想的發展。沒有必要,還是不要用比較好。
  • 如果考慮性能問題,那就用最后的大招dispatch_semaphore
    pthread_mutexOSSpinLock
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • iOS線程安全的鎖與性能對比 一、鎖的基本使用方法 1.1、@synchronized 這是我們最熟悉的枷鎖方式,...
    Jacky_Yang閱讀 2,265評論 0 17
  • GCD簡介 GCD 是 libdispatch 的市場名稱,而 libdispatch 作為 Apple 的一個庫...
    獨木舟的木閱讀 1,302評論 0 5
  • Object C中創建線程的方法是什么?如果在主線程中執行代碼,方法是什么?如果想延時執行代碼、方法又是什么? 1...
    AlanGe閱讀 1,793評論 0 17
  • 多線程的安全隱患 當多個線程訪問同一塊資源時,很容易引發數據錯亂和數據安全問題 安全隱患解決 方案一:使用“同步塊...
    AYuan_閱讀 748評論 0 3
  • 為什么CFRunLoopRef是線程安全的,而基于此的NSRunLoop卻不是線程安全的呢? 線程安全時多線程領域...
    小貓仔閱讀 1,229評論 2 4