小碼哥iOS學習筆記第二十天: 多線程的安全隱患

1、多線程的安全隱患

  • 資源共享
    • 1塊資源 可能會被多個線程共享,也就是多個線程可能會訪問同一塊資源
    • 好比多個線程訪問同一個對象、同一個變量、同一個文件
  • 當多個線程訪問同一塊資源時,很容易引起數據錯亂和數據安全問題

2、多線程安全隱患示例01 – 存錢取錢

  • 模擬代碼以下

  • 運行程序, 結果以下

  • 正常狀況, 應該存5000, 取2500, 因此應該剩3500, 可是結果剩了2500
  • 再次運行模擬

  • 能夠看到只剩了2000, 這就是多線程的安全隱患問題, 是數據錯亂

3、多線程安全隱患示例02 – 賣票

  • 代碼模擬以下

  • 運行程序, 模擬賣票

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

4、多線程安全隱患分析和解決方案

一、多線程安全隱患分析

二、多線程安全隱患的解決方案

  • 解決方案:使用線程同步技術(同步,就是協同步調,按預約的前後次序進行)
  • 常見的線程同步技術是:加鎖

5、iOS中的線程同步方案

  • iOS中線程加鎖有如下幾種方案
OSSpinLock
os_unfair_lock
pthread_mutex
dispatch_semaphore
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized
複製代碼

6、準備代碼

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

  • BaseDemo暴露出五個方法, 兩個測試調用, 三個線程調用
  • 建立AddLockDemo繼承自BaseDemo

  • ViewController中代碼以下

7、OSSpinLock(自旋鎖)

  • OSSpinLock叫作自旋鎖,等待鎖的線程會處於忙等(busy-wait)狀態,一直佔用着CPU資源

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

  • 存錢取錢賣票中加入OSSpinLock

  • 運行程序, 屢次點擊屏幕試驗, 均可以發現結果正確

二、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
這樣就會出現相似於死鎖的問題
複製代碼

8、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

  • 運行程序, 屢次點擊屏幕試驗, 均可以發現結果正確

9、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
複製代碼

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

  • 導入頭文件, 建立鎖, 加鎖解鎖

  • 運行程度, 屢次點擊屏幕試驗, 均可以發現結果正確

二、遞歸鎖

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

  • ViewController中代碼以下, 點擊屏幕後調用PthreadTestrecursive方法

  • 點擊屏幕, 能夠看到發生了死鎖, 這是由於recursive中調用recursive, 此時尚未解鎖, 再次進行加鎖, 因此發生了死鎖

  • 設置pthread初始化時的屬性類型爲PTHREAD_MUTEX_RECURSIVE, 這樣pthread就是一把遞歸鎖

  • 遞歸鎖容許同一線程內, 對同一把鎖進行重複加鎖, 因此能夠看到遞歸方法調用成功

三、條件

  • PthreadTest中代碼以下

  • ViewController中代碼以下

  • 當點擊屏幕時, 會在array中移除最後一個元素添加一個新元素, 代碼中能夠看到, 使用不一樣線程調用__remove__add兩個方法安全

  • 如今的需求是, 只有在array不爲空的狀況下, 才能執行刪除操做, 若是直接運行, 那麼可能會先調用__remove在調用__add, 那麼就與需求相違背bash

  • 因此, 咱們可使用條件對兩個方法進行優化多線程

  • 建立cond函數

  • array.count == 0時, 是程序進入休眠, 只有當array中添加了新數據後在發起信號, 將休眠的線程喚醒

  • 運行程序, 點擊屏幕, 能夠看到程序先進入__remove方法, 可是卻在__add中添加新元素以後再移除元素

10、NSLock、NSRecursiveLock、NSCondition、NSConditionLock

  • NSLockNSRecursiveLockNSConditionNSConditionLock是基於pthread封裝的OC對象

一、NSLock

  • AddLockDemo中代碼以下, 直接使用NSLock進行加鎖

  • ViewController中點擊屏幕時調用方法

  • 運行程序, 點擊屏幕, 能夠看到結果正確

  • 查看GNUStep中關於NSLock的底層代碼, 能夠看到NSLock是基礎pthread封裝的normal

二、NSRecursiveLock

  • PthreadTest中代碼以下, 使用NSRecursiveLock遞歸函數加鎖解鎖

  • ViewController中, 當點擊屏幕時調用recursive方法

  • 運行程序, 點擊屏幕, 能夠看到遞歸鎖的結果

  • 查看GNUStep中關於NSRecursiveLock的底層代碼

三、NSCondition

  • PthreadTest中代碼以下, 使用NSCondition加鎖解鎖

  • ViewController中, 當點擊屏幕時調用pthreadTest方法

  • 能夠看到, 先調用了__remove方法, 可是卻在__add中給array添加了新元素以後, 才刪除一個元素

  • 查看GNUStep中關於NSCondition的底層代碼

四、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設置線程的執行順序

  • 運行程序, 能夠看到打印順序

11、同步隊列解決多線程隱患

  • 使用同步隊列, 代碼以下圖

  • ViewController代碼以下

  • 點擊屏幕, 能夠看到結果正確

12、dispatch_semaphore_t

  • 可使用dispatch_semaphore_t設置信號量爲1, 來控制贊成之間只有一條線程能執行, 實際代碼以下

  • 運行程序, 點擊屏幕, 能夠看到打印結果正確

十3、@synchronized

  • @synchronized是對mutex遞歸鎖的封裝
  • 源碼查看:objc4中的objc-sync.mm文件
  • @synchronized(obj)內部會生成obj對應的遞歸鎖,而後進行加鎖、解鎖操做

一、解決多線程的安全隱患

  • 使用@synchronized進行加鎖

  • 執行代碼, 點擊屏幕, 效果以下

二、@synchronized底層原理

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

  • 查看SyncData

  • 經過所點進去, 找到recursive_mutex_tt

  • 查看recursive_mutex_tt, 能夠看到底層是經過os_unfair_recursive_lock封裝的鎖

  • 接着查看經過對象獲取鎖的代碼

  • 找到LIST_FOR_OBJ, 點擊查看

  • 能夠看到, 經過傳入的對象, 會獲取惟一標識所謂鎖

十4、iOS線程同步方案性能比較

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

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

  • 什麼狀況使用自旋鎖比較划算?
    • 預計線程等待鎖的時間很短
    • 加鎖的代碼(臨界區)常常被調用,但競爭狀況不多發生
    • CPU資源不緊張
    • 多核處理器
  • 什麼狀況使用互斥鎖比較划算?
    • 預計線程等待鎖的時間較長
    • 單核處理器
    • 臨界區有IO操做
    • 臨界區代碼複雜或者循環量大
    • 臨界區競爭很是激烈
相關文章
相關標籤/搜索