所有的语言都会涉及并发编程,并发就是多个任务同时运行,这也几乎是所有语言最难的地方。iOS 开发中,并发编程主要用于提升 App 的运行性能,保证App实时响应用户的操作。其中我们日常操作的 UI 界面就是运行在主线程之上,是一个串行线程。如果我们将所有的代码放在主线程上运行,那么主线程将承担网络请求、数据处理、图像渲染等各种操作,无论是 GPU 还是内存都会性能耗尽,从而影响用户体验。
本章将从并发编程的理论说起,重点关注 GCD 和 Operations——iOS 开发中最主要的处理并发编程的两套 API。除了理论,实战部分将涉及如何将并发编程运用在实际 App 开发中。最后,本章将涉及如何 debug 并发编程的问题:包括如何观察在 Xcode 中观察线程信息、如何发现并解决编程中的并发问题。
关键词:#NSThread #GCD #Operations
在 iOS 开发中,基本有 3 种方式实现多线程:
关键词:#多任务 #阻塞线程
这 4 个关键词,前两个 Serial/Concurrent 构成一对,后两个 Sync/Async 构成一对。我们分别来看:
关键词:#串行 #同步 #异步
// 串行同步
serialQueue.sync {
print(1)
}
print(2)
serialQueue.sync {
print(3)
}
print(4)
// 串行异步
serialQueue.async {
print(1)
}
print(2)
serialQueue.async {
print(3)
}
print(4)
// 串行异步中嵌套同步
print(1)
serialQueue.async {
print(2)
serialQueue.sync {
print(3)
}
print(4)
}
print(5)
// 串行同步中嵌套异步
print(1)
serialQueue.sync {
print(2)
serialQueue.async {
print(3)
}
print(4)
}
print(5)
首先,在串行队列上进行同步操作,所有任务将顺序发生,所以第一段的打印结果一定是 1234;
其次,在串行队列上进行异步操作,此时任务完成的顺序并不保证。所以可能会打印出这几种结果:1234 ,2134,1243,2413,2143。注意 1 一定在 3 之前打印出来,因为前者在后者之前派发,串行队列一次只能执行一个任务,所以一旦派发完成就执行。同理 2 一定在 4 之前打印,2 一定在 3 之前打印。
接着,对同一个串行队列中进行异步、同步嵌套。这里会构成死锁,所以只会打印出 125 或者 152。
最后,在串行队列中进行同步、异步嵌套,不会构成死锁。这里会打印出 3 个结果:12345,12435,12453。这里1一定在最前,2 一定在 4 前,4 一定在 5 前。
关键词:#并发 #同步 #异步
// 并发同步
concurrentQueue.sync {
print(1)
}
print(2)
concurrentQueue.sync {
print(3)
}
print(4)
// 并发异步
concurrentQueue.async {
print(1)
}
print(2)
concurrentQueue.async {
print(3)
}
print(4)
// 并发异步中嵌套同步
print(1)
concurrentQueue.async {
print(2)
concurrentQueue.sync {
print(3)
}
print(4)
}
print(5)
// 并发同步中嵌套异步
print(1)
concurrentQueue.sync {
print(2)
concurrentQueue.async {
print(3)
}
print(4)
}
print(5)
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群931 542 608来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
首先,在并发队列上进行同步操作,所有任务将顺序执行、顺序完成,所以第一段的打印结果一定是 1234;
其次,在并发队列上进行异步操作,因为并发对列有多个线程 。所以这里只能保证 24 顺序执行,13 乱序,可能插在任意位置:1234,1243,2413 ,2431,2143,2341,2134,2314。
接着,对同一个并发队列中进行异步、同步嵌套。这里不会构成死锁,因为同步操作只会阻塞一个线程,而并发队列对应多个线程。这里会打印出 4 个结果:12345,12534,12354,15234。注意同步操作保证了 3 一定会在 4 之前打印出来。
最后,在并发队列中进行同步、异步嵌套,不会构成死锁。而且由于是并发队列,所以在运行异步操作时也同时会运行其他操作。这里会打印出 3 个结果:12345,12435,12453。这里同步操作保证了 2 和 4 一定在 3 和 5 之前打印出来。
关键词:#竞态条件 #优先倒置 #死锁问题
在并发编程中,一般会面对这样的三个问题:竞态条件、优先倒置、死锁问题。针对 iOS 开发,它们的具体定义为:
var num = 0
DispatchQueue.global().async {
for _ in 1…10000 {
num += 1
}
}
for _ in 1…10000 {
num += 1
}
最后的计算结果 num 很有可能小于 20000,因为其操作为非原子操作。在上述两个线程对num进行读写时其值会随着进程执行顺序的不同而产生不同结果。
var highPriorityQueue = DispatchQueue.global(qos: .userInitiated)
var lowPriorityQueue = DispatchQueue.global(qos: .utility)
let semaphore = DispatchSemaphore(value: 1)
lowPriorityQueue.async {
semaphore.wait()
for i in 0...10 {
print(i)
}
semaphore.signal()
}
highPriorityQueue.async {
semaphore.wait()
for i in 11...20 {
print(i)
}
semaphore.signal()
}
上述代码如果没有 semaphore,高优先权的 highPriorityQueue 会优先执行,所以程序会优先打印完 11 到 20。而加了 semaphore 之后,低优先权的 lowPriorityQueue 会先挂起 semaphore,高优先权的highPriorityQueue 就只有等 semaphore 被释放才能再执行打印。
也就是说,低优先权的线程可以锁上某种高优先权线程需要的资源,从而优于迫使高优先权的线程等待低优先权的线程,这就叫做优先倒置。
let operationA = Operation()
let operationB = Operation()
operationA.addDependency(operationB)
operationB.addDependency(operationA)
还有一种经典的情况,就是在对同一个串行队列中进行异步、同步嵌套:
serialQueue.async {
serialQueue.sync {
}
}
因为串行队列一次只能执行一个任务,所以首先它会把异步 block 中的任务派发执行,当进入到 block 中时,同步操作意味着阻塞当前队列 。而此时外部 block 正在等待内部 block 操作完成,而内部block 又阻塞其操作完成,即内部 block 在等待外部 block 操作完成。所以串行队列自己等待自己释放资源,构成死锁。这也提醒了我们,千万不要在主线程中用同步操作。
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群931 542 608来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
关键词:#竞态条件 #thread sanitizer
func getUser(id: String) throws -> User {
return try storage.getUser(id)
}
func setUser(_ user: User) throws {
try storage.setUser(user)
}
上面这段代码的功能是读写用户信息。乍一看上去没有什么问题,但是一旦多线程涉及到读写,就会产生竞态条件(race condition)。解决方法是打开Xcode中的线程检测工具 thread sanitizer(在 Xcode 的scheme 中勾选 Thread Sanitizer 即可),它会检测出代码中出现竞态条件之处,并提醒我们修改。
对于读写问题,一般有 3 种处理方式。
第 1 种是用串行队列,无论读写,同一时间只能做一个操作,这样就保证了队列的安全。其缺点是速度慢,尤其是在大量读写发生时,每次只能做单个读或写操作的效率实在太低。修改代码如下:
func getUser(id: String) throws -> User {
return serialQueue.sync {
return try storage.getUser(id)
}
}
func setUser(_ user: User) throws {
try serialQueue.sync {
try storage.setUser(user)
}
}
第 2 种是用并发队列配合异步操作完成。异步操作由于会直接返回,所以必须配合逃逸闭包来保证后续操作的合法性。
enum Result<T> {
case value(T)
case error(Error)
}
func getUser(id: String, completion: @escaping (Result<User>) -> Void) {
try serialQueue.async {
do {
user = try storage.getUser(id)
completion(.value(user))
} catch {
completion(.error(error))
}
}
}
func setUser(_ user: User, completion: @escaping (Result<()>) -> Void) {
try serialQueue.async {
do {
try storage.setUser(user)
completion(.value(())
} catch {
completion(.error(error))
}
}
}
第 3 种方法是用并发队列,读操作时用 sync 直接返回结果,写操作时用 barrier flag 来保证此时并发队列只进行当前的写操作(类似将并发队列暂时转为串行队列),而无视其他操作。示例代码如下:
enum Result<T> {
case value(T)
case error(Error)
}
func getUser(id: String) throws -> User {
var user: User!
try concurrentQueue.sync {
user = try storage.getUser(id)
}
return user
}
func setUser(_ user: User, completion: @escaping (Result<()>) -> Void) {
try concurrentQueue.async(flags: .barrier) {
do {
try storage.setUser(user)
completion(.value(())
} catch {
completion(.error(error))
}
}
}
关键词:#异步 #延时 #单例 #线程同步
首先要明确,这几个关键词都是 Objective-C 编程中出现的。它们分别有如下用法:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *image = [client fetchImageFromURL: url];
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
self.title = @”等待”;
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
self.title = @”完成”;
});
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群931 542 608来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
+ (instancetype)sharedManager {
static Manager *sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[Manager alloc] init];
});
return sharedManager;
}
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@”开始做任务1!”);
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@”开始做任务2!”)
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@”任务1和任务2都完成了!”)
});
关键词:#QoS
首先,全局队列肯定是并发队列。如果不指定优先级,就是默认(default)优先级。另外还有 background,utility,user-Initiated,unspecified,user-Interactive。下面按照优先级顺序从低到高来排列:
关键词:#Operation
Operations 是 iOS 中除 GCD 外有一种实现并发编程的方式。它将单个任务算作一个 Operation,然后放在 OperationQueue 队列中进行管理和运行。其中最常用的三个关键词就是 Operation,BlockOperation,OperationQueue。
class ImageFilterOperation: Operation {
var inputImage: UIImage?
var outputImage: UIImage?
override func main() {
outputImage = filter(image: inputImage)
}
}
let multiTasks = BlockOperation()
multiPrinter.completionBlock = {
print("所有任务完成!")
}
multiTasks.addExecutionBlock { print("任务1开始"); sleep(1) }
multiTasks.addExecutionBlock { print("任务2开始"); sleep(2) }
multiTasks.addExecutionBlock { print("任务3开始"); sleep(3) }
multiPrinter.start()
Let multiTaskQueue = OperationQueue()
multiTaskQueue.addOperation { print("任务1开始"); sleep(1) }
multiTaskQueue.addOperation { print("任务2开始"); sleep(2) }
multiTaskQueue.addOperation { print("任务3开始"); sleep(3) }
multiTaskQueue.waitUntilAllOperationsAreFinished()
关键词:#cancel() #isCancelled
在 Operation 抽象类中,有一个 cancel() 方法,它做的唯一一件事情就是将 Operation 的 isCancelled 属性从 false 改为 true。由于它并不会真正去深入代码将具体执行的工作暂停,所以我们必须利用 isCancelled 属性的变化来暂停 main() 方法中的工作。
举个例子,我们有个求和整型数组的操作,其对应 Operation 如下:
class ArraySumOperation: Operation {
let nums: [Int]
var sum: Int
init(nums: [Int]) {
self.nums = nums
self.sum = 0
super.init()
}
override func main() {
for num in nums {
sum += num
}
}
}
如果我们要运行该 Operation,就应该将其加入到 OperationQueue 中:
let queue = OperationQueue()
let sumOperation = ArraySumOperation(nums: Array(1...1000))
// 一旦假如到OperationQueue中,Operation就开始执行
queue.addOperation(sumOperation)
// 打印出500500(1+2+3+...+1000)
sumOperation.completionBlock = { print(sumOperation.sum) }
如果要取消,我们应该调用 cancel() 方法,而它只会将 isCancelled 改成 false,而我们要利用 isCancelled 的状态控制 main() 中的操作,所以可将 ArraySumOperation 改成如下:
class ArraySumOperation: Operation {
let nums: [Int]
var sum: Int
init(nums: [Int]) {
self.nums = nums
self.sum = 0
super.init()
}
override func main() {
for num in nums {
if isCancelled {
return
}
sum += num
}
}
}
此时运行 Operation,就会得到不同结果:
let queue = OperationQueue()
let sumOperation = ArraySumOperation(nums: Array(1...1000))
// 一旦假如到OperationQueue中,Operation就开始执行
queue.addOperation(sumOperation)
sumOperation.cancel()
// sumOperation在彻底完成前已经暂停,sum值小于500500
sumOperation.completionBlock = { print(sumOperation.sum) }
同时 OperationQueue 还有 cancelAllOperations() 方法可以调用,它相当于是对于所有在该队列上工作的 Operation,都调用其 cancel() 方法。
关键词:#UI #耗时
主线程一般用于负责 UI 相关操作,如绘制图层、布局、响应用户响应。很多 UIKit 相关的控件如果不在主线程操作,会产生未知效果。Xcode 中的 Main Thread Checker 可以将相关问题检测出来并报错。
其他线程例如后台线程一般用来处理比较耗时的工作。网络请求、数据解析、复杂计算、图片的编码解码管理等都属于耗时的工作,应该放在其他线程处理。如果放在主线程,由于其是串行队列,会直接阻塞主线程的 UI 操作,直接影响用户体验。
iOS开发人群越来越少,说实在的,每次在后台看到一些读者的回应都觉得很欣慰,至少你们依然坚守iOS技术岗…为了感谢读者们,我想把我收藏的一些编程干货贡献给大家,回馈每一个读者,希望能帮到你们。
干货主要有:
① iOS中高级开发必看的热门书籍(经典必看)
② iOS开发全套面试教学视频
③ BAT等各个大厂iOS面试真题+答案.PDF文档
④ iOS开发高级面试"简历制作"指导
如果你用得到的话可以直接拿走;如何获取,具体内容请转看-我的GitHub
我的:GitHub地址
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有