在移動應用開發中,流暢的用戶體驗至關重要,而并發編程是實現這一目標的關鍵技術。本文將深入探討iOS平臺上的并發編程和多線程架構,幫助你構建高性能、響應迅速的應用程序。
1. iOS線程調度機制
1.1 線程本質和iOS線程調度機制
線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。一個進程可以擁有多個線程,每個線程共享進程的資源,但擁有自己的執行路徑。
在iOS系統中,線程調度由系統內核負責,通過優先級隊列和時間片輪轉算法確定線程的執行順序和執行時長。蘋果對標準的線程模型進行了優化,引入了更高級的抽象,如GCD和Operation,使開發者能夠專注于任務本身,而非線程管理的細節。
1.2 并發計算模型中的同步/異步與串行/并行
并發編程中的兩組基本概念:同步/異步和串行/并行。這兩組概念看似簡單,但常被混淆,它們實際上描述了兩個不同的維度。
同步/異步的本質
同步(Synchronous)與異步(Asynchronous)描述的是調用方式,關注的是線程的等待方式。
- 同步調用:調用者會一直等待被調用的任務完成后,才繼續執行后續代碼。調用者線程在任務執行期間處于阻塞狀態。
- 異步調用:調用者不會等待被調用的任務完成,會立即繼續執行后續代碼。任務的完成通常通過回調、通知或其他機制通知調用者。
串行/并行的本質
**串行(Serial)與并行(Concurrent)**描述的是任務的執行方式,關注的是多個任務之間的關系。
- 串行執行:多個任務按順序依次執行,任何時刻只有一個任務在執行。
- 并行執行:多個任務可以同時執行,任務之間相互獨立,各自在不同的線程上執行。
同步/異步與串行/并行排列組合的調度機制和執行效果
這兩組概念可以組合出四種不同的調度情況,下面我們詳細分析每種組合的調度機制和執行效果。
1. 同步 + 串行
- 調度機制:任務在當前線程上按順序執行
- 執行效果:調用者線程被阻塞,直到所有任務完成
2. 同步 + 并行
- 調度機制:任務被分配到多個線程并發執行
- 執行效果:調用者線程仍然被阻塞,直到所有任務完成
- 注意:這種組合在實際中較少使用,因為即使任務并行執行,調用者仍需等待所有任務完成,無法充分利用并行的優勢
3. 異步 + 串行
- 調度機制:任務在另一個線程上按順序執行
- 執行效果:調用者線程立即返回繼續執行后續代碼,不會被阻塞
4. 異步 + 并行
- 調度機制:任務被分配到多個線程并發執行
- 執行效果:調用者線程立即返回繼續執行后續代碼,不會被阻塞,同時多個任務可以同時執行,充分利用多核處理器性能
這四種組合方式構成了iOS多線程編程的基礎模型,也是理解GCD和NSOperation等高級API的關鍵。
2. iOS線程方案
iOS提供了多種多線程編程方案,從底層的pthread到高級的GCD和NSOperation,為開發者提供了靈活的選擇。
2.1 pthread
pthread
(POSIX thread)是一套跨平臺的線程API標準,iOS也支持這一標準。它比NSThread更底層,提供了更多的控制選項。
#import <pthread.h>// 創建線程
pthread_t thread;
pthread_create(&thread, NULL, ThreadFunction, NULL);// 線程函數
void *ThreadFunction(void *data) {// 執行任務NSLog(@"任務在pthread中執行");return NULL;
}// 等待線程結束
pthread_join(thread, NULL);
優點:
- 跨平臺兼容性好
- 控制粒度更細
- 可以設置線程的各種屬性
缺點:
- API復雜,使用不便
- 需要手動管理線程的各個方面
- 缺乏Objective-C與iOS集成的便利特性
2.2 NSThread
NSThread
是Objective-C中最基本的線程類,它是對pthread的面向對象封裝,提供了創建和管理線程的基本功能。
// 創建并啟動線程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:nil];
thread.name = @"MyCustomThread";
[thread start];// 或者使用類方法創建并自動啟動線程
[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];
優點:
- 簡單直觀,面向對象的API
- 可以直接控制線程的生命周期
缺點:
- 需要手動管理線程生命周期
- 缺乏高級特性,如線程池、任務依賴等
- 線程創建和銷毀的開銷較大
2.3 GCD
GCD是一個強大的并發編程框架,通過任務和隊列的概念簡化了多線程編程。GCD的核心思想是讓開發者關注"做什么"而不是"怎么做"。
2.3.1 GCD的核心概念:
隊列(Queue):負責存儲和管理任務。
- 串行隊列(Serial Queue):按順序執行任務。
- 并行隊列(Concurrent Queue):可以同時執行多個任務。
- 主隊列(Main Queue):在主線程上執行任務,通常用于UI更新。
任務(Task):以Block(代碼塊)形式提交到隊列。
調度方式:
- 同步調度(sync):等待任務完成后返回。
- 異步調度(async):提交任務后立即返回。
下面是GCD的常見用法示例:
// 獲取全局并行隊列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);// 異步執行任務
dispatch_async(globalQueue, ^{// 耗時操作NSData *data = [self fetchDataFromServer];// 在主隊列更新UIdispatch_async(dispatch_get_main_queue(), ^{[self updateUIWithData:data];});
});// 創建自定義串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);// 同步執行任務
dispatch_sync(serialQueue, ^{// 這會阻塞當前線程直到該任務完成[self processData];
});
2.3.2 GCD其他高級功能
如分組(group)、信號量(semaphore)、一次性執行(once)等:
// 使用組管理多個任務
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);// 添加任務到組
dispatch_group_async(group, queue, ^{// 任務1
});
dispatch_group_async(group, queue, ^{// 任務2
});// 等待所有任務完成
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"所有任務已完成");
});
優點:
- 簡潔高效的API
- 自動管理線程池
- 針對多核處理器優化
- 低系統開銷
缺點:
- 相比NSOperation,不支持取消任務
- 不支持任務優先級(舊版本)
- 調試難度相對較高
2.4 NSOperation/NSOperationQueue
NSOperation
和NSOperationQueue
是基于GCD構建的更高級的抽象,提供了面向對象的API和更強大的任務管理能力。
NSOperation是一個抽象類,開發者通常使用其子類:
- NSBlockOperation:用于執行一個或多個Block的操作。
- NSInvocationOperation:用于調用特定對象的選擇器。
- 自定義NSOperation子類:實現復雜任務邏輯。
NSOperationQueue用于管理和執行NSOperation對象:
// 創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4; // 設置最大并發數// 創建操作
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{NSData *imageData = [self downloadImageData];UIImage *image = [UIImage imageWithData:imageData];// 在主隊列更新UI[[NSOperationQueue mainQueue] addOperationWithBlock:^{self.imageView.image = image;}];
}];// 添加完成回調
operation1.completionBlock = ^{NSLog(@"圖片下載完成");
};// 添加操作到隊列
[queue addOperation:operation1];
NSOperation/NSOperationQueue最強大的特性是可以設置操作之間的依賴關系,構建復雜的工作流:
// 創建多個操作
NSBlockOperation *downloadOp = [NSBlockOperation blockOperationWithBlock:^{// 下載圖片
}];NSBlockOperation *filterOp = [NSBlockOperation blockOperationWithBlock:^{// 過濾圖片
}];NSBlockOperation *saveOp = [NSBlockOperation blockOperationWithBlock:^{// 保存圖片
}];// 設置依賴關系
[filterOp addDependency:downloadOp]; // 先下載,再過濾
[saveOp addDependency:filterOp]; // 先過濾,再保存// 添加到隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[downloadOp, filterOp, saveOp] waitUntilFinished:NO];
NSOperation還支持任務的取消、暫停和恢復:
// 取消單個操作
[operation cancel];// 取消隊列中所有操作
[queue cancelAllOperations];// 暫停隊列
queue.suspended = YES;// 恢復隊列
queue.suspended = NO;
優點:
- 面向對象的API
- 支持操作的取消、暫停和恢復
- 支持操作優先級
- 支持操作間依賴關系
- 內置了完成塊(completionBlock)
- KVO兼容,可以觀察操作狀態
缺點:
- 相比GCD,開銷略大
- API相對復雜
- 初始化和配置需要更多代碼
3. iOS中的線程安全方案
多線程編程中,一個關鍵問題是如何確保共享資源的訪問安全。iOS提供了多種鎖機制和同步方案,下面按性能從高到低介紹。
3.1 os_unfair_lock
這是iOS 10引入的鎖機制,用于替代已廢棄的OSSpinLock。它是一種低級互斥鎖,性能極高。
#import <os/lock.h>// 創建鎖
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;// 加鎖
os_unfair_lock_lock(&lock);
// 臨界區代碼
os_unfair_lock_unlock(&lock);// 嘗試加鎖(非阻塞)
if (os_unfair_lock_trylock(&lock)) {// 加鎖成功,執行臨界區代碼os_unfair_lock_unlock(&lock);
} else {// 加鎖失敗
}
適用場景:需要高性能且臨界區操作簡短的場景。
3.2 OSSpinLock (已廢棄)
自旋鎖在等待鎖釋放時會持續嘗試獲取鎖,不會進入休眠狀態。雖然性能很高,但在iOS平臺上存在優先級反轉問題,已被蘋果廢棄。
#import <libkern/OSAtomic.h>// 創建鎖
OSSpinLock lock = OS_SPINLOCK_INIT;// 加鎖
OSSpinLockLock(&lock);
// 臨界區代碼
OSSpinLockUnlock(&lock);
注意:由于存在優先級反轉問題,不推薦使用,應改用os_unfair_lock。
3.3 dispatch_semaphore_t
信號量是一種計數器,可以用來控制訪問共享資源的線程數量。
// 創建信號量,初始值為1(表示互斥鎖)
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);// 等待(減1,如果結果小于0則等待)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 臨界區代碼
// 釋放(加1,如果之前小于0則喚醒等待線程)
dispatch_semaphore_signal(semaphore);
適用場景:需要控制并發訪問數量的場景,不僅限于互斥訪問。
3.4 pthread_mutex
POSIX線程庫提供的互斥鎖,是一種通用的同步機制。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);// 加鎖
pthread_mutex_lock(&mutex);
// 臨界區代碼
pthread_mutex_unlock(&mutex);// 釋放鎖
pthread_mutex_destroy(&mutex);
適用場景:需要可靠互斥且對性能要求不極端高的通用場景。
3.5 dispatch_queue(DISPATCH_QUEUE_SERIAL)
串行隊列可以用作同步機制,確保任務按順序執行。
// 創建串行隊列
dispatch_queue_t queue = dispatch_queue_create("com.example.safeQueue", DISPATCH_QUEUE_SERIAL);// 同步執行(類似于加鎖)
dispatch_sync(queue, ^{// 臨界區代碼
});// 異步執行(非阻塞)
dispatch_async(queue, ^{// 臨界區代碼
});
適用場景:適合需要異步執行且保證順序的場景,更偏向任務編排而非簡單鎖定。
3.6 NSLock
Foundation框架提供的Objective-C互斥鎖類。
NSLock *lock = [[NSLock alloc] init];// 加鎖
[lock lock];
// 臨界區代碼
[lock unlock];// 嘗試加鎖(非阻塞)
if ([lock tryLock]) {// 加鎖成功[lock unlock];
}// 帶超時的加鎖
if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]) {// 在1秒內獲取到鎖[lock unlock];
}
適用場景:需要面向對象API和超時功能的一般場景。
3.7 NSCondition
條件變量和互斥鎖的結合,用于線程間的等待和通知機制。
NSCondition *condition = [[NSCondition alloc] init];// 生產者線程
[condition lock];
// 修改共享數據
[condition signal]; // 或 [condition broadcast]
[condition unlock];// 消費者線程
[condition lock];
while (/* 條件不滿足 */) {[condition wait]; // 等待通知
}
// 臨界區代碼
[condition unlock];
適用場景:生產者-消費者模式等需要線程間通信的場景。
3.8 NSRecursiveLock
遞歸鎖允許同一線程多次獲取鎖,而不會導致死鎖。
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];// 第一次加鎖
[lock lock];
// 可以再次獲取同一把鎖而不會死鎖
[lock lock];
// 臨界區代碼
[lock unlock];
[lock unlock]; // 需要平衡調用unlock
適用場景:需要在遞歸調用或嵌套調用中使用鎖的場景。
3.9 @synchronized
Objective-C提供的語言級同步原語,使用簡單但性能相對較低。
@synchronized(self) {// 臨界區代碼
}
適用場景:對性能要求不高的簡單同步場景,或原型開發。
3.10 原子屬性 (atomic) 實現原理
Objective-C中的屬性可以聲明為atomic,保證讀寫操作的原子性:
@property (atomic, strong) NSString *name;
實現原理:編譯器會為atomic屬性生成訪問器方法,使用自旋鎖/互斥鎖確保讀寫操作的原子性。
限制:atomic只保證單個屬性的讀寫原子性,不保證相關操作的原子性。例如,對數組的原子性讀寫不保證數組內容的訪問也是原子的。
3.11 讀寫安全方案
除了互斥鎖外,還有專門針對讀多寫少場景優化的讀寫鎖:
pthread_rwlock
讀寫鎖允許多個線程同時讀取共享資源,但寫操作需要獨占訪問。
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);// 讀鎖(共享)
pthread_rwlock_rdlock(&rwlock);
// 讀取操作
pthread_rwlock_unlock(&rwlock);// 寫鎖(獨占)
pthread_rwlock_wrlock(&rwlock);
// 寫入操作
pthread_rwlock_unlock(&rwlock);// 銷毀鎖
pthread_rwlock_destroy(&rwlock);
dispatch_barrier_async:異步柵欄調用
GCD提供的柵欄函數可以用于實現高效的讀寫分離:
// 創建并發隊列
dispatch_queue_t queue = dispatch_queue_create("com.example.rwQueue", DISPATCH_QUEUE_CONCURRENT);// 讀操作(多個可并發執行)
dispatch_async(queue, ^{// 讀取操作
});// 寫操作(柵欄函數,保證獨占訪問)
dispatch_barrier_async(queue, ^{// 寫入操作
});
柵欄函數的工作原理是:
- 等待隊列中已有的任務完成
- 獨占式執行柵欄任務
- 柵欄任務完成后,后續的普通任務才能執行
這種方式非常適合讀多寫少的場景,能夠提供極高的并發性能。
4. 常見陷阱
4.1 死鎖情況及預防
死鎖發生在兩個或多個線程互相等待對方釋放鎖的情況。最常見的死鎖場景:
// 主線程
dispatch_sync(dispatch_get_main_queue(), ^{// 這會導致死鎖,因為主線程嘗試同步等待主隊列的任務,// 而主隊列的任務必須等待主線程完成當前執行
});
預防措施:
- 避免在持有鎖時獲取另一個鎖
- 如需多個鎖,按固定順序獲取
- 使用帶超時的鎖獲取方式
- 避免在主線程上同步派發到主隊列
- 使用GCD的dispatch_group或信號量來協調多個異步操作
4.2 優先級反轉
當低優先級線程持有鎖,高優先級線程等待該鎖,而中優先級線程占用CPU時,高優先級線程會被無限期阻塞。
解決方案:
- 使用優先級繼承的鎖機制
- 避免在臨界區執行耗時操作
- 使用合適的隊列優先級
4.3 主線程阻塞引起的UI卡頓
在主線程上執行耗時操作會導致UI無法響應,造成卡頓感:
// 錯誤示例: 主線程同步等待耗時操作
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://example.com/large-file.zip"]];
正確做法:
- 將耗時操作移至后臺線程
- 使用異步API而非同步API
- 合理分解大任務為小任務
- 使用Instruments等工具監測和優化主線程性能
// 正確示例: 異步執行耗時操作,完成后回到主線程更新UI
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://example.com/large-file.zip"]];dispatch_async(dispatch_get_main_queue(), ^{// 更新UI[self.imageView setImage:[UIImage imageWithData:data]];});
});
總結
iOS提供了多種并發編程和線程安全的機制,從底層的pthread到高級的GCD和NSOperation。選擇合適的機制需要考慮以下因素:
- 性能需求:對于性能要求極高的場景,考慮os_unfair_lock或dispatch_semaphore;對于一般場景,NSLock和串行隊列足夠。
- 編程范式:如果偏好面向對象的API,選擇NSOperation和NSLock系列;如果偏好函數式編程,選擇GCD。
- 任務特性:如果需要復雜的任務依賴和取消機制,選擇NSOperation;如果是簡單的并發任務,GCD更簡潔。
- 同步需求:讀多寫少場景推薦讀寫鎖或柵欄函數;需要線程通信的場景適合條件變量。