一、多執行緒的安全隱患
- 資源共享
- 1塊資源 可能會被多個執行緒共享,也就是多個執行緒可能會訪問同一塊資源
- 比如多個執行緒訪問同一個物件、同一個變數、同一個檔案
- 當多個執行緒訪問同一塊資源時,很容易引發資料錯亂和資料安全問題
二、多執行緒安全隱患示例01 – 存錢取錢
- 模擬程式碼如下
- 執行程式, 結果如下
- 正常情況, 應該存
5000
, 取2500
, 所以應該剩3500
, 但是結果剩了2500
- 再次執行模擬
- 可以看到只剩了
2000
, 這就是多執行緒的安全隱患問題, 是資料錯亂
三、多執行緒安全隱患示例02 – 賣票
- 程式碼模擬如下
- 執行程式, 模擬賣票
- 一共賣出
10
張, 應該剩餘0
張, 但是結果卻剩餘3
張, 說明資料出現了錯亂
四、多執行緒安全隱患分析和解決方案
1、多執行緒安全隱患分析
2、多執行緒安全隱患的解決方案
- 解決方案:使用執行緒同步技術(同步,就是協同步調,按預定的先後次序進行)
- 常見的執行緒同步技術是:加鎖
五、iOS中的執行緒同步方案
- iOS中執行緒加鎖有以下幾種方案
OSSpinLock
os_unfair_lock
pthread_mutex
dispatch_semaphore
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized
複製程式碼
六、準備程式碼
- 將上面的
多執行緒安全隱患示例01 – 存錢取錢
和多執行緒安全隱患示例02 – 賣票
程式碼封裝到一個BaseDemo
類中, 具體程式碼如下圖
- 在
BaseDemo
暴露出五個方法, 兩個測試呼叫, 三個執行緒呼叫 - 建立
AddLockDemo
繼承自BaseDemo
ViewController
中程式碼如下
七、OSSpinLock(自旋鎖)
OSSpinLock
叫做自旋鎖
,等待鎖的執行緒會處於忙等(busy-wait)狀態,一直佔用著CPU資源
1、解決存錢取錢
和賣票
的安全隱患
- 在
存錢取錢
和賣票
中加入OSSpinLock
- 執行程式, 多次點選螢幕試驗, 都可以發現結果正確
2、OSSpinLock
目前已經不再安全,可能會出現優先順序反轉問題
- 一個程式中可能會有多個執行緒, 但是隻有一個CPU
- CPU給執行緒分配資源, 讓他們穿插的執行, 比如有三個執行緒
thread1
、thread2
和thread3
- 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
- 執行程式, 多次點選螢幕試驗, 都可以發現結果正確
九、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、解決存錢取錢
和賣票
的安全隱患
- 匯入標頭檔案, 建立鎖, 加鎖解鎖
- 執行程度, 多次點選螢幕試驗, 都可以發現結果正確
2、遞迴鎖
- 定義
PthreadTest
類繼承自NSObject
, 其中recursive
是一個遞迴方法
ViewController
中程式碼如下, 點選螢幕後呼叫PthreadTest
的recursive
方法
- 點選螢幕, 可以看到發生了死鎖, 這是因為
recursive
中呼叫recursive
, 此時還沒有解鎖, 再次進行加鎖, 所以發生了死鎖
- 設定
pthread
初始化時的屬性型別為PTHREAD_MUTEX_RECURSIVE
, 這樣pthread
就是一把遞迴鎖
- 遞迴鎖允許同一執行緒內, 對同一把鎖進行重複加鎖, 所以可以看到遞迴方法呼叫成功
3、條件
PthreadTest
中程式碼如下
ViewController
中程式碼如下
-
當點選螢幕時, 會在
array
中移除最後一個元素
和新增一個新元素
, 程式碼中可以看到, 使用不同執行緒呼叫__remove
和__add
兩個方法 -
現在的需求是, 只有在
array
不為空的情況下, 才能執行刪除操作, 如果直接執行, 那麼可能會先呼叫__remove
在呼叫__add
, 那麼就與需求相違背 -
所以, 我們可以使用
條件
對兩個方法進行優化 -
建立
cond
- 當
array.count == 0
時, 是程式進入休眠, 只有當array
中新增了新資料後在發起訊號, 將休眠的執行緒喚醒
- 執行程式, 點選螢幕, 可以看到程式先進入
__remove
方法, 但是卻在__add
中新增新元素之後再移除元素
十、NSLock、NSRecursiveLock、NSCondition、NSConditionLock
NSLock
、NSRecursiveLock
、NSCondition
和NSConditionLock
是基於pthread
封裝的OC物件
1、NSLock
AddLockDemo
中程式碼如下, 直接使用NSLock
進行加鎖
ViewController
中點選螢幕時呼叫方法
- 執行程式, 點選螢幕, 可以看到結果正確
- 檢視
GNUStep
中關於NSLock
的底層程式碼, 可以看到NSLock
是基礎pthread
封裝的normal
鎖
2、NSRecursiveLock
PthreadTest
中程式碼如下, 使用NSRecursiveLock
對遞迴函式
加鎖解鎖
ViewController
中, 當點選螢幕時呼叫recursive
方法
- 執行程式, 點選螢幕, 可以看到
遞迴鎖
的結果
- 檢視
GNUStep
中關於NSRecursiveLock
的底層程式碼
3、NSCondition
PthreadTest
中程式碼如下, 使用NSCondition
加鎖解鎖
ViewController
中, 當點選螢幕時呼叫pthreadTest
方法
- 可以看到, 先呼叫了
__remove
方法, 但是卻在__add
中給array
新增了新元素之後, 才刪除一個元素
- 檢視
GNUStep
中關於NSCondition
的底層程式碼
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
設定執行緒的執行順序
- 執行程式, 可以看到列印順序
十一、同步佇列解決多執行緒隱患
- 使用同步佇列, 程式碼如下圖
ViewController
程式碼如下
- 點選螢幕, 可以看到結果正確
十二、dispatch_semaphore_t
- 可以使用
dispatch_semaphore_t
設定訊號量為1
, 來控制同意之間只有一條執行緒能執行, 實際程式碼如下
- 執行程式, 點選螢幕, 可以看到列印結果正確
十三、@synchronized
@synchronized
是對mutex
遞迴鎖的封裝- 原始碼檢視:
objc4
中的objc-sync.mm
檔案 @synchronized(obj)
內部會生成obj
對應的遞迴鎖,然後進行加鎖、解鎖操作
1、解決多執行緒的安全隱患
- 使用
@synchronized
進行加鎖
- 執行程式碼, 點選螢幕, 效果如下
2、@synchronized底層原理
- 找到
objc_sync_enter
和objc_sync_exit
兩個函式, 分別用於加鎖和解鎖
- 檢視
SyncData
- 通過所點進去, 找到
recursive_mutex_tt
- 檢視
recursive_mutex_tt
, 可以看到底層是通過os_unfair_recursive_lock
封裝的鎖
- 接著檢視通過
物件
獲取鎖的程式碼
- 找到
LIST_FOR_OBJ
, 點選檢視
- 可以看到, 通過傳入的
物件
, 會獲取唯一標識所謂鎖
十四、iOS執行緒同步方案效能比較
效能從高到低排序
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized
複製程式碼
十五、自旋鎖、互斥鎖比較
- 什麼情況使用自旋鎖比較划算?
- 預計執行緒等待鎖的時間很短
- 加鎖的程式碼(臨界區)經常被呼叫,但競爭情況很少發生
- CPU資源不緊張
- 多核處理器
- 什麼情況使用互斥鎖比較划算?
- 預計執行緒等待鎖的時間較長
- 單核處理器
- 臨界區有IO操作
- 臨界區程式碼複雜或者迴圈量大
- 臨界區競爭非常激烈