最早学习 obj-C 时就有看多线程相关的知识,但也只是停留在肤浅的认识中会用两三种常用的异步调用的固定写法而已。当时面对着 cocoa 大量的 API 我更多的是去查文档学会使用这些 API 来实现需求,关于异步开发的一些常用写法而不知道为什么要这么写,也不知道怎么改进或者说改了之后可能会出现哪些问题。
借着公司要求新人巩固 iOS 多线程开发相关知识的机会,笔者准备把自己对这方面了解到的知识梳理一下,顺便用一用最近学的 Swift 巩固一下语法知识。
iOS 的 App 启动之后 App 会默认开启一条主线程并且开启 Runloop 来处理消息,Runloop 相关的知识可以去看 @ibireme 大神的文章《 深入理解 RunLoop 》是我看到最好的介绍 Runloop 相关知识的文章。主线程负责处理 App 的 UI 响应,所以它是串行的,也就是说主线程同一时间只能处理一件事,如果要处理下一件事就必须等当前正在做的事情做完才可以( 阻塞 )。那么如果我们要进行一项耗时的操作,比如遍历 10000 个图片的数组并压缩它们,显然这件事就是 CPU 勤勤恳恳的工作也要一段时间。如果这件事也放在主线程操作的话,那么 UI 就没人处理了,也就是说 APP 会卡顿甚至假死,这显然不是我们想看到的……
这就引入并说明了异步开发的必要性。但是是不是所有操作都再开启一个线程然后丢进去就好了呢?
答案是否定的。
通过确保主线程自由响应用户事件,并发可以很好地提高应用的响应性。通过将工作分配到多核,还能提高应用处理的性能。但是并发也带来一定的额外开销( 调度 ),并且使代码更加复杂,更难编写和调试。
因此在应用设计阶段,就应该考虑并发,设计应用需要执行的任务,及任务所需的数据结构。
我们先来尝试用并发编程解决实际问题,并且实际感受下并发编程。
现在情景模拟,假设现在你的 App 要处理一项耗时巨大的任务,我们把这项任务分为 10 步来操作,每一步操作都要消耗1秒钟的时间,UI 上有一个按钮和一个进度条,当用户点击按钮启动任务,并且将当前任务的完成进度通过进度条反馈给用户。
好了,需求很简单,我们现在来尝试着解决问题,为了加深印象我会给出常见错误代码示例。笔者推荐大家使用 GCD 和 Operation Queues 来实现并发编程,所以只给出这两种代码的示例(每种示例分别给出了 Objective-C 和 Swift 两个实现版本):
Ps : NSLog() 与 println() 的区别。println()会把需要输出的每个字符一个一个的输出到控制台。普通使用并没有问题,可是当多线程同步输出的时候问题就来了,由于很多 println() 同时打印,就会导致控制台上的字符混乱的堆在一起,而NSLog() 就没有这个问题,所以在并发编程时尽量使用 NSLog() 来输出。 更多 NSLog() 与 println() 的区别看这里
第一个常见的错误就是把耗时操作放在主线程处理,导致 App 假死,用户体验糟糕:
// obj-C 错误示范,阻塞了主线程
for (int index = 0; index < 10; ++index) {
sleep(1);
self.progressView.progress = (CGFloat)(index + 1) / 10;
}
// Swift 错误示范,阻塞了主线程
for index in 0...10 {
sleep(1);
progressView.progress = (float_t)(index + 1) / 10;
}
运行效果如图:
第二个常见的错误是把要通过 UI 与用户交互的代码也放在非主线程中使得 UI 无任何变化,用户并不清楚操作到底做了没有,又或者操作进度到了哪里:
// obj-C 错误示范,其他线程不能处理 UI
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int index = 0; index < 10; ++index) {
sleep(1);
self.progressView.progress = (CGFloat)(index + 1) / 10;
}
});
// Swift 错误示范,其他线程不能处理 UI
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
for index in 0...10 {
sleep(1);
self.progressView.progress = (float_t)(index + 1) / 10;
}
}
// obj-C 错误示范,其他线程不能处理 UI
NSOperationQueue *aQueue = [[NSOperationQueue alloc] init];
aQueue.maxConcurrentOperationCount = 1;
[aQueue addOperationWithBlock:^{
for (int index = 0; index < 10; ++index) {
sleep(1);
self.progressView.progress = (CGFloat)(index + 1) / 10;
}
}];
// Swift 错误示范,其他线程不能处理 UI
let aQueue = NSOperationQueue();
aQueue.maxConcurrentOperationCount = 1;
aQueue.addOperationWithBlock { () -> Void in
for index in 0...10 {
sleep(1);
self.progressView.progress = (float_t)(index + 1) / 10;
}
}
运行效果如图:
如果项目中有不得不用到并发编程的需求时,多数情况下用下面代码示例就可以解决,也是我们必须掌握的常用并发编程模板:
// obj-C 正确示范,最常用的 GCD
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int index = 0; index < 10; ++index) {
sleep(1);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"index - %d", index);
self.progressView.progress = (CGFloat)(index + 1) / 10;
});
}
});
// Swift 最常用的 GCD
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
for index in 0...10 {
sleep(1);
dispatch_async(dispatch_get_main_queue(), { () -> Void in
NSLog("index - %d", index);
self.progressView.progress = (float_t)(index + 1) / 10;
})
}
}
// obj-C 下 Operation Queues 对应正确示范
NSOperationQueue *aQueue = [[NSOperationQueue alloc] init];
aQueue.maxConcurrentOperationCount = 1;
for (int index = 0; index < 10; ++index) {
NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
sleep(1);
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"index - %d", index);
self.progressView.progress = (CGFloat)(index + 1) / 10;
}];
}];
[aQueue addOperation:blockOp];
}
// Swift 下 Operation Queues 对应正确示范
let aQueue = NSOperationQueue();
aQueue.maxConcurrentOperationCount = 1;
for index in 0...10 {
let blockOp = NSBlockOperation.init(block: { () -> Void in
sleep(1);
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
NSLog("index - %d", index);
self.progressView.progress = (float_t)(index + 1) / 10;
})
})
aQueue.addOperation(blockOp)
}
下面会逐一展开介绍上面陈列的 4 种方式,给出实现并发的简单代码实例(提供 Objective-C 与 Swift 两个版本)以及列出其在 iOS 中的数据结构(同样提供 Objective-C 与 Swift 两个版本):
POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。
跨平台,适用于多种操作系统,可移植性强。
是一套纯C语言的通用API,且线程的生命周期需要程序员自己管理,使用难度较大,笔者不建议使用。
下面简单给出使用 Pthreads 并发编程的范例代码和 Pthreads 相关数据结构:
使用 Pthreads 来做并发编程一定要记得先包含头文件: #import <pthread.h>
- (IBAction)startBtnClick:(UIButton *)sender {
// 声明一个线程
pthread_t thread;
// 创建一个线程并自动执行
pthread_create(&thread, NULL, start, NULL);
}
// start 函数指针
void *start(void *data) {
NSLog(@"%@", [NSThread currentThread]);
return NULL;
}
使用起来非常困难,需要开发者自己管理 pthread_t 的声明和销毁,一不小心就有可能出问题,比如上面的代码没有销毁 pthread_t thread
这条线程。
Swift 1.x (Old Way)
这里利用 Objective-C Runtime 的技巧来做到这一点:
import CoreMIDI
let block : @objc_block
(UnsafePointer<MIDIPacketList>,
UnsafeMutablePointer<Void>,
UnsafeMutablePointer<Void>) -> Void =
{ (pktlist,readProcRefCon,srcConnRefCon) in
//Your code goes here...
}
let imp : COpaquePointer =
imp_implementationWithBlock(unsafeBitCast(block, AnyObject.self))
let callback : MIDIReadProc = unsafeBitCast(imp, MIDIReadProc.self)
Swift 2.x (New Way)
在 Swift 2.x 的过程中变得不那么骇客(也更具可读性):
import CoreMIDI
let callback : @convention(c) (pktlist : UnsafePointer<MIDIPacketList>,
readProcRefCon : UnsafeMutablePointer<Void>,
srcConnRefCon : UnsafeMutablePointer<Void>) -> Void =
{ (pktlist, readProcRefCon, srcConRefCon) in
}
let usableCallback = unsafeBitCast(callback, MIDIReadProc.self)
pthread_t 在 iOS 中的数据结构如下:
// pthread_t == __darwin_pthread_t == _opaque_pthread_t *
struct _opaque_pthread_t {
long __sig;
struct __darwin_pthread_handler_rec *__cleanup_stack;
char __opaque[__PTHREAD_SIZE__];
};
NSThread
是 Objective-C 的基础框架的一部分,并为开发者提供一种方法来创建和管理线程。
特点:
Ps : 首先,iOS 开发中能遇到的两个线程对象: pthread_t 和 NSThread。过去苹果有份文档/tn/tn2028.html)标明了 NSThread 只是 pthread_t 的封装,但那份文档已经失效了,现在它们也有可能都是直接包装自最底层的 mach thread。苹果并没有提供这两个对象相互转换的接口,但不管怎么样,可以肯定的是 pthread_t 和 NSThread 是一一对应的。比如,你可以通过 pthread_main_thread_np() 或 [NSThread mainThread] 来获取主线程;也可以通过 pthread_self() 或 [NSThread currentThread] 来获取当前线程。
- (IBAction)startBtnClick:(UIButton *)sender {
// 创建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
// 启动
[thread start];
}
- (void)run {
NSLog(@"%@", [NSThread currentThread]);
}
@IBAction func startBtnClick(sender: UIButton) {
// 创建
let thread = NSThread.init(target: self, selector: "run", object: nil)
// 启动
thread.start()
}
func run() {
NSLog("%@", NSThread.currentThread())
}
- (IBAction)startBtnClick:(UIButton *)sender {
// 创建并自动启动
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}
- (void)run {
NSLog(@"%@", [NSThread currentThread]);
}
@IBAction func startBtnClick(sender: UIButton) {
// 创建并自动启动
NSThread.detachNewThreadSelector("run", toTarget: self, withObject: nil)
}
func run() {
NSLog("%@", NSThread.currentThread())
}
- (IBAction)startBtnClick:(UIButton *)sender {
// 使用 NSObject 的方法创建并自动启动
[self performSelectorInBackground:@selector(run) withObject:nil];
}
- (void)run {
NSLog(@"%@", [NSThread currentThread]);
}
@IBAction func startBtnClick(sender: UIButton) {
// 使用 NSObject 的方法创建并自动启动
self.performSelectorInBackground("run", withObject: nil);
}
func run() {
NSLog("%@", NSThread.currentThread())
}
封装好的类内部有很多变量和接口,笔者加入了中文注释,列在这里供大家查阅:
@interface NSThread : NSObject {
@private
id _private;
uint8_t _bytes[44]; // 所占字节
}
// 获得当前所在线程
+ (NSThread *)currentThread;
// 创建并启动线程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
// 是否为多线程
+ (BOOL)isMultiThreaded;
@property (readonly, retain) NSMutableDictionary *threadDictionary;
// 等待一定时间间隔启动线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 退出
+ (void)exit;
// 获取线程优先级
+ (double)threadPriority;
// 设置线程优先级
+ (BOOL)setThreadPriority:(double)p;
@property double threadPriority NS_AVAILABLE(10_6, 4_0); // To be deprecated; use qualityOfService below
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0); // read-only after the thread is started
+ (NSArray<NSNumber *> *)callStackReturnAddresses NS_AVAILABLE(10_5, 2_0);
+ (NSArray<NSString *> *)callStackSymbols NS_AVAILABLE(10_6, 4_0);
// 线程别名,用于 Debug
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
// 线程堆栈大小
@property NSUInteger stackSize NS_AVAILABLE(10_5, 2_0);
// 是否为主线程
@property (readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0);
+ (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
// 获取主线程
+ (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);
// 构造函数
- (instancetype)init NS_AVAILABLE(10_5, 2_0) NS_DESIGNATED_INITIALIZER;
// 构造函数,并添加 SEL
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);
// 线程状态 - 执行中
@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);
// 线程状态 - 执行完毕
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);
// 线程状态 - 被取消
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);
// 取消线程
- (void)cancel NS_AVAILABLE(10_5, 2_0);
// 开始线程
- (void)start NS_AVAILABLE(10_5, 2_0);
// 线程主函数,一般 start 内部会调用 main
- (void)main NS_AVAILABLE(10_5, 2_0); // thread body method
@end
public class NSThread : NSObject {
// 获得当前所在线程
public class func currentThread() -> NSThread
// 创建并启动线程
public class func detachNewThreadSelector(selector: Selector, toTarget target: AnyObject, withObject argument: AnyObject?)
// 是否为多线程
public class func isMultiThreaded() -> Bool
public var threadDictionary: NSMutableDictionary { get }
// 等待一定时间间隔启动线程
public class func sleepUntilDate(date: NSDate)
public class func sleepForTimeInterval(ti: NSTimeInterval)
// 退出
public class func exit()
// 获取线程优先级
public class func threadPriority() -> Double
// 设置线程优先级
public class func setThreadPriority(p: Double) -> Bool
@available(iOS 4.0, *)
public var threadPriority: Double // To be deprecated; use qualityOfService below
@available(iOS 8.0, *)
public var qualityOfService: NSQualityOfService // read-only after the thread is started
@available(iOS 2.0, *)
public class func callStackReturnAddresses() -> [NSNumber]
@available(iOS 4.0, *)
public class func callStackSymbols() -> [String]
// 线程别名,用于 Debug
@available(iOS 2.0, *)
public var name: String?
// 线程堆栈大小
@available(iOS 2.0, *)
public var stackSize: Int
// 是否为主线程
@available(iOS 2.0, *)
public var isMainThread: Bool { get }
@available(iOS 2.0, *)
public class func isMainThread() -> Bool // reports whether current thread is main
@available(iOS 2.0, *)
// 获取主线程
public class func mainThread() -> NSThread
// 构造函数
@available(iOS 2.0, *)
public init()
// 构造函数,并添加 SEL
@available(iOS 2.0, *)
public convenience init(target: AnyObject, selector: Selector, object argument: AnyObject?)
// 线程状态 - 执行中
@available(iOS 2.0, *)
public var executing: Bool { get }
// 线程状态 - 执行完毕
@available(iOS 2.0, *)
public var finished: Bool { get }
// 线程状态 - 被取消
@available(iOS 2.0, *)
public var cancelled: Bool { get }
// 取消线程
@available(iOS 2.0, *)
public func cancel()
// 开始线程
@available(iOS 2.0, *)
public func start()
// 线程主函数,一般 start 内部会调用 main
@available(iOS 2.0, *)
public func main() // thread body method
}
GCD为Grand Central Dispatch的缩写。Grand Central Dispatch (GCD)是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并行任务。在Mac OS X 10.6雪豹中首次推出,也可在IOS 4及以上版本使用。
在深入理解并使用 GCD 前,你应该先掌握下面的知识点:
下面主要对 GCD 相关知识点的展开详解,其中有些地方比较长(例如 Dispatch Source 章节等)有兴趣的同学可以仔细看一下,如果有错误还望提醒我。如果觉得暂时用不到这些知识可以选择性跳过:
Block 是一个代码块,比如一些其它Web编程语言中的“匿名函数”。在 obj-C 中通常使用 Block 实现代理方法实现的功能,也就是回调。使用代理需要设置代理的数据接收者,而且代理方法是被分离开来处理的,Block 可以将这些分离的代码放到一个代码块中。
关于 Block 的知识足够扩充成为一篇独立的文章了,所以笔者在本文中不展开来说,如果想深入的掌握 Block 相关知识请看 Apple 的官方文档
int x = 123;
int y = 456;
void (^aBlock)(int) = ^(int z) {
NSLog(@"%d %d %d/n", x, y, z);
};
Swift 中使用闭包来代替 Block
let x = 123;
let y = 456;
let aClosures = { (z : Int) -> Void in
NSLog("%d %d %d/n", x, y, z);
}
GCD Dispatch Queues 是执行任务的强大工具,允许你同步或异步地执行任意 代码block。原先使用单独线程执行的所有任务都可以替换为使用Dispatch Queues。 而 Dispatch Queues 最大的优点在于使用简单,而且更加高效。
Dispatch Queues 任务的概念就是应用需要执行的一些工作,如计算、创建或 修改数据结构、处理数据等等。我们使用函数或 block 对象来定义任务,并添加 到 Dispatch Queue。
Dispatch Queue 是类似于对象的结构体,管理你提交给它的任务,而且都是 先进先出的数据结构。因此 queue 中的任务总是以添加的顺序开始执行。GCD 提供了几种 Dispatch Queues,不过你也自己创建。
类型 | 描述 |
---|---|
串行队列 | 每次 只执行一个 任务, 按任务添加顺序执行(FIFO) 。当前正在执行的任务在 独立的线程中运行(不同任务的线程可能不同) ,Dispatch Queue 管理了这些线程。 通常 串行 queue 主要用于对特定资源的同步访问(它比锁更轻量好用) 。 你可以创建任意数量的串行 queues,虽然每个 queue 本身每次只能执行一个任务,但是 各个 queue 之间是并发执行 的 |
并发队列 | 可以并发执行一个或多个任务,任务 仍然是以添加到 queue 的顺序启动(这里是开始执行的顺序,但是由于任务耗时以及其他因素,导致执行结果不一定按照 FIFO 的顺序呈现) 。每个任务运行 于独立的线程中,Dispatch Queue 管理所有线程。 同时运行的任务数量随时都会变化 ,而且依赖于系统条件。 你可以创建并发 Queues,但是笔者建议在开发中最好使用已经定义好的全局并发 queues |
主队列 | 全局可用的串行 queue,在应用主线程中执行任务。这个 queue 与应用的 RunLoop 交叉执行,通常用于应用的关键同步点。 |
在创建和管理 queue 之前先讲怎么样获得系统的并发 queue
dispatch_queue_create
函数用于创建 queue,两个参数分别是 queue 名和一组 queue 属性。 调试器和性能工具会显示 queue 的名字,便于你跟踪任务的执行。
//OBJECTIVE-C
//串行队列
dispatch_queue_t queue = dispatch_queue_create("", NULL);
dispatch_queue_t queue = dispatch_queue_create("test.Lision.testQueue", DISPATCH_QUEUE_SERIAL);
//并行队列
dispatch_queue_t queue = dispatch_queue_create("test.Lision.testQueue", DISPATCH_QUEUE_CONCURRENT);
//SWIFT
//串行队列
let queue = dispatch_queue_create("test.Lision.testQueue", nil);
let queue = dispatch_queue_create("test.Lision.testQueue", DISPATCH_QUEUE_SERIAL)
//并行队列
let queue = dispatch_queue_create("test.Lision.testQueue", DISPATCH_QUEUE_CONCURRENT)
Ps : 使用 dispatch_get_current_queue
函数作为调试用途,或者测试当前 queue 的标识。在 block 对象中调用这个函数会返回 block 提交到的 queue (这个时候 queue 应该正在执行中)。
GCD 提供函数,让应用访问几个公共 Dispatch Queue:
dispatch_get_main_queue
函数获得应用主线程关联的串行 dispatch queue。Cocoa 应用、调用了 dispatch_main
函数或配置了 run loop (CFRunLoopRef 类型 或一个 NSRunLoop 对象)的应用,会自动创建这个 queue。 DISPATCH_QUEUE_PRIORITY_LOW
, DISPATCH_QUEUE_PRIORITY_DEFAULT
, DISPATCH_QUEUE_PRIORITY_HIGH
。 下面演示如何获取默认优先级的并发 queue:
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
let aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
Ps : 你不需要存储这三个 queue 的引用,每次都直接调用 dispatch_get_global_queue 获得 queue 就行了。
所有 dispatch 对象(包括 dispatch queue)都允许你关联 custom context data。 使用 dispatch_set_context
和 dispatch_get_context
函数来设置和获取对象的上下文数据。 系统不会使用你的上下文数据,所以需要你自己在适当的时候分配和销毁这些数据。
对于 Queue,你可以使用上下文数据来存储一个指针,指向 Objective-C 对象或其它数据结构,协助标识这个 queue 或代码的其它用途。
在创建 dispatch queue 之后,可以附加一个 finalizer
函数,在 queue 被销毁之前执行自定义的清理操作。使用 dispatch_set_finalizer_f
函数为 queue 指定一个清理函数, 当 queue 的引用计数到达 0 时(ARC 下虽然你看不到了但是原理依然如此),且只有上下文指针不为 NULL 时 才会调用这个清理函数。
下面例子演示了自定义 finalizer
函数:
// 当 queue 的引用计数到达 0 时执行清理函数
void myFinalizerFunction(void *context) {
char *theData = (char *)context;
printf("myFinalizerFunction - data = %s/n", theData);
// 具体清理细节可以另写一个函数
myCleanUpDataContextFunction(theData);
}
// 具体清理细节
void myCleanUpDataContextFunction(char *data) {
printf("myCleanUpDataContextFunction - data = %s/n", data);
}
// 具体初始化细节
void myInitializeDataContextFunction(char **data) {
*data = "Lision";
printf("myInitializeDataContextFunction - data = %s/n", *data);
}
// 自定义创建队列函数
dispatch_queue_t createMyQueue() {
char *data = (char *) malloc(sizeof(char));
myInitializeDataContextFunction(&data);
// 创建队列并为其设置上下文
dispatch_queue_t serialQueue = dispatch_queue_create("test.Lision.CriticalTaskQueue", NULL);
if (serialQueue) {
dispatch_set_context(serialQueue, data);
dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);
}
return serialQueue;
}
#pragma mark - Foundations
- (IBAction)startBtnClick:(UIButton *)sender {
// 通过自定义函数创建队列
dispatch_queue_t queue = createMyQueue();
// 异步执行队列,并在队列中修改上下文
dispatch_async(queue, ^{
char *name = dispatch_get_context(queue);
name = "LiXin";
dispatch_set_context(queue, name);
});
}
这里就先不提供 Swift 的实现了,感兴趣的同学可以自己把上面的 Objective-C 代码用 Swift 写出来。
要执行一个任务,你需要将它 dispatch 到一个适当的 dispatch queue,你可以同步或异步地 dispatch 一个任务,也可以单个或按组(group)来 dispatch。 一旦进入到 queue,queue 会负责尽快地执行你的任务。(这时候你就很难做主了)
你可以异步或同步地添加一个任务到 Queue (异步与同步的区别就是是否阻塞当前线程) 。
尽可能地使用 dispatch_async
或 dispatch_async_f
函数异步地 dispatch 任务。因为添加任务到 Queue 中时,无法确定这些代码什么时候能够执行。因此异步地添加 block 或函数,可以让你立即调度这些代码的执行,然后调用线程可以继续去做其它事情。
少数时候你可能希望同步地 dispatch 任务,以避免竞争条件或其它同步错误。使用 dispatch_sync
和 dispatch_sync_f
函数同步地添加任务到 Queue,这两个函数会阻塞当前线程,直到相应任务完成执行。
dispatch_sync
或 dispatch_sync_f
函数,并同步 dispatch 新任务到当前正在执行的 queue。对于串行 queue 这一点特别重要,因为这样做肯定会导致死锁;而并发 queue 也应该避免这样做,否则虽然并发 queue 不去引起运行时错误,但是被锁的部分永远不会被执行到。 // 自定义串行队列
dispatch_queue_t myCustomQueue = dispatch_queue_create("test.Lision.MyCustomQueue", NULL);
// 异步执行
dispatch_async(myCustomQueue, ^{
NSLog("Do some work here./n");
});
// 由于上面是异步执行操作,所以很难知道下面的打印和上面异步操作中的打印谁先谁后
NSLog("The first block may or may not have run./n");
// 同步执行串行队列,好吧其实这串行队列幸好不是当前队列-主队列,否则程序死锁
// 而下面这样写也好不到哪里去,因为它就相当于直接顺序打印一句话,根本不需要加队列
dispatch_sync(myCustomQueue, ^{
NSLog("Do some more work here./n");
});
// 由于上面的操作是同步操作会阻塞当前线程,所以执行下面的打印时上面的操作肯定是已经完毕的
NSLog("Both blocks have completed./n");
// 自定义串行队列
let myCustomQueue = dispatch_queue_create("test.Lision.MyCustomQueue", nil);
// 异步执行
dispatch_async(myCustomQueue, { () -> Void in
NSLog("Do some work here./n");
});
// 由于上面是异步执行操作,所以很难知道下面的打印和上面异步操作中的打印谁先谁后
NSLog("The first block may or may not have run./n");
// 同步执行串行队列,好吧其实这串行队列幸好不是当前队列-主队列,否则程序死锁
// 而下面这样写也好不到哪里去,因为它就相当于直接顺序打印一句话,根本不需要加队列
dispatch_sync(myCustomQueue, { () -> Void in
NSLog("Do some more work here./n");
});
// 由于上面的操作是同步操作会阻塞当前线程,所以执行下面的打印时上面的操作肯定是已经完毕的
NSLog("Both blocks have completed./n");
dispatch 到 queue 中的任务,通常与创建任务的代码独立运行。在任务完成时,应用可能希望得到通知并使用任务完成的结果数据。在传统的异步编程模型中,你可能会使用回调机制,不过 dispatch queue 允许你使用 Completion Block。
Completion Block 是你 dispatch 到 queue 的另一段代码,在原始任务完成时自动执行。调用代码在启动任务时通过参数提供 Completion Block。任务代码只需要在完成工作时提交指定的 Block 或函数到指定的 queue。
这里我们拿我们最早的实例来说明,Completion Block 就是异步 queue 的任务执行完毕后自动执行的。就是在任务代码完成时把更新 ui 的 Block 提交给了主线程 queue:
// obj-C 正确示范,最常用的 GCD
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int index = 0; index < 10; ++index) {
sleep(1);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"index - %d", index);
self.progressView.progress = (CGFloat)(index + 1) / 10;
});
}
});
// Swift 最常用的 GCD
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
for index in 0...10 {
sleep(1);
dispatch_async(dispatch_get_main_queue(), { () -> Void in
NSLog("index - %d", index);
self.progressView.progress = (float_t)(index + 1) / 10;
})
}
}
如果你使用循环执行固定次数的迭代,并发 dispatch queue 可能会提高性能。
例如下面 for 循环:
for (int i = 0; i < 10; ++i) {
NSLog(@"%d", i);
}
for i in 0...10 {
NSLog("%d", i);
}
如果 每次迭代执行的任务与其它迭代独立无关,而且循环迭代执行顺序也无关紧要 的话,你可以调用 dispatch_apply
或 dispatch_apply_f
函数来替换循环。这两个函数为每次循环迭代将指定的 block 或函数提交到 queue。当 dispatch 到并发 queue 时,就有可能同时执行多个循环迭代。
调用 dispatch_apply
或 dispatch_apply_f
时你虽然可以指定串行或并发 queue。 并发 queue 允许同时执行多个循环迭代,而串行 queue 就没太大必要使用了。
需要注意:这两个函数会 阻塞当前线程 ,而且和普通 for 循环一样, dispatch_apply
和 dispatch_apply_f
函数也是在所有迭代完成之后才会返回 。所以如果你传递的参数是串行 queue,而且正是执行当前代码的 Queue, 就会产生死锁。主线程中调用这两个函数必须小心,可能会阻止事件处理循环并无法响应用户事件。
下面代码使用 dispatch_apply
替换了 for 循环,你传递的 block 必须包含一个参数,用来标识当前循环迭代。第一次迭代这个参数值为 0,第二次时为 1,最后一次值为 count - 1。
// 公用异步队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 异步执行
dispatch_apply(10, queue, ^(size_t i) {
NSLog(@"%zu", i);
});
// 公用异步队列
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
// 异步执行
dispatch_apply(10, queue) { (i) -> Void in
NSLog("%zu", i);
}
Ps : 如果应用消耗大量内存,并且创建大量对象,你需要创建自己的 autorelease pool,用来及时地释放不再使用的对象。
我们可以暂停一个 queue 以阻止它执行 block 对象,使用 dispatch_suspend
函数挂起一个 dispatch queue;使用 dispatch_resume
函数继续 dispatch queue。调 用 dispatch_suspend
会增加 queue 的挂起计数,调用 dispatch_resume 则减少queue 的挂起计数。当挂起计数大于 0 时,queue 就保持挂起状态。因此你必须对应地调用 suspend 和 resume 函数。
挂起和继续是异步的,而且只在执行 block 之间生效。挂起一个 queue 不会导致正在执行的 block 停止。
使用 Dispatch Queue 实现应用并发时,也需要注意线程安全性:
dispatch_sync
函数调度相同的 queue,这样做会死锁这个 queue。如果你需要 dispatch 到当前 queue,需要使用 dispatch_async
函数异步调度。 类似于传统的 semaphore(信号量),但是更加高效。只有当调用线程由于信号量不可用,需要阻塞时,Dispatch semaphore 才会去调用内核。如果信号量可用,就不会与内核进行交互。
使用信号量可以实现对有限资源的访问控制:
如果提交到 Dispatch Queue 中的任务需要访问某些有限资源,可以使用 Dispatch Semaphore 来控制同时访问这个资源的任务数量。Dispatch Semaphore 和普通的信号量类似,唯一的区别是当资源可用时,需要更少的时间来获得 Dispatch Semaphore。
使用 Dispatch Semaphore 的过程如下:
dispatch_semaphore_create
函数创建 semaphore,指定正数值表示 资源的可用数量。 dispatch_semaphore_wait
来等待 semaphore dispatch_semaphore_signal
函数释放和 signal 这个 semaphore // 创建信号,指定初始池大小
dispatch_semaphore_t sema = dispatch_semaphore_create(getdtablesize() / 2);
// 等待一个可用的文件描述符
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
// code 这里写代码获取文件并对其进行操作
// 完成后释放文件描述符
// code 这里写代码关闭文件
dispatch_semaphore_signal(sema);
// 创建信号,指定初始池大小
let sema = dispatch_semaphore_create((Int)(getdtablesize() / 2));
// 等待一个可用的文件描述符
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
// code 这里写代码获取文件并对其进行操作
// 完成后释放文件描述符
// code 这里写代码关闭文件
dispatch_semaphore_signal(sema);
用于监控一组 Block 对象完成(你可以同步或异步地监控 block)。Group 提供了一个非常有用的同步机制,你的代码可以等待其它任务的完成。
Dispatch group 用来阻塞一个线程,直到一个或多个任务完成执行。有时候 你必须等待任务完成的结果,然后才能继续后面的处理。dispatch group 也可以替代线程 join。
基本的流程是设置一个组,dispatch 任务到 queue,然后等待结果。你需要使用 dispatch_group_async
函数,会关联任务到相关的组和 queue。使用 dispatch_group_wait
等待一组任务完成。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
// 把 queue 加入到 group
dispatch_group_async(group, queue, ^{
// 一些异步操作任务
});
// code 你可以在这里写代码做一些不必等待 group 内任务的操作
// 当你在 group 的任务没有完成的情况下不能做更多的事时,阻塞当前线程等待 group 完工
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let group = dispatch_group_create()
// 把 queue 加入到 group
dispatch_group_async(group, queue) { () -> Void in
// 一些异步操作任务
}
// code 你可以在这里写代码做一些不必等待 group 内任务的操作
// 当你在 group 的任务没有完成的情况下不能做更多的事时,阻塞当前线程等待 group 完工
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
Dispatch Source 在特定类型的系统事件发生时,会产生通知。你可以使用 Dispatch Source 来监控各种事件,如:进程通知、信号、描述符事件、等等。当事件发生时,Dispatch Source 异步地提交你的任务到指定的 Dispatch Queue 来进行处理。
现代系统通常提供异步接口,允许应用向系统提交请求,然后在系统处理请求时应用可以继续处理自己的事情。GCD 正是基于这个基本行为而设计,允许你提交请求,并通过 Block 和 Dispatch Queue 报告结果。
Dispatch Source 是基础数据类型,协调特定底层系统事件的处理。
GCD 支持 以下 Dispatch Source:
Dispatch source 替代了异步回调函数,来处理系统相关的事件。当你配置一个 dispatch source 时,你指定要监测的事件、dispatch queue、以及处理事件的代码(block 或函数)。当事件发生时,dispatch source 会提交你的 block 或函数到 指定的 queue 去执行。和手工提交到 queue 的任务不同,dispatch source 为应用提供连续的事件源。 除非你显式地取消,否则dispatch source 会一直保留与 dispatch queue 的关联。只要相应的事件发生,就会提交关联的代码到 dispatch queue 去执行。
创建 Dispatch Source 需要同时创建事件源和 Dispatch Source 本身。事件源是处理事件所需要的 native 数据结构,例如基于描述符的 Dispatch Source,你需要打开描述符;基于进程的事件,你需要获得目标程序的进程 ID。
可以按下面的步骤创建 Dispatch Source:
dispatch_source_create
函数创建 Dispatch Source 配置 Dispatch Source:
dispatch_source_set_timer
函数设置定时器信息 为 Dispatch Source 赋予一个取消处理器(可选)
dispatch_resume
函数开始处理事件 由于 Dispatch Source 必须进行额外的配置才能被使用, dispatch_source_create
函数返回的 Dispatch Source 将处于挂起状态。此时 Dispatch Source 会接收事件, 但是不会进行处理。 这时候你可以安装事件处理器,并执行额外的配置。
你需要定义一个事件处理器来处理事件,可以是函数或 block 对象,并使用 dispatch_source_set_event_handler
或 dispatch_source_set_event_handler_f
安装事件处理器。事件到达时,Dispatch Source 会提交你的事件处理器到指定的 Dispatch Queue,由 queue 执行事件处理器。
函数事件处理器有一个 context 指针指向 Dispatch Source 对象,没有返回值。 Block 事件处理器没有参数,也没有返回值。
// 基于 Block 的事件处理
void (^dispatch_block_t)(void)
// 基于函数的事件处理
void (*dispatch_function_t)(void *)
// 基于 Block 的事件处理
@convention(block) () -> Void
// 基于函数的事件处理
void (*dispatch_function_t)(void *)
在事件处理器中,你可以从 Dispatch Source 中获得事件的信息,函数处理器可以直接使用参数指针,Block 则必须自己捕获到 Dispatch Source 指针,一般 block 定义时会自动捕获到外部定义的所有变量。
// 创建 dispatch_queue_t
dispatch_queue_t myQueue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
// 创建 dispatch_source_t
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, "myDescriptor", 0, myQueue);
dispatch_source_set_event_handler(source, ^{
// 从上下文中获取信息
size_t estimated = dispatch_source_get_data(source);
});
dispatch_resume(source);
// 创建 dispatch_queue_t
let myQueue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL)
// 创建 dispatch_source_t
let source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, 0, 0, myQueue)
dispatch_source_set_event_handler(source) { () -> Void in
// 从上下文中获取信息
let estimated = dispatch_source_get_data(source);
}
dispatch_resume(source);
Block 捕获外部变量允许更大的灵活性和动态性。当然,在 Block 中这些变量默认是只读的,虽然可以使用 __block
来修改捕获的变量,但是你最好不要在事件处理器中这样做。因为 Dispatch source 异步执行事件处理器,当事件处理器修改原始外部变量时,有可能这些变量已经不存在了。
下面是事件处理器能够获得的事件信息:
函数 | 描述 |
---|---|
dispatch_source_get_handle |
这个函数返回 dispatch source 管理的底层系统数据类型;对于描述符 dispatch source,函数返回一个 int,表示关联的描述符;对于信号 dispatch source,函数返回一个 int 表示最新事件的信号数值;对于进程 dispatch source,函数返回一个 pid_t 数据结构,表示被监控的进程;对于 Mach port dispatch source,函数返回一个 mach_port_t 数据结构;对于其它 dispatch source,函数返回的值未定义 |
dispatch_source_get_data |
这个函数返回事件关联的所有未决数据。对于从文件中读取数据的描述符 dispatch source,这个函数返回可以读取的字节数;对于向文件中写入数据的描述符 dispatch source,如果可以写入,则返回正数值;对于监控文件系统活动的描述符 dispatch source,函数返回一个常量,表示发生的事件类型,参考 dispatch_source_vnode_flags_t 枚举类型;对于进程 dispatch source,函数返回一个常量,表示发生的事件类型,参考 dispatch_source_proc_flags_t 枚举类型;对于 Mach port dispatch source,函数返回一个常量,表示发生的事件类型,参考 dispatch_source_machport_flags_t 枚举类型;对于自定义 dispatch source,函数返回从现有数据创建的新数据,以及传递给 dispatch_source_merge_data 函数的新数据。 |
dispatch_source_get_mask |
这个函数返回用来创建 dispatch source 的事件标志;对于进程 dispatch source,函数返回 dispatch source 接收到的事件掩码,参考 dispatch_source_proc_flags_t 枚举类型;对于发送 Mach port dispatch source,函数返回期望事件的掩码,参考 dispatch_source_mach_send_flags_t 枚举类型;对于自定义 dispatch source,函数返回用来合并数据值的掩码。 |
取消处理器在 dispatch soruce 释放之前执行清理工作。多数类型的 dispatch source 不需要取消处理器,除非你对 dispatch source 有自定义行为需要在释放时执行。 但是使用描述符或 Mach port 的 dispatch source 必须设置取消处理器,用来关闭描述符或释放 Mach port。否则可能导致微妙的 bug,这些结构体会被系统其它部分或你的应用在不经意间重用。
你可以在任何时候安装取消处理器,但 通常我们在创建 dispatch source 时就会安装取消处理器 。使用 dispatch_source_set_cancel_handler
或 dispatch_source_set_cancel_handler_f
函数来设置取消处理器。
下面取消处理器关闭描述符:
// 创建 dispatch_queue_t
dispatch_queue_t myQueue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
// 创建 dispatch_source_t
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, "myDescriptor", 0, myQueue);
dispatch_source_set_event_handler(source, ^{
// 从上下文中获取信息
size_t estimated = dispatch_source_get_data(source);
});
// 看这里,这里是新增代码!!!
dispatch_source_set_cancel_handler(source, ^{
// 这里写取消处理器实现代码
});
dispatch_resume(source);
// 创建 dispatch_queue_t
let myQueue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL)
// 创建 dispatch_source_t
let source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, 0, 0, myQueue)
dispatch_source_set_event_handler(source) { () -> Void in
// 从上下文中获取信息
let estimated = dispatch_source_get_data(source);
}
// 看这里,这里是新增代码!!!
dispatch_source_set_cancel_handler(source) { () -> Void in
// 这里写取消处理器实现代码
}
dispatch_resume(source);
前面说过在创建 dispatch source 时可以指定一个 queue,用来执行事件处理器和取消处理器。同样你也可以使用 dispatch_set_target_queue
函数在任何时候修改目标 queue。 修改 queue 可以改变执行 dispatch source 事件的优先级。
修改 dispatch source 的目标 queue 是异步操作,dispatch source 会尽可能快地完成这个修改,如果事件处理器已经进入 queue 并等待处理,它会继续在原来 的 Queue 中执行。随后到达的所有事件的处理器都会在后面修改的 queue 中执行。
和 GCD 的其它类型一样,你可以使用 dispatch_set_context
函数关联自定义数据到 dispatch source。使用 context 指针存储事件处理器需要的任何数据。 如果你在 context 指针中存储了数据,你就应该安装一个取消处理器,在 dispatch source 不再需要时释放这些 context 自定义数据。
如果你使用 block 实现事件处理器,你也可以捕获本地变量,并在 Block 中使用。虽然这样也可以代替 context 指针,但是你应该明智地使用 Block 捕获变量。 因为 dispatch source 长时间存在于应用中,Block 捕获指针变量时必须非常小心,因为指针指向的数据可能会被释放。
不管使用哪种方法,你都应该提供一个取消处理器,在最后释放这些数据。
除非你显式地调用 dispatch_source_cancel
函数,dispatch source 将一直保持活动,取消一个 dispatch source 会停止递送新事件,并且不能撤销。
取消一个 dispatch source 是异步操作,调用 dispatch_source_cancel
之后, 不会再有新的事件被处理,但是正在被 dispatch source 处理的事件会继续被处理完成。在处理完最后的事件之后,dispatch source 会执行自己的取消处理器。
取消处理器是你最后的执行机会,在那里执行内存或资源的释放工作。例如描述符或 mach port 类型的 dispatch source,必须提供取消处理器,用来关闭描述符或 mach port。
你可以使用 dispatch_suspend
和 dispatch_resume
临时地挂起和继续 dispatch source 的事件递送。这两个函数分别增加和减少 dispatch 对象的挂起计数。 因此,你必须每次 dispatch_suspend 调用之后,都需要相应的 dispatch_resume 才能继续事件递送。
再次强调一下事件合并:挂起一个 dispatch source 期间,发生的任何事件都会被累积,直到 dispatch source 继续。但是不会递送所有事件,而是先合并到单一事件,然后再一次递送。例如你监控一个文件的文件名变化,就只会递送最后一次的变化事件。
Operation && Operation Queues 是与 GCD 中的 Block 和 Dispatch Queues 对应的。
Operation Queues 是 Cocoa 版本的并发 dispatch queue,由 NSOperationQueue 类实现。 dispatch queue 总是按先进先出的顺序执行任务,而 Operation Queues 在确定任务执行顺序时,还会考虑其它因素。最主要的一个因素是指定任务是否依赖于另一个任务的完成。你在定义任务时配置依赖性,从而创建复杂的任务执行顺序图。
提交到 Operation Queues 的任务必须是 NSOperation 对象,operation object 封装了你要执行的工作,以及所需的所有数据。由于 NSOperation 是一个抽象基类,通常你需要定义自定义子类来执行任务。不过 Foundation framework 自带了一些具体子类,你可以创建并执行相关的任务。
Operation objects 会产生 key-value observing(KVO)通知,对于监控任务的进程非常有用。虽然 operation queue 总是并发地执行任务,但你可以使用依赖,在需要时确保顺序执行。
Operation 对应 GCD 中的 Block。
operation object 是 NSOperation 类的实例,封装了应用需要执行的任务,和执行任务所需的数据。NSOperation 本身是抽象基类,我们必须实现子类。Foundation framework 提供了两个具体子类,你可以直接使用:
类 | 描述 |
---|---|
NSInvocationOperation | 可以直接使用的类,基于应用的一个对象和 selector 来创建 operation object。如果你已经有现有的方法来执行需要的任务,就可以使用这个类。 |
NSBlockOperation | 可以直接使用的类,用来并发地执行一个或多个 block 对象。operation object 使用“组”的语义来执行多个 block 对象,所有相关的 block 都执行完成之后,operation object 才算完成。 |
NSOperation | 基类,用来自定义子类 operation object。继承 NSOperation 可以完全控制 operation object 的实现,包括修改操作执行和状态报告的方式。 |
所有 operation objects 都支持以下关键特性:
通常我们通过将 operation 添加到 operation queue 中来执行该操作。但是我们也可以手动调用 start 方法来执行一个 operation 对象,这样做不保证 operation 会并发执行。NSOperation 类对象的 isConcurrent
方法告诉你这个 operation 相对于调用 start 方法的线程,是同步还是异步执行的。 isConcurrent
方法默认返回 NO,表示 operation 与调用线程同步执行。
如果你需要实现并发 operation,也就是相对调用线程异步执行的操作。你必须添加额外的代码,来异步地启动操作。例如生成一个线程、调用异步系统函数,以确保 start
方法启动任务,并立即返回。
多数开发者从来都不需要实现并发 operation 对象,我们只需要将 operations 添加到 operation queue。当你提交非并发 operation 到 operation queue 时,queue 会创建线程来运行你的操作,因此也能达到异步执行的目的。只有你不希望使用 operation queue 来执行 operation 时,才需要定义并发 operations。
所以笔者建议后面的自定义 Operation 对象有兴趣的同学可以看,觉得浪费时间或者暂时用不到的同学可以跳过。
如果已经现有一个方法,需要并发地执行,就可以直接创建 NSInvocationOperation 对象,而不需要自己继承 NSOperation。
- (void)myTaskMethod {
NSLog(@"Do task!");
}
#pragma mark - Foundations
- (IBAction)startBtnClick:(UIButton *)sender {
NSInvocationOperation *theOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(myTaskMethod) object:nil];
[theOp start];
}
在 Swift 构建的和谐社会里,是容不下 NSInvocationOperation 这种不是类型安全的类的。苹果如是说。这里有 相关解释 。
NSBlockOperation 对象用于封装一个或多个 block 对象,一般创建时会添加至少一个 block,然后再根据需要添加更多的 block。
当 NSBlockOperation 对象执行时,会把所有 block 提交到默认优先级的并发 dispatch queue。然后 NSBlockOperation 对象等待所有 block 完成执行,最后标记自己已完成。因此可以使用 block operation 来跟踪一组执行中的 block,有点类似于 thread join 等待多个线程的结果。区别在于 block operation 本身也运行在一个单独的线程,应用的其它线程在等待 block operation 完成时可以继续工作。
NSBlockOperation *theOp = [NSBlockOperation blockOperationWithBlock: ^{
NSLog(@"Beginning operation./n");
}];
[theOp start];
let theOp = NSBlockOperation.init(block: { () -> Void in
NSLog("Beginning operation./n")
})
theOp.start()
使用 addExecutionBlock:
可以添加更多 block 到这个 block operation 对象。如果需要顺序地执行 block,你必须直接提交到所需的 dispatch queue。
前面的 NSInvocationOperation 和 NSBlockOperation 相信已经可以满足一般的需求了,下面的自定义 Operation 对象可能有些深入,篇幅也比较长。感兴趣的同学可以选择继续看下去,觉得暂时用不到的同学也可以跳过本章节。
如果 block operation 和 invocation operation 对象不符合应用的需求,你可以直接继承 NSOperation,并添加任何你想要的行为。NSOperation 类提供通用的子类继承点,而且实现了许多重要的基础设施来处理依赖 和 KVO 通知。继承所需的工作量主要取决于你要实现非并发还是并发的 operation。
定义非并发 operation 要简单许多,只需要执行主任务,并正确地响应取消事件;NSOperation 处理了其它所有事情。对于并发 operation,你必须替换某些现有的基础设施代码。
每个 operation 对象至少需要实现以下方法:
你也可以选择性地实现以下方法:
@interface MyNonConcurrentOperation : NSOperation {
id myData;
}
-(id)initWithData:(id)data;
@end
@implementation MyNonConcurrentOperation
- (id)initWithData:(id)data {
if (self = [super init])
myData = data;
return self;
}
-(void)main {
@try {
} @catch(...) {
// Do not rethrow exceptions.
}
}
@end
class MyNonConcurrentOperation : NSOperation {
var myData : AnyObject
init(data: AnyObject) {
self.myData = data
}
override func main() {
do {
}catch {
}
}
}
operation 开始执行之后,会一直执行任务直到完成,或者显式地取消操作。取消可能在任何时候发生,甚至在 operation 执行之前。尽管 NSOperation 提供了一个方法,让应用取消一个操作,但是识别出取消事件则是你的事情。如果 operation 直接终止,可能无法回收所有已分配的内存或资源。因此 operation 对象需要检测取消事件,并优雅地退出执行。
operation 对象定期地调用 isCancelled
方法,如果返回 YES(表示已 取消),则立即退出执行。不管是自定义 NSOperation 子类,还是使用系统提供的两个具体子类,都需要支持取消。 isCancelled
方法本身非常轻量,可以频繁地调用而不产生大的性能损失。
以下地方可能需要调用 isCancelled
:
- (void)main {
@try {
BOOL isDone = NO;
while (![self isCancelled] && !isDone) {
// Do some work and set isDone to YES when finished
}
}
@catch(...) {
// Do not rethrow exceptions.
}
}
override func main() {
do {
let isDone = false
while (!self.cancelled && !isDone) {
// Do some work and set isDone to YES when finished
}
}catch {
}
}
Ps : 注意你的代码还需要完成所有相关的资源清理工作
Operation 对象默认按同步方式执行,也就是在调用 start
方法的那个线程中直接执行。由于 operation queue 为非并发 operation 提供了线程支持,对应用来说,多数 operations 仍然是异步执行的。但是如果你希望手工执行 operations,而且仍然希望能够异步执行操作,你就必须采取适当的措施,通过定义 operation 对象为并发操作来实现。
方法 | 描述 |
---|---|
start | (必须)所有并发操作都必须覆盖这个方法,以自定义的实现替换默认行为。手动执行一个操作时,你会调用 start 方法。因此你对这个方法的实现是操作的起点,设置一个线程或其它执行环境,来执行你的任务。你的实现在任何时候都绝对不能调用 super。 |
main | (可选)这个方法通常用来实现 operation 对象相关联的任务。尽管你可以在 start 方法中执行任务,使用 main 来实现任务可以让你的代码更加清晰地分离设置和任务代码 |
isExecuting isFinished | (必须)并发操作负责设置自己的执行环境,并向外部 client 报告执行环境的状态。因此并发操作必须维护某些状态信息,以知道是否正在执行任务,是否已经完成任务。使用这两个方法报告自己的状态。这两个方法的实现必须能够在其它多个线程中同时调用。另外这些方法报告的状态变化时,还需要为相应的 key path 产生适当的 KVO 通知。 |
isConcurrent | 必须)标识一个操作是否并发 operation,覆盖这个方法并返回 YES |
Ps : 即使操作被取消,你也应该通知 KVO observers,你的操作已经完成。当某个 operation 对象依赖于另一个 operation 对象的完成时,它会监测后者的 isFinished key path。只有所有依赖的对象都报告已经完成,第一个 operation 对象才会开始运行。如果你的 operation 对象没有产生完成通知,就会阻止其它依赖于你的 operation 对象运行。
NSOperation 类的 key-value observing(KVO)依从于以下 key paths:
如果你覆盖 start 方法,或者对 NSOperation 对象的其它自定义运行 (覆盖 main
除外),你必须确保自定义对象对这些 key paths 保留 KVO 依从。覆盖 start
方法时,需要关注 isExecuting
和 isFinished
两个key paths。
如果你希望实现依赖于其它东西(非 operation 对象),你可以覆盖 isReady
方法,并强制返回 NO,直到你等待的依赖得到满足。如果你需要保留默认的依赖管理系统,确保你调用了 [super isReady]
。当你的 operation 对象的准备就绪状态发生改变时,生成一个 isReady
的key path 的 KVO 通知。
除非你覆盖了 addDependency:
或 removeDependency:
方法,否则 你不需要关注 dependencies key path。
虽然你也可以生成 NSOperation 的其它 KVO 通知,但通常你不需要这样做。如果需要取消一个操作,你可以直接调用现有的 cancel
方法。类似地,你也很少需要修改 queue 优先级信息。最后,除非你的 operation 对象可以动态地改变并发状态,你也不需要提供 isConcurrent
key path 的 KVO 通知。
对 Operation 对象的配置发生在创建对象之后,将其添加到 queue 之前。
依赖关系可以顺序地执行相关的 operation 对象,依赖于其它操作,则必须等到该操作完成之后自己才能开始。你可以创建一对一的依赖关系,也可以创建多个对象之间的依赖图。
使用 NSOperation 的 addDependency:
方法在两个 operation 对象之间建立依赖关系。表示当前 operation 对象将依赖于参数指定的目标 operation 对象。依赖关系不局限于相同 queue 中的 operations 对象, Operation 对象会管理自己的依赖,因此完全可以在不同的 queue 之间 的 Operation 对象创建依赖关系。
当一个 operation 对象依赖的所有其它对象都已经执行完成,该 operation 就变成准备执行状态(如果你自定义了 isReady
方法,则由你 的方法确定是否准备好运行)。如果 operation 已经在一个 queue 中,queue 就可以在任何时候执行这个 operation。如果你需要手动执行该 operation,就自己调用 operation 的 start
方法。
配置依赖必须在运行 operation 和添加 operation 到 queue 之前进行, 之后添加的依赖关系可能不起作用。
依赖要求每个 operation 对象在状态发生变化时必须发出适当的 KVO 通知。如果你自定义了 operation 对象的行为,就必须在自定义代码中生成适当的 KVO 通知,以确保依赖能够正确地执行。
对于添加到 queue 的 Operations,执行顺序首先由已入队列的 operations 是否准备好,然后再根据所有 operations 的相对优先级确定。
是否准备好由对象的依赖关系确定,优先级等级则是 operation 对象本身的一个属性。默认所有 operation 都拥有“普通”优先级,不过你可以通过 setQueuePriority:
方法来提升或降低 operation 对象的优先级。
优先级只能应用于相同 queue 中的 operations。如果应用有多个 operation queue,每个 queue 的优先级等级是互相独立的。因此不同 queue 中的低优先级操作仍然可能比高优先级操作更早执行。
优先级不能替代依赖关系,优先级只是 queue 对已经准备好的 operations 确定执行顺序。先满足依赖关系,然后再根据优先级从所有准备好的操作中选择优先级最高的那个执行。
Mac OS X 10.6 之后,我们可以配置 operation 底层线程的执行优先级,线程直接由内核管理,通常优先级高的线程会给予更多的执行机会。对于 operation 对象,你指定线程优先级为 0.0 到 1.0 之间的某个数值,0.0 表示最低优先级,1.0 表示最高优先级。默认线程优先级为 0.5。
要设置 operation 的线程优先级,你必须在将 operation 添加到 queue 之前,调用 setThreadPriority:
方法进行设置。当 queue 执行该 operation 时,默认的 start
方法会使用你指定的值来修改当前线程的优先级。不过新的线程优先级只在 operation 的 main
方法范围内有效。其它所有代码仍然(包括 completion block)运行在默认线程优先级。这也是为什么推荐把实现放在 main
函数中而不是 start
中。
如果你创建了并发 operation,并覆盖了 start
方法,你必须自己配置线程优先级。
在 Mac OS X 10.6 之后,operation 可以在主任务完成之后执行一个 completion block。你可以使用这个 completion block 来执行任何不属于主任务的工作。例如你可以使用这个 block 来通知相关的 client,操作已经执行完成。而并发 operation 对象则可以使用这个 block 来产生最终的 KVO 通知。
调用 NSOperation 的 setCompletionBlock:
方法来设置一个 completion block,你传递的 block 应该没有参数和返回值。
总结实现自定义 Operation 对象的技巧以及要注意的事项:
虽然多数 operation 都在线程中执行,但对于非并发 operation,通常由 operation queue 提供线程,这时候 queue 拥有该线程,而你的应用不应该去动这个线程。特别是不要关联任何数据到不是你创建和拥有的线程。这些线程由 queue 管理,根据系统和应用的需求创建或销毁。因此使用 Per-Thread storage 在 operations 之间传递数据是不可靠的,而且很有可能会失败。
对于 operation 对象,你完全没有理由使用 Per-Thread Storage,应该在创建对象的时候就给它需要的所有数据。所有输入和输出数据都应该存储在 operation 对象中,最后再整合到你的应用,或者最终释放掉。
operation 本质上是应用中独立的实体,因此需要自己负责处理所有的错误和异常。NSOperation 默认的 start
方法并没有捕获异常。所以你自己的代码总是应该捕获并抑制异常。你还应该检查错误代码并适当地通知应用。如果你覆盖了 start
方法,你也必须捕获所有异常,阻止它离开底层线程的范围。
你需要准备好处理以下错误或异常:
start
方法 start
方法 和任何对象一样,NSOperation 对象也会消耗内存,执行时也会带来开销。因此如果 operation 对象只做很少的工作,但是却创建成千上万个小的 operation 对象,你就会发现更多的时间花在了调度 operations 而不是执行它们。
要高效地使用 Operations,关键是在 Operation 执行的工作量和保持计算机繁忙之间,找到最佳的平衡。确保每个 Operation 都有一定的工作量可以执行。例如 100 个 operations 执行 100 次相同任务,可以考虑 换成 10 个 operations,每个执行 10 次。
你同样要避免向一个 queue 中添加过多的 operations,或者持续快 速地向 queue 中添加 operation,超过 queue 所能处理的能力。这里可 以考虑分批创建 operations 对象,在一批对象执行完之后,使用 completion block 告诉应用创建下一批 operations 对象。
应用需要执行 Operations 来处理相关的工作,你有几种方法来执行 Operations 对象。
手动执行 Operation,要求 Operation 已经准备好, isReady
返回 YES,此时你才能调用 start
方法来执行它。 isReady
方法与 Operations 依赖是结合在一起的。
调用 start
而不是 main
来手动执行 Operation,因为 start
在执行你的自定义代码之前,会首先执行一些安全检查。而且 start
还会产生 KVO 通知,以正确地支持 Operations 的依赖机制。 start
还能处理 Operations 已经被取消的情况,此时会抛出一个异常。
手动执行 Operation 对象之前,还需要调用 isConcurrent
方法,如果返回 NO,你的代码可以决定在当前线程同步执行这个 Operation,或者创建一个独立的线程以异步执行。
Ps : 手动执行 Operation,如果这个方法返回 NO,表示不能执行,你需要设置一个定时器,稍后再次调用本方法,直到这个方法返回 YES,表示已经执行 Operation。
你可以调用 Operation 对象的 cancel
方法取消 Operations。
只有你确定不再需要 Operations 对象时,才应该取消它。发出取消命令会将 Operations 对象设置为”Canceled”状态,会阻止它被执行。由于取消也被认为是完成,依赖于它的其它 Operations 对象会收到适当的 KVO 通知,并清除依赖状态,然后得到执行。
为了最佳的性能,你应该尽量设计你的应用尽可能地异步操作,让应用在操作正在执行时可以去处理其它事情。
如果创建 operation 的代码需要处理 operation 完成后的结果,可以使用 NSOperation 的 waitUntilFinished
方法等待 operation 完成。通常我们应该避免编写这样的代码,阻塞当前线程可能是一种简便的解决方案,但是它引入了更多的串行代码,限制了整个应用的并发性,同时也降低了用户体验。
Operation Queues 对应 GCD 中的 Dispatch Queue。
执行 Operations 最简单的方法是添加到 operation queue,后者是 NSOperationQueue 对象。应用负责创建和维护自己使用的所有 NSOperationQueue 对象。
调用 addOperation:
方法添加一个 operation 到 queue,Mac OS X 10.6 之后可以使用 addOperations:waitUntilFinished:
方法一次添加一组 operations,或者也可以直接使用 addOperationWithBlock:
方法添加 block 对象到 queue。
// 创建一个 NSBlockOperation
NSBlockOperation *theOp = [NSBlockOperation blockOperationWithBlock: ^{
NSLog(@"Beginning operation./n");
}];
// 创建一个 NSOperationQueue
NSOperationQueue *aQueue = [[NSOperationQueue alloc] init];
// 把 theOp 加入到 aQueue
[aQueue addOperation:theOp];
// 这个方法可以批量添加 OP 后面的参数为是否阻塞当前线程
[aQueue addOperations:@[theOp] waitUntilFinished:NO];
// NSOperationQueue 也可以调用下面的方法直接添加一个 block 当做任务
[aQueue addOperationWithBlock:^{
/* Do something. */
}];
// 创建一个 NSBlockOperation
let theOp = NSBlockOperation.init(block: { () -> Void in
NSLog("Beginning operation./n")
})
// 创建一个 NSOperationQueue
let aQueue = NSOperationQueue()
// 把 theOp 加入到 aQueue
aQueue.addOperation(theOp)
// 这个方法可以批量添加 OP 后面的参数为是否阻塞当前线程
aQueue.addOperations([theOp], waitUntilFinished:false)
// NSOperationQueue 也可以调用下面的方法直接添加一个 block 当做任务
aQueue.addOperationWithBlock { () -> Void in
/* Do something. */
}
Operations 添加到 queue 后,通常短时间内就会得到运行。但是如果存在依赖,或者 Operations 挂起等原因,也可能需要等待。
注意 Operations 添加到 queue 之后,绝对不要再修改 Operations 对象。因为 Operations 对象可能会在任何时候运行,因此改变依赖或数据会产生不利的影响。你只能通过 NSOperation 的方法来查看操作的状态,是否正在运行、等待运行、已经完成等。
虽然 NSOperationQueue 类设计用于并发执行 Operations,你也可以强制单个 queue 一次只能执行一个 Operation。 setMaxConcurrentOperationCount:
方法可以配置 operation queue 的最大并发操作数量。设为 1 就表示 queue 每次只能执行一个操作。不过 operation 执行的顺序仍然依赖于其它因素,像操作是否准备好和优先级等。因此串行化的 operation queue 并不等同于 GCD 中的串行 dispatch queue。
可以调用 operation queue 的 cancelAllOperations
方法取消当前 queue 中的所有操作。
常见的做法是当发生重大事件时,一次性取消 queue 中的所有操作,例如应用退出或用户请求取消操作。
为了最佳的性能,你应该尽量设计你的应用尽可能地异步操作,让应用在操作正在执行时可以去处理其它事情。
可以同时等待一个 queue 中的所有操作,使用 NSOperationQueue 的 waitUntilAllOperationsAreFinished
方法。注意在等待一个 queue 时,应用的其它线程仍然可以往 queue 中添加 Operation,因此可能加长你线程的等待时间。
如果你想临时挂起 Operations 的执行,可以使用 setSuspended:
方法暂停相应的 queue。不过挂起一个 queue 不会导致正在执行的 Operation 在任务中途暂停,只是简单地阻止调度新 Operation 执行。你可以在响应用户请求时,挂起一个 queue,来暂停等待中的任务。稍后根据用户的请求,可以再次调用 setSuspended:
方法继续 Queue 中操作 的执行。
本文是笔者借着新入职公司要求巩固 iOS 并发编程知识的机会把之前收藏的一些文章和博客翻了一遍又把公司下发的2011年版本《iOS 并发编程指南》(里面还有写 MRC 下的并发编程内存管理)啃了一遍之后对自己这方面的知识梳理而成。
文章结合了新老资料的描述和 Apple 官方文档 ,并且每一份代码实例笔者都尽量给出了 OC 和 Swift 双版本。由于篇幅很长写得时间跨度有点大(每天加班加上周末一共 4 天),文中部分地方可能显得思路不太连贯,排版有失协调。如果发现错误或者有更好的建议请留言下方,好让笔者第一时间做出更正和修改,我们的进步需要你的支持!