1.概述
1.1 類型
- 自旋鎖 OSSpinLock
- 信號量 dispatch_semaphore
- 互斥鎖 pthread_mutex夯尽、NSLock
- 遞歸鎖 pthread_mutex(recursive)脑漫、NSRecursiveLock、@sychronized
- 條件鎖 NSCondition咕幻、NSConditionLock
參考一張著名的圖:
他們的性能從上到下依次降低
1.2 基礎原理
- 什么是鎖?通過一種機制,將代碼片段劃分成一段一段臨界區(qū)域翁狐,確保同一時間只有一個臨界區(qū)域處于運行狀態(tài)钥飞;鎖主要解決線程同步問題,避免引發(fā)數(shù)據(jù)混亂等未知問題
- 互斥鎖 也叫同步鎖
- 遞歸鎖 其實也屬于互斥鎖的一種衫嵌,對于互斥鎖读宙,當同一個線程多次訪問同一個鎖時會引發(fā)死鎖;而遞歸鎖不會出現(xiàn)此問題楔绞,遞歸鎖的例子结闸,參考Reentrant_mutex#Example:
Thread A calls function F which acquires a reentrant lock for itself before proceeding
Thread B calls function F which attempts to acquire a reentrant lock for itself but cannot due to one already outstanding, resulting in either a block (it waits), or a timeout if requested
Thread A's F calls itself recursively. It already owns the lock, so it will not block itself (no deadlock). This is the central idea of a reentrant mutex, and is what makes it different from a regular lock.
Thread B's F is still waiting, or has caught the timeout and worked around it
Thread A's F finishes and releases its lock(s)
Thread B's F can now acquire a reentrant lock and proceed if it was still waiting
理解:當一個線程調用F加鎖之后唇兑,若F中又調用自己,就不再加鎖了桦锄,所以不會死鎖
- 自旋鎖 是一種內核級別的鎖扎附, 采用忙等機制加鎖,OSSpinLock的頭文件是<libkern/OSAtomic.h>结耀,該鎖在iOS10以上廢棄了留夜,改為os_unfair_lock_t
2. 實現(xiàn)原理
2.1 原理概述
- 這么多種鎖其實大的實現(xiàn)思路就兩種,一種叫忙等待图甜,一種叫阻塞
- 所謂忙等待就是碍粥,讓一個線程處于一直處于等待狀態(tài),直至鎖釋放黑毅;阻塞是讓一個線程掛起嚼摩,直至鎖釋放,操作系統(tǒng)會切換上下文到另個線程矿瘦;相比而言枕面,忙等待機制會更耗CPU資源,因為線程還在跑
現(xiàn)在互斥鎖大多使用隊列和上下文切換以達到節(jié)約資源和降低延遲的目的缚去;但總有情況潮秘,掛起一個線程,然后切換上下文再恢復的時間比線程忙等所用的時間長病游,所以這時候就要用自旋鎖唇跨。參考:互斥鎖
- 使用鎖會出現(xiàn)優(yōu)先級倒置(高優(yōu)先級線程會等待地優(yōu)先級線程執(zhí)行,下邊OSSpinLock自旋鎖就是衬衬,有詳細解釋)买猖、資源饑荒(某個線程一直得不到足夠的鎖資源)的情況
2.2 各種鎖的原理
- OSSpinLock 是一種忙等機制,利用全局變量表示鎖的狀態(tài)滋尉,它會存在優(yōu)先級反轉問題玉控,就是說當高優(yōu)先級通過信號機制訪問低優(yōu)先級,信號量已經(jīng)被低優(yōu)先級占有狮惜,導致高優(yōu)先級任務被低優(yōu)先級任務所阻塞
- dispatch_semaphore 信號量機制不僅能操作線程高诺,其實還能夠操作進程,只是iOS 單個應用來說是單進程的(此句有誤碾篡,UI渲染就在一個新線程叫backbond)虱而;當wait時,信號量的值-1开泽;當signal時牡拇,信號量的值+1,當信號量的值==0時,當前線程會被阻塞惠呼;所以初始值應該傳入1
- pthread_mutex 互斥鎖导俘,是一套跨平臺的API,采用的阻塞機制
- pthread_mutex(recursive) 與pthread_mutex類似
- NSLock 一種互斥鎖剔蹋,內部封裝的 pthread_mutex實現(xiàn)
- NSRecursiveLock 與NSLock類似旅薄,內部封裝的pthread_mutex實現(xiàn),鎖的類型不同PTHREAD_MUTEX_RECURSIVE
- NSCondition 封裝了一個互斥鎖和條件變量
- NSConditionLock 以此來實現(xiàn)(以上兩個沒有用過泣崩,沒有發(fā)言權暫不探究了)
- @sychronized 這個性能最慢的鎖少梁,但用起來也最爽,它的原理是需要深入了解的(凡是封裝的較深的是有必要探究下的)
NSLock是pthread_mutex的封裝可以通用
NSRecursiveLock是pthread_mutex(recursive) 的封裝
2.3 @sychronized原理
參考:關于 @synchronized律想,這兒比你想知道的還要多
具體探究過程請參考文章猎莲,結合源碼閱讀即可
一句話總結:
編譯器會將{}代碼放入try{}finally{}中,在{開始時技即,會轉化成objc_sync_enter方法,內部調用pthread_mutex創(chuàng)建一個遞歸鎖樟遣,并分配給@sychronized(obj)傳入的obj而叼;在}結束時調用objc_sync_exit方法釋放分配的鎖
偽代碼如下:
NSString *test = @"test";
id synchronizeTarget = (id)test;
@try {
objc_sync_enter(synchronizeTarget);
test = nil;
} @finally {
objc_sync_exit(synchronizeTarget);
}
理解:
- 分配的鎖通過對象的內存地址與對象建立關聯(lián)
- 若傳入nil對象,不會出問題豹悬,但等于沒有加鎖
- 若在{}中對象置為nil葵陵,也不會出現(xiàn)問題,編譯器做了處理瞻佛,即保證enter和exit方法傳入的對象是同一個
- @sychronized不會修改對象的引用計數(shù)
3. 使用場景
目前用過兩種脱篙,一種是互斥鎖,一種是信號量伤柄,平時開發(fā)中把這兩個掌握好基本就能滿足大部分需求绊困,從一些知名的開源庫中尋找使用場景:
3.1 在ReactiveCocoa 中大量使用OSSpinLock,來保證大量全局變量的同步适刀,一般這樣用
eg: 在RACCompoundDisposable.m中
- (BOOL)isDisposed {
OSSpinLockLock(&_spinLock);
BOOL disposed = _disposed;
OSSpinLockUnlock(&_spinLock);
return disposed;
}
3.2 在AFNetworking秤朗、ReactiveCocoa中可以看到NSLock
eg:在AFURLResponseSerializer.m中,可以看到從NSData轉UIImage時使用了同步鎖笔喉,解決[UIImage imageWithData:data]線程不安全的問題取视,參考issue #2572 Crash on AFImageWithDataAtScale when loading image
+ (UIImage *)af_safeImageWithData:(NSData *)data {
UIImage* image = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
imageLock = [[NSLock alloc] init];
});
[imageLock lock];
image = [UIImage imageWithData:data]; //該方法不是線程安全的
[imageLock unlock];
return image;
}
3.3 在CocoaLumberjack中大量使用pthread_mutex來保證線程同步,在DDContextFilterLogFormatter.m一個例子:
- (NSArray *)currentSet {
NSArray *result = nil;
pthread_mutex_lock(&_mutex);
{
result = [_set allObjects];
}
pthread_mutex_unlock(&_mutex);
return result;
}
3.4 在AFURLSessionManager.m tasksForKeyPath方法使用dispatch_semaphore_t來阻塞當前線程常挚,例子如下:
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //當getTasksWithCompletionHandler執(zhí)行完畢signal時時作谭,才會該函數(shù)才會return,是一種保證函數(shù)返回值同步的好辦法
return tasks;
}
小總結:
- 使用最多的是NSLock這種形式奄毡、其次是pthread_mutex和信號量
- 盡量不要使用@sychronized這種方式折欠,因為它性能較低
4. 補充
多線程中棧是私有的,堆是公有的
每個線程擁有一個棧和計數(shù)器,棧和計數(shù)器用來保存線程的執(zhí)行歷史和執(zhí)行狀態(tài)怨酝;
其他資源(堆傀缩、地址空間、全局變量)是同一個進程內多個線程共享