本文对iOS系统上的异步调用方式进行了深入的讲解。
异步来源
早期,计算机在一个单位时间内的最大运行数目取决于CPU的周期速度。但是,发热和其他物理因素限制了处理器的最大周期速度,因此,芯片工程师尝试通过增加处理器的核数量,从而可以同时执行多个指令。但是,如何利用这些额外的核成为一个需要解决的问题。
现在的操作系统,都可以在任意时间内运行上百甚至更多的程序,因此将这些程序分布在不同的核上运算来利用多核变成可能的。但是,大多数的程序是系统后台驻留或者后台应用,只需要占用很少的处理时间,因此,如何更高效地利用多核成为一个新的需求。
传统的处理方式是建立多个线程。然而,随着核的数量的增加,多线程方式存在很多问题。最大问题是,线程的代码不能随着核的数量变化而变化,无法实时创建跟核数量一样多的线程,且运行正确。另外,多线程之间保证正确地交互也成为一个难题。
异步方法
为了避免开发者疲于解决上述的多线程编程难题,iOS设计了一些异步方法来解决异步问题。
异步函数
异步函数通常用于处理需要长时间的task,异步函数会自动调用一个后台线程,并在运行完成通过通知的形式通知调用者。
举例:
[NSObject performSelectorInBackground:@selector(doSomething) withObject:nil];
GCD
GCD(Grand Central Dispatch)在系统级别处理线程管理,而无需开发者编写代码。开发者只需要将task加入到一个dispatch queue中即可。GCD会自动创建需要的线程去处理task。因为线程处理是系统级别的,因为GCD提供了一种更高效地处理task的方法。
GCD处理task的方式是先进先出,按照添加的顺序执行。
*Type
下面是三种类型:
Type | Description |
---|---|
Serial(private dispatch queues) | 每次只执行一个task |
Concurrent(global dispatch queues) | 每次执行一个或多个task,数量取决于系统条件 |
Main dispatch queues | 主线程的queue,与主线程的RunLoop进行交互 |
当两个线程同时访问一个共享资源时,与其用锁来控制,不如用Serial,保证每次只有一个Operation访问到该资源,效率更高。
*Init
(1)Serial dispatch queue
Serial dispatch queue需要自己创建:
dispatch_queue_t bQueue = dispatch_queue_create("com.baidu.carlife", NULL);
第一个参数是ID,第二个参数为NULL即可。
(2)Global dispatch queue
系统预定义了四个Queue,可以直接调用:
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
第一个参数是优先级值,有high、default、low、background四种,分别对应四个Queue,第二个参数为0即可。
(3)Main dispatch queue
Main dispatch queue已经定义好,直接调用:
dispatch_queue_t cQueue = dispatch_get_main_queue();
*Add
当前线程无需等待执行完成时,用以下方法:
dispatch_async
dispatch_async_f
如果需要block当前线程,调用以下方法:
dispatch_sync
dispatch_sync_f
举例:
dispatch_queue_t aQueue = dispatch_queue_create("com.baidu.carlife", NULL);
dispatch_async(aQueue, ^{
NSLog(@"Block one..");
});
NSLog(@"Block one may or may not finish..");
dispatch_sync(aQueue, ^{
NSLog(@"Block two..");
});
NSLog(@"Both finish..");
运行结果:
Block one may or may not finish..
Block one..
Block two..
Both finish..
注意:不要在dispatch_sync当前正在执行task里面调用,dispatch_sync,否则会出现死锁。
dispatch_queue_t queue = dispatch_queue_create("com.baidu.carlife", nil);
dispatch_sync(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"Dead lock");
});
});
如果涉及到UI的更新,需要切回到主线程执行:
dispatch_async(dispatch_get_main_queue(), ^{
[_tableView reloadData];
});
*Storing Custom Context
Diapatch Queue可以保存一些自定义的上下文信息,参考下面的例子。
*Clean Up Function
Serial dispatch queue可以设置一个回收函数,当它设置了Context,并被析构时,将调用该函数。没有设置Context,将不会调用该函数。
1 | void myFinalizerFunction(void *context) |
*Loop Iterations Concurrently
对于for循环,如果每次的循环结果是独立的,不互相依赖,可以使用dispatch queue的以下方法使其同时运行,提高效率。
dispatch_apply
dispatch_apply_f
注意,上述两个方法是同步的,会阻塞当前线程,但是其执行block代码块是异步的。
举例:
NSLog(@"Begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(5, queue, ^(size_t i) {
NSLog(@"%ld", i);
});
NSLog(@"End");
运行结果为:
Begin
1
2
0
3
4
End
*Suspending & Resuming
挂起Queue,挂起并不能中断一个已经在运行的task,只会阻止新的Operation开始运行,可以调用方法进行挂起或恢复:
dispatch_suspend(queue);
dispatch_resume(queue);
*Dispatch Semaphores(信号量)
Dispatch Semaphores可以用来控制对于有限资源的访问,例如每个应用只能访问有限数量的文件修饰符,但是很多线程要访问,例如一个停车场有三个车位,而有五辆车要停。
举例:
1 | dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2); |
创建Semaphores时,必须设置一个数量变量。每次wait,将减一,而signal,则加一。如果值为负,将挂起当前线程。
*Wait on Groups of Tasks
有时候,需要等待一组异步task完成后,才能进行下一步操作,可以使用dispatch group来完成。
举例:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"Group work..");
});
NSLog(@"Before group..");
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"After group..");
运行结果:
Before group..
Group work..
After group..
*Timer
除了主线程自动设置了RunLoop外,其他线程是没有设置的,这时如果直接调用
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
或者新建NSTimer,都会发现,该方法无效的情况。解决办法是:
dispatch_time_t time=dispatch_time(DISPATCH_TIME_NOW, 5ull *NSEC_PER_SEC);
dispatch_after(time, queue, ^{
// Do something..
}
});
Operation Queues
Operation Queues很像dispatch queue,它会自动处理进行线程管理,开发者只需要定义task,并加入到Operation Queues中即可。
Operation Queues是一种处理NSOperation的队列。
*Init & Add
Operation Queue创建并addOperation后,即开始异步运行Operation。
注意,尽管可以创建任意多个Queue,但并不代表可以同时运行,取决于可获取的CPU核和系统的负载。
举例:
_op = [CustomOperation new];
NSOperationQueue *queue = [NSOperationQueue new];
[queue addOperation:_op];
[queue addOperations:[NSArray arrayWithObject:[CustomOperation new]] waitUntilFinished:NO];
[queue addOperationWithBlock:^{
// Do work..
}];
如果要限制Operation Queue每次只执行一个Operation,可以在add前设置:
[queue setMaxConcurrentOperationCount:1];
*Cancel
当Operation添加到Queue后,不能进行移除操作,要取消,只能调用以下方法。
Operation:cancel
Queue:cancelAllOperations
*WaitForFinished
当需要waitForFinished时,调用以下方法:
Operation:waitUntilFinished
Queue:waitUntilAllOperationsAreFinished
注意,不要在主线程中调用,否则会阻塞主线程。
*Suspending & Resuming
挂起Queue,挂起并不能中断一个已经在运行的task,只会阻止新的Operation开始运行,可以调用方法进行挂起或恢复:
setSuspended:
性能Tips
- 通过计算直接得到值,而不要从内存取:计算值直接使用寄存器,访问速度比内存快;
- 同步改为异步:如果同步任务是因为依赖共享资源,调整资源架构,可能可以通过拷贝资源来实现异步;
- 避免使用锁:使用dispatch queues和operation queues,而尽量避免使用锁;
- 使用系统framework:系统frameworks大多数有异步方法,内部是使用线程和其他技术实现的,而不需要自己编码。