文章目錄
- 前言
- 1??什么是鎖🔒?
- 1.1 基本概念
- 1.2 鎖的分類
- 2??OC 中的常用鎖
- 2.1 OSSpinLock(已棄用):“自旋鎖”的經典代表
- 為什么盡量在開發中不使用自旋鎖
- 自旋鎖的本質缺陷:忙等待(Busy Waiting)
- os_unfair_lock的局限性:不適用于復雜場景
- 蘋果的官方建議:優先使用更高效的鎖
- 2.2 dispatch_semaphore_t(GCD 信號量):“高性能通用鎖”
- 2.3 pthread_mutex(POSIX 互斥鎖):“最通用的系統級鎖”
- 2.4 NSLock:“OC 層面的互斥鎖封裝”
- 2.5 @synchronized:“OC 特有的語法糖”
- 2.6 NSCondition:“條件等待鎖”(生產者-消費者型)
- 3??鎖的對比與選擇
- 4??常見陷阱
- 4.1 死鎖(Deadlock)
- 4.2 鎖內執行耗時操作
- 4.3 錯誤使用鎖對象
- 總結
前言
??在前段時間學習dyld時,接觸到了鎖,由于并不是很清楚鎖的知識,導致涉及到鎖的其他內容也有點懵懂,再加上,想要深入了解OC中的多線程,鎖是前提預備知識,所以筆者對鎖進行簡單學習并撰寫了這篇博客。
1??什么是鎖🔒?
1.1 基本概念
百度解釋如下:
鎖是編程中用于協調多個線程或進程對共享資源訪問的機制,主要用于防止并發沖突、確保數據一致性和程序正確性。 ?
互斥性?:保證同一時刻只有一個線程/進程訪問共享資源,避免數據競爭。
協調線程/進程的執行順序,確保操作原子性(全部完成或全部不完成)。
簡單來說,鎖就像一個“開關”,在同一時間只允許一個線程“進入”某個代碼段或訪問某個資源,其他線程需要等待,直到鎖被釋放。
1.2 鎖的分類
根據分類標準,一般情況下我們把鎖分為一下7大類別:
(1)悲觀鎖和樂觀鎖
(2)公平鎖和非公平鎖
(3)共享鎖和獨占鎖
(4)可重入鎖和非可重入鎖
(5)自旋鎖和非自旋鎖
(6)偏向鎖、輕量級鎖和重量級鎖
(7)可中斷鎖和不可中斷鎖
在OC中,鎖的大類就兩種自旋鎖和互斥鎖,可以細分為以下幾類:
類型 | 特點 | 典型代表 |
---|---|---|
互斥鎖(Mutex) | 同一時間僅允許一個線程加鎖,不可重入 | pthread_mutex、NSLock |
自旋鎖 | 線程會反復檢查變量是否可用 | OSSpinLock(已棄用)、atomic |
條件鎖(Condition) | 等待特定條件滿足后再加鎖 | NSCondition、pthread_cond_t |
遞歸鎖(Recursive Lock) | 允許同一線程多次加鎖(需對應次數解鎖) | pthread_mutex(recursive)、NSRecursiveLock |
信號量(Semaphore) | 控制并發線程數量(非嚴格互斥) | dispatch_semaphore_t |
語言特性鎖 | OC 語法糖,簡化鎖操作 | @synchronized(互斥鎖) |
另外還有一個讀寫鎖:讀寫鎖實際是一種特殊的自旋鎖。將對共享資源的訪問分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作。這種鎖相對于自旋鎖而言,能提高并發性。
2??OC 中的常用鎖
2.1 OSSpinLock(已棄用):“自旋鎖”的經典代表
原理:自旋鎖(Spin Lock)通過忙等待(循環檢查鎖狀態)實現互斥。當鎖被占用時,其他線程不會休眠,而是不斷循環嘗試獲取鎖,直到成功。
歷史:OSSpinLock 曾是 OC 中最快的鎖(無系統調用,純用戶態操作),但因 優先級反轉問題(低優先級線程持有鎖時,高優先級線程會瘋狂自旋搶占 CPU,導致系統卡頓)在 iOS 10 后被棄用。
示例代碼(僅作原理參考,禁止在生產環境使用):
#import <os/lock.h>os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; // OSSpinLock 已棄用,替代方案是 os_unfair_lock(本質是改良的自旋鎖)- (void)threadSafeMethod {os_unfair_lock_lock(&lock);// 臨界區:訪問共享資源os_unfair_lock_unlock(&lock);
}
注意:os_unfair_lock是 OSSpinLock 的替代方案,但仍為自旋鎖,適用于輕量級同步(如單次數據訪問),避免在鎖內執行耗時操作。
為什么盡量在開發中不使用自旋鎖
自旋鎖的本質缺陷:忙等待(Busy Waiting)
os_unfair_lock的底層實現與被棄用的 OSSpinLock類似,均基于自旋鎖機制。當鎖被其他線程占用時,當前線程會進入一個 無限循環(忙等待),不斷檢查鎖是否釋放。這種機制會導致以下問題:
1.cpu資源的浪費
自旋鎖的“忙等待”會持續占用 CPU 時間片,即使線程并未執行任何有效操作。在高負載場景(如多線程競爭激烈)下,大量線程空轉會導致 CPU 使用率飆升,甚至引發系統卡頓。
示例對比:
- 傳統互斥鎖(如 pthread_mutex):鎖被占用時,線程會主動休眠(釋放 CPU),等待鎖釋放后被喚醒。
- 自旋鎖(如 os_unfair_lock):鎖被占用時,線程持續空轉,CPU 資源被無意義消耗。
2.優先級反轉
當低優先級的線程持有鎖時,高優先級線程會因不斷嘗試獲取鎖而頻繁搶占cpu,導致低優先級線程無法運行(被“餓死”)。這種現象被稱為優先級反轉,會嚴重破壞系統的實時性和公平性。
eg:低優先級線程 A 持有 os_unfair_lock。高優先級線程 B 嘗試獲取鎖,進入忙等待,持續占用 CPU。系統被迫頻繁調度高優先級線程 B,低優先級線程 A 無法獲得執行機會。
os_unfair_lock的局限性:不適用于復雜場景
盡管 os_unfair_lock比 OSSpinLock更輕量(減少了部分內存屏障),但它的設計目標僅限于 輕量級、短時間的臨界區保護(如單次內存訪問)。對于以下場景,它無法提供可靠支持:
1. 長時間持有鎖的操作
如果臨界區內需要執行耗時操作(如文件 I/O、網絡請求、復雜計算),os_unfair_lock
的忙等待會導致線程長時間占用 CPU,嚴重影響其他任務的執行效率。
eg:
// 錯誤用法:臨界區執行耗時操作(如讀取大文件)
os_unfair_lock_lock(&lock);
NSData *largeData = [NSData dataWithContentsOfFile:@"/bigfile.dat"]; // 耗時操作
os_unfair_lock_unlock(&lock);
此時,線程會因忙等待導致 CPU 空轉,而文件讀取的 I/O 操作本身是阻塞的(無需 CPU 參與),造成資源浪費。
2.遞歸鎖需求
os_unfair_lock不支持遞歸加鎖(同一線程無法多次獲取同一把鎖)。如果代碼中存在遞歸調用(如方法 A 調用方法 B,兩者都需要加同一把鎖),會導致死鎖。
eg:
- (void)recursiveMethod {os_unfair_lock_lock(&lock);// 遞歸調用自身[self recursiveMethod]; // 第二次加鎖會失敗,導致死鎖os_unfair_lock_unlock(&lock);
}
蘋果的官方建議:優先使用更高效的鎖
蘋果在官方文檔中明確推薦,避免使用自旋鎖(包括 os_unfair_lock)處理需要長時間持有或高競爭的場景,并提供了更優的替代方案:
1. dispatch_semaphore_t(GCD 信號量)
基于內核信號量實現,鎖被占用時線程會休眠(釋放 CPU),支持并發控制(計數 >1 時允許多線程同時訪問)。適用于輕量級同步、限制并發任務數(如限制同時下載的文件數)等場景。
2. pthread_mutex(POSIX 互斥鎖)
支持遞歸鎖(通過 PTHREAD_MUTEX_RECURSIVE類型),內核級實現,穩定性高。適用于需要遞歸加鎖或多線程頻繁競爭的場景(如嵌套方法調用)。
3. NSLock(OC 層面封裝)
API 簡潔,基于 pthread_mutex實現,支持 tryLock非阻塞加鎖。適用于簡單的線程同步(如保護單次數據訪問)。
2.2 dispatch_semaphore_t(GCD 信號量):“高性能通用鎖”
信號量(Semaphore)是基于計數器的一種多線程同步機制,用來管理對資源的并發訪問。信號量就是一種可用來控制訪問資源的數量的標識,設定了一個信號量,在線程訪問之前,加上信號量的處理,則可告知系統按照我們指定的信號量數量來執行多個線程。
相關函數:
-
dispatch_semaphore_t、dispatch_semaphore_create(long value):創建信號量,初始化計數(通常為 1 時表示互斥鎖)。參數為信號量的初值,小于零就會返回NULL。
-
線程加鎖時調用 dispatch_semaphore_wait減少計數(若計數為 0 則阻塞)。long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); :等待降低信號量,接收一個信號和時間值(多為DISPATCH_TIME_FOREVER)。若信號的信號量為0,則會阻塞當前線程,直到信號量大于0或者經過輸入的時間值。若信號量大于0,則會使信號量減1并返回,程序繼續住下執行。
-
解鎖時調用 dispatch_semaphore_signal增加計數(喚醒等待線程)。long dispatch_semaphore_signal(dispatch_semaphore_t dsema); :提高信號量, 使信號量加1并返回 在dispatch_semaphore_wait和dispatch_semaphore_signal這兩個函數中間的執行代碼,每次只會允許限定數量的線程進入,這樣就有效的保證了在多線程環境下,只能有限定數量的線程進入。
特點:
- 性能接近 OSSpinLock(底層通過內核信號量實現,但比傳統鎖更高效)。
- 支持控制并發線程數量(計數 >1 時為“并發鎖”,允許指定數量線程同時訪問)。
示例代碼:
/*
// 初始化信號量(計數為 1,即互斥鎖)
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);- (void)threadSafeMethod {// 加鎖:計數減 1(若計數為 0 則阻塞當前線程)dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);// 臨界區:訪問共享資源// 解鎖:計數加 1(喚醒等待線程)dispatch_semaphore_signal(semaphore);
}
*///具體實例
#import <Foundation/Foundation.h>
#import <os/lock.h>int main(int argc, const char * argv[]) {@autoreleasepool {// 共享資源:一個需要線程安全的計數器__block int counter = 0;// 初始化信號量(計數為 1,即互斥鎖;若計數>1,允許指定數量線程同時訪問)dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);// 模擬 10 個線程同時修改計數器for (int i = 0; i < 10; i++) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{for (int j = 0; j < 1000; j++) {// 加鎖:計數減 1(若計數為 0 則阻塞)dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);// 臨界區:修改共享資源(需保證原子性)counter++;// 解鎖:計數加 1(喚醒等待線程)dispatch_semaphore_signal(semaphore);}});}// 等待所有線程完成后打印結果(實際開發中需用更嚴謹的同步機制)sleep(2);NSLog(@"最終計數器值:%d", counter); // 應輸出 10000(10 線程×1000 次)}return 0;
}
運行測試:
適用場景:
- 需要高性能互斥的場景(如高頻數據讀寫)。
- 控制并發任務數量(如限制同時下載的任務數,計數設為 3)。
2.3 pthread_mutex(POSIX 互斥鎖):“最通用的系統級鎖”
POSIX 標準的互斥鎖(Mutex),通過內核實現線程阻塞。支持兩種類型:
- 普通鎖(PTHREAD_MUTEX_INITIALIZER):不可重入,同一線程重復加鎖會死鎖。
- 遞歸鎖(PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP):允許同一線程多次加鎖(需對應次數解鎖)。
示例代碼:
/*
#import <pthread.h>// 初始化遞歸鎖(允許同一線程多次加鎖)
pthread_mutex_t recursiveLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 設置為遞歸鎖
pthread_mutex_init(&recursiveLock, &attr);- (void)recursiveMethod {pthread_mutex_lock(&recursiveLock);// 臨界區(可能遞歸調用自身)pthread_mutex_unlock(&recursiveLock);
}// 銷毀鎖(避免內存泄漏)
pthread_mutex_destroy(&recursiveLock);
pthread_mutexattr_destroy(&attr);
*///具體實例
#import <Foundation/Foundation.h>
#import <os/lock.h>
#import <pthread/pthread.h>int main(int argc, const char * argv[]) {@autoreleasepool {// 共享資源:一個需要線程安全的數組__block NSMutableArray *sharedArray = [NSMutableArray array];// 初始化遞歸鎖(允許同一線程多次加鎖)pthread_mutex_t recursiveLock;pthread_mutexattr_t attr;pthread_mutexattr_init(&attr);pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 關鍵:設置為遞歸鎖pthread_mutex_init(&recursiveLock, &attr);// 線程 1:嵌套調用方法dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{pthread_mutex_lock(&recursiveLock); // 第一次加鎖[sharedArray addObject:@"A"];// 嵌套調用(需再次加鎖)pthread_mutex_lock(&recursiveLock); // 第二次加鎖(遞歸鎖允許)[sharedArray addObject:@"B"];pthread_mutex_unlock(&recursiveLock); // 第一次解鎖pthread_mutex_unlock(&recursiveLock); // 第二次解鎖});// 線程 2:直接訪問共享資源dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{pthread_mutex_lock(&recursiveLock);[sharedArray addObject:@"C"];pthread_mutex_unlock(&recursiveLock);});// 等待線程完成后打印結果sleep(2);NSLog(@"共享數組:%@", sharedArray); // 應輸出 ["A", "B", "C"](順序可能不同)}return 0;
}
運行測試:
特點:
- 性能穩定,兼容所有 iOS 版本。
- 遞歸鎖適合嵌套調用場景(如方法 A 調用方法 B,兩者都需要加同一把鎖)。
2.4 NSLock:“OC 層面的互斥鎖封裝”
原理:NSLock是 pthread_mutex的 OC 封裝,提供了更簡潔的 API(如 lock、unlock、tryLock)。默認是普通鎖(不可重入),但可通過 NSRecursiveLock子類實現遞歸鎖。
示例代碼:
/*
// 普通互斥鎖
NSLock *lock = [[NSLock alloc] init];- (void)threadSafeMethod {[lock lock];// 臨界區[lock unlock];
}// 嘗試加鎖(非阻塞,返回 BOOL 表示是否成功)
if ([lock tryLock]) {// 臨界區[lock unlock];
}// 遞歸鎖(允許同一線程多次加鎖)
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
*///具體實例
#import <Foundation/Foundation.h>
#import <os/lock.h>
#import <pthread/pthread.h>int main(int argc, const char * argv[]) {@autoreleasepool {// 共享資源:一個需要線程安全的用戶信息字典__block NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];// 初始化普通互斥鎖NSLock *lock = [[NSLock alloc] init];// 創建一個調度組dispatch_group_t group = dispatch_group_create();// 線程 1:修改用戶信息dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{[lock lock]; // 加鎖userInfo[@"name"] = @"張三";userInfo[@"age"] = @25;[lock unlock]; // 解鎖});// 線程 2:讀取用戶信息(需等待線程 1 解鎖)dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{[lock lock]; // 加鎖(若線程 1 未解鎖會阻塞)NSString *name = userInfo[@"name"];NSNumber *age = userInfo[@"age"];NSLog(@"用戶信息:%@,%@", name, age); // 應輸出 "張三",25[lock unlock]; // 解鎖});// 等待所有異步任務完成dispatch_group_wait(group, DISPATCH_TIME_FOREVER);}return 0;
}
運行測試:
注意:NSLock的 unlock必須與 lock成對出現,否則可能導致死鎖(如異常未捕獲導致 unlock未執行)。
2.5 @synchronized:“OC 特有的語法糖”
原理:@synchronized是 OC 編譯器提供的語法糖,底層通過哈希表將鎖對象映射到內核互斥鎖。語法簡潔,無需手動管理鎖的創建和銷毀。
示例代碼:
// 以某個對象(如 self)為鎖的標識
- (void)threadSafeMethod {@synchronized (self) { // 鎖對象為 self// 臨界區:訪問共享資源}
}// 也可以用其他對象作為鎖(推薦專用鎖對象,避免與其他代碼沖突)
NSObject *lockObj = [[NSObject alloc] init];
@synchronized (lockObj) {// 臨界區
}
特點:
- 代碼簡潔,無需手動加鎖/解鎖(自動管理)。
- 鎖對象需唯一(不同鎖對象無法同步)。
- 性能較差(底層涉及哈希表查找和內核調用),適合小范圍同步(如單次數據訪問)。
2.6 NSCondition:“條件等待鎖”(生產者-消費者型)
原理:NSCondition
是 pthread_cond_t
的 OC 封裝,結合了互斥鎖和條件變量。允許線程在條件不滿足時等待(釋放鎖并休眠),條件滿足時被喚醒(重新加鎖)。
示例代碼(生產者-消費者模型):
@interface ProducerConsumer : NSObject {NSMutableArray *_queue;NSCondition *_condition;NSInteger _maxCount;
}
@end@implementation ProducerConsumer
- (instancetype)init {if (self = [super init]) {_queue = [NSMutableArray array];_condition = [[NSCondition alloc] init];_maxCount = 10; // 隊列最大容量}return self;
}// 生產者:添加數據(隊列滿時等待)
- (void)produce {[_condition lock];while (_queue.count >= _maxCount) {[_condition wait]; // 條件不滿足,釋放鎖并休眠}[_queue addObject:@(arc4random_uniform(100))];[_condition signal]; // 喚醒一個等待的消費者[_condition unlock];
}// 消費者:取出數據(隊列空時等待)
- (void)consume {[_condition lock];while (_queue.count == 0) {[_condition wait]; // 條件不滿足,釋放鎖并休眠}id obj = _queue.firstObject;[_queue removeObjectAtIndex:0];[_condition signal]; // 喚醒一個等待的生產者[_condition unlock];
}
@end
適用場景:需要線程間協調(如“生產者-消費者”模型),等待特定條件滿足后再執行。
3??鎖的對比與選擇
OC 中常見鎖的性能(從高到低,單線程加鎖/解鎖耗時):
os_unfair_lock ≈ dispatch_semaphore_t > pthread_mutex(遞歸) > NSRecursiveLock > NSLock > @synchronized
鎖的選擇:
- 性能要求高:優先選
dispatch_semaphore_t
(通用)或os_unfair_lock
(輕量同步)。 - 需要遞歸:選
pthread_mutex(recursive)
或NSRecursiveLock
。 - 代碼簡潔性:選
@synchronized
(適合小范圍同步)。 - 條件等待:選
NSCondition
(如生產者-消費者模型)。
4??常見陷阱
4.1 死鎖(Deadlock)
原因:多個線程互相等待對方釋放鎖(如線程 A 持有鎖 1 等待鎖 2,線程 B 持有鎖 2 等待鎖 1)。
解決:
- 避免嵌套加鎖(如非必要不使用遞歸鎖)。
- 統一加鎖順序(所有線程按相同順序獲取鎖)。
4.2 鎖內執行耗時操作
原因:鎖的加鎖/解鎖涉及內核調用,若臨界區內執行耗時操作(如 IO、大量計算),會導致其他線程長時間等待,降低并發效率。
解決:
- 將耗時操作移到鎖外(如先讀取數據到臨時變量,再在鎖內處理)。
4.3 錯誤使用鎖對象
原因:@synchronized
使用動態對象(如可能被釋放的 self
)作為鎖標識,或不同線程使用不同的鎖對象。
解決:
@synchronized
的鎖對象需是生命周期穩定的(如專用NSObject
實例)。
總結
OC 中的鎖機制需根據場景選擇:
- 輕量同步:
dispatch_semaphore_t
或os_unfair_lock
(性能最優)。 - 遞歸需求:
pthread_mutex(recursive)
或NSRecursiveLock
。 - 代碼簡潔:
@synchronized
(小范圍使用)。 - 條件協調:
NSCondition
(如生產者-消費者)。
核心原則:鎖的范圍盡可能小(僅保護臨界區),避免死鎖,優先使用系統提供的高性能鎖(如 GCD 信號量)。