小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

冰凌天發表於2019-03-18

一、多執行緒的安全隱患

  • 資源共享
    • 1塊資源 可能會被多個執行緒共享,也就是多個執行緒可能會訪問同一塊資源
    • 比如多個執行緒訪問同一個物件、同一個變數、同一個檔案
  • 當多個執行緒訪問同一塊資源時,很容易引發資料錯亂和資料安全問題

二、多執行緒安全隱患示例01 – 存錢取錢

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 模擬程式碼如下

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 執行程式, 結果如下

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 正常情況, 應該存5000, 取2500, 所以應該剩3500, 但是結果剩了2500
  • 再次執行模擬

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 可以看到只剩了2000, 這就是多執行緒的安全隱患問題, 是資料錯亂

三、多執行緒安全隱患示例02 – 賣票

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 程式碼模擬如下

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 執行程式, 模擬賣票

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 一共賣出10張, 應該剩餘0張, 但是結果卻剩餘3張, 說明資料出現了錯亂

四、多執行緒安全隱患分析和解決方案

1、多執行緒安全隱患分析

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

2、多執行緒安全隱患的解決方案

  • 解決方案:使用執行緒同步技術(同步,就是協同步調,按預定的先後次序進行)
  • 常見的執行緒同步技術是:加鎖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

五、iOS中的執行緒同步方案

  • iOS中執行緒加鎖有以下幾種方案
OSSpinLock
os_unfair_lock
pthread_mutex
dispatch_semaphore
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized
複製程式碼

六、準備程式碼

  • 將上面的多執行緒安全隱患示例01 – 存錢取錢多執行緒安全隱患示例02 – 賣票程式碼封裝到一個BaseDemo類中, 具體程式碼如下圖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • BaseDemo暴露出五個方法, 兩個測試呼叫, 三個執行緒呼叫
  • 建立AddLockDemo繼承自BaseDemo

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • ViewController中程式碼如下

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

七、OSSpinLock(自旋鎖)

  • OSSpinLock叫做自旋鎖,等待鎖的執行緒會處於忙等(busy-wait)狀態,一直佔用著CPU資源

1、解決存錢取錢賣票的安全隱患

  • 存錢取錢賣票中加入OSSpinLock

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 執行程式, 多次點選螢幕試驗, 都可以發現結果正確

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

2、OSSpinLock目前已經不再安全,可能會出現優先順序反轉問題

  • 一個程式中可能會有多個執行緒, 但是隻有一個CPU
  • CPU給執行緒分配資源, 讓他們穿插的執行, 比如有三個執行緒thread1thread2thread3
  • CPU通過分配, 讓thread1執行一段時間後, 接著讓thread2執行一段時間, 然後再讓thread3執行一段時間
  • 這樣就給了我們有多個執行緒同時執行任務的錯覺
  • 而執行緒是有優先順序的
    • 如果優先順序高, CPU會多分配資源, 就會有更多的時間執行
    • 如果優先順序低, CPU會減少分配資源, 那麼執行的就會慢
  • 那麼就可能出現低優先順序的執行緒先加鎖,但是CPU更多的執行高優先順序執行緒, 此時就會出現類似死鎖的問題
假設通過OSSpinLock給兩個執行緒`thread1`和`thread2`加鎖
thread優先順序高, thread2優先順序低
如果thread2先加鎖, 但是還沒有解鎖, 此時CPU切換到`thread1`
因為`thread1`的優先順序高, 所以CPU會更多的給`thread1`分配資源, 這樣每次`thread1`中遇到`OSSpinLock`都處於使用狀態
此時`thread1`就會不停的檢測`OSSpinLock`是否解鎖, 就會長時間的佔用CPU
這樣就會出現類似於死鎖的問題
複製程式碼

八、os_unfair_lock(互斥鎖)

  • os_unfair_lock用於取代不安全的OSSpinLock, 從iOS10開始才支援
  • 從底層呼叫看, 等待os_unfair_lock鎖的執行緒會處於休眠狀態, 並非忙等
  • 需要匯入標頭檔案#import <os/lock.h>
// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 嘗試加鎖, 如果lcok已經被使用, 加鎖失敗返回false, 如果加鎖成功, 返回true
os_unfair_lock_trylock(&lock);
// 加鎖
os_unfair_lock_lock(&lock);
// 解鎖
os_unfair_lock_unlock(&lock);
複製程式碼

解決存錢取錢賣票的安全隱患

  • 在存錢取錢和賣票中加入os_unfair_lock

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 執行程式, 多次點選螢幕試驗, 都可以發現結果正確

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

九、pthread_mutex

  • mutex叫做互斥鎖,等待鎖的執行緒會處於休眠狀態
  • 需要匯入標頭檔案#import <pthread.h>
// 初始化屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化鎖
pthread_mutex_t pthread;
pthread_mutex_init(&pthread, &attr);
// 銷燬屬性
pthread_mutexattr_destroy(&attr);
// 銷燬鎖
pthread_mutex_destroy(&pthread);
複製程式碼
  • 屬性型別的取值
#define PTHREAD_MUTEX_NORMAL		0
#define PTHREAD_MUTEX_ERRORCHECK	1
#define PTHREAD_MUTEX_RECURSIVE		2
#define PTHREAD_MUTEX_DEFAULT		PTHREAD_MUTEX_NORMAL
複製程式碼

1、解決存錢取錢賣票的安全隱患

  • 匯入標頭檔案, 建立鎖, 加鎖解鎖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 執行程度, 多次點選螢幕試驗, 都可以發現結果正確

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

2、遞迴鎖

  • 定義PthreadTest類繼承自NSObject, 其中recursive是一個遞迴方法

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • ViewController中程式碼如下, 點選螢幕後呼叫PthreadTestrecursive方法

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 點選螢幕, 可以看到發生了死鎖, 這是因為recursive中呼叫recursive, 此時還沒有解鎖, 再次進行加鎖, 所以發生了死鎖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 設定pthread初始化時的屬性型別為PTHREAD_MUTEX_RECURSIVE, 這樣pthread就是一把遞迴鎖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 遞迴鎖允許同一執行緒內, 對同一把鎖進行重複加鎖, 所以可以看到遞迴方法呼叫成功

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

3、條件

  • PthreadTest中程式碼如下

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • ViewController中程式碼如下

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 當點選螢幕時, 會在array中移除最後一個元素新增一個新元素, 程式碼中可以看到, 使用不同執行緒呼叫__remove__add兩個方法

  • 現在的需求是, 只有在array不為空的情況下, 才能執行刪除操作, 如果直接執行, 那麼可能會先呼叫__remove在呼叫__add, 那麼就與需求相違背

  • 所以, 我們可以使用條件對兩個方法進行優化

  • 建立cond

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • array.count == 0時, 是程式進入休眠, 只有當array中新增了新資料後在發起訊號, 將休眠的執行緒喚醒

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 執行程式, 點選螢幕, 可以看到程式先進入__remove方法, 但是卻在__add中新增新元素之後再移除元素

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

十、NSLock、NSRecursiveLock、NSCondition、NSConditionLock

  • NSLockNSRecursiveLockNSConditionNSConditionLock是基於pthread封裝的OC物件

1、NSLock

  • AddLockDemo中程式碼如下, 直接使用NSLock進行加鎖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • ViewController中點選螢幕時呼叫方法

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 執行程式, 點選螢幕, 可以看到結果正確

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 檢視GNUStep中關於NSLock的底層程式碼, 可以看到NSLock是基礎pthread封裝的normal

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

2、NSRecursiveLock

  • PthreadTest中程式碼如下, 使用NSRecursiveLock遞迴函式加鎖解鎖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • ViewController中, 當點選螢幕時呼叫recursive方法

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 執行程式, 點選螢幕, 可以看到遞迴鎖的結果

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 檢視GNUStep中關於NSRecursiveLock的底層程式碼

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

3、NSCondition

  • PthreadTest中程式碼如下, 使用NSCondition加鎖解鎖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • ViewController中, 當點選螢幕時呼叫pthreadTest方法

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 可以看到, 先呼叫了__remove方法, 但是卻在__add中給array新增了新元素之後, 才刪除一個元素

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 檢視GNUStep中關於NSCondition的底層程式碼

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

4、NSConditionLock

  • NSConditionLock是對NSCondition的進一步封裝
@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

// 初始化, 同時設定 condition
- (instancetype)initWithCondition:(NSInteger)condition;

// condition值
@property (readonly) NSInteger condition;

// 只有NSConditionLock例項中的condition值與傳入的condition值相等時, 才能加鎖
- (void)lockWhenCondition:(NSInteger)condition;
// 嘗試加鎖
- (BOOL)tryLock;
// 嘗試加鎖, 只有NSConditionLock例項中的condition值與傳入的condition值相等時, 才能加鎖
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
// 解鎖, 同時設定NSConditionLock例項中的condition值
- (void)unlockWithCondition:(NSInteger)condition;
// 加鎖, 如果鎖已經使用, 那麼一直等到limit為止, 如果過時, 不會加鎖
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 加鎖, 只有NSConditionLock例項中的condition值與傳入的condition值相等時, 才能加鎖, 時間限制到limit, 超時加鎖失敗
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
// 鎖的name
@property (nullable, copy) NSString *name;

@end
複製程式碼
  • 可以使用NSConditionLock設定執行緒的執行順序

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 執行程式, 可以看到列印順序

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

十一、同步佇列解決多執行緒隱患

  • 使用同步佇列, 程式碼如下圖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • ViewController程式碼如下

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 點選螢幕, 可以看到結果正確

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

十二、dispatch_semaphore_t

  • 可以使用dispatch_semaphore_t設定訊號量為1, 來控制同意之間只有一條執行緒能執行, 實際程式碼如下

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 執行程式, 點選螢幕, 可以看到列印結果正確

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

十三、@synchronized

  • @synchronized是對mutex遞迴鎖的封裝
  • 原始碼檢視:objc4中的objc-sync.mm檔案
  • @synchronized(obj)內部會生成obj對應的遞迴鎖,然後進行加鎖、解鎖操作

1、解決多執行緒的安全隱患

  • 使用@synchronized進行加鎖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 執行程式碼, 點選螢幕, 效果如下

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

2、@synchronized底層原理

  • 找到objc_sync_enterobjc_sync_exit兩個函式, 分別用於加鎖和解鎖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 檢視SyncData

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 通過所點進去, 找到recursive_mutex_tt

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 檢視recursive_mutex_tt, 可以看到底層是通過os_unfair_recursive_lock封裝的鎖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 接著檢視通過物件獲取鎖的程式碼

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 找到LIST_FOR_OBJ, 點選檢視

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

  • 可以看到, 通過傳入的物件, 會獲取唯一標識所謂鎖

小碼哥iOS學習筆記第二十天: 多執行緒的安全隱患

十四、iOS執行緒同步方案效能比較

效能從高到低排序
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized
複製程式碼

十五、自旋鎖、互斥鎖比較

  • 什麼情況使用自旋鎖比較划算?
    • 預計執行緒等待鎖的時間很短
    • 加鎖的程式碼(臨界區)經常被呼叫,但競爭情況很少發生
    • CPU資源不緊張
    • 多核處理器
  • 什麼情況使用互斥鎖比較划算?
    • 預計執行緒等待鎖的時間較長
    • 單核處理器
    • 臨界區有IO操作
    • 臨界區程式碼複雜或者迴圈量大
    • 臨界區競爭非常激烈

相關文章