在目标C中序列化asynchronous任务

我希望能够序列化“真正的”asynchronous方法,例如:

  • 提出networking请求
  • 显示一个UIAlertView

这通常是一个棘手的业务,大多数串行队列样本在NSBlockOperation的块中显示“睡眠”。 这不起作用,因为操作只在callback发生时完成。

我已经通过inheritanceNSOperation来实现这个,下面是实现中最有趣的部分:

+ (MYOperation *)operationWithBlock:(CompleteBlock)block { MYOperation *operation = [[MYOperation alloc] init]; operation.block = block; return operation; } - (void)start { [self willChangeValueForKey:@"isExecuting"]; self.executing = YES; [self didChangeValueForKey:@"isExecuting"]; if (self.block) { self.block(self); } } - (void)finish { [self willChangeValueForKey:@"isExecuting"]; [self willChangeValueForKey:@"isFinished"]; self.executing = NO; self.finished = YES; [self didChangeValueForKey:@"isExecuting"]; [self didChangeValueForKey:@"isFinished"]; } - (BOOL)isFinished { return self.finished; } - (BOOL) isExecuting { return self.executing; } 

这工作得很好,这里是一个演示…

 NSOperationQueue *q = [[NSOperationQueue alloc] init]; q.maxConcurrentOperationCount = 1; dispatch_queue_t queue = dispatch_queue_create("1", NULL); dispatch_queue_t queue2 = dispatch_queue_create("2", NULL); MYOperation *op = [MYOperation operationWithBlock:^(MYOperation *o) { NSLog(@"1..."); dispatch_async(queue, ^{ [NSThread sleepForTimeInterval:2]; NSLog(@"1"); [o finish]; // this signals we're done }); }]; MYOperation *op2 = [MYOperation operationWithBlock:^(MYOperation *o) { NSLog(@"2..."); dispatch_async(queue2, ^{ [NSThread sleepForTimeInterval:2]; NSLog(@"2"); [o finish]; // this signals we're done }); }]; [q addOperations:@[op, op2] waitUntilFinished:YES]; [NSThread sleepForTimeInterval:5]; 

请注意,我也使用了睡眠,但确保这些在后台线程中执行以模拟networking调用。 日志如下所示

 1... 1 2... 2 

这是根据需要。 这种方法有什么问题? 有什么需要注意的吗?

乍看起来,这将工作,但有些部分缺less一个“适当的”NSOperation子类。

你不应该处理'取消'状态,你应该检查isCancelled在开始,而不是开始,如果这返回YES( “响应取消命令” )

isConcurrent方法也需要被覆盖,但是为了简洁起见,可能会省略。

“序列化”asynchronous任务将被命名为“continuation”(另请参阅本wiki文章Continuation 。

假设你的任务可以被定义为一个带有完成处理程序的asynchronous函数/方法,该处理程序的参数是asynchronous任务的最终结果,例如:

 typedef void(^completion_handler_t)(id result); -(void) webRequestWithCompletion:(completion_handler_t)completionHandler; -(void) showAlertViewWithResult:(id)result completion:(completion_handler_t)completionHandler; 

有了可用的块 ,可以通过从前一个任务完成块中调用下一个asynchronous任务来轻松完成“延续”:

 - (void) foo { [self webRequestWithCompletion:^(id result) { [self showAlertViewWithResult:result completion:^(id userAnswer) { NSLog(@"User answered with: %@", userAnswer); } } } 

请注意,方法foo “感染”asynchronous“;)

也就是说,这里foo方法的最终效果,即将用户的答案打印到控制台,实际上是再次asynchronous的。

但是,“链接”多个asynchronous任务,即“继续”多个asynchronous任务,可能会很快变得非常笨拙:

使用完成块实现“continuation”会增加每个任务完成处理程序的缩进。 而且,实现一种让用户在任何状态下取消任务的方法,还可以实现代码来处理错误情况,代码很快就会混乱,难以编写,难以理解。

实施“延续”以及取消和error handling的更好方法是使用期货或承诺的概念。 Future或Promise代表asynchronous任务的最终结果 。 基本上,这只是一个不同的方法来向呼叫站点“发出最终结果”。

在Objective-C中,“Promise”可以作为一个普通的类来实现。 有第三方图书馆实施“承诺”。 以下代码使用特定的实现RXPromise

当使用这样一个承诺时 ,你将如下定义你的任务:

 -(Promise*) webRequestWithCompletion; -(Promise*) showAlertViewWithResult:(id)result; 

注意:没有完成处理程序。

通过Promise ,asynchronous任务的“结果”将通过“成功”或“错误”处理程序来获得,该处理程序将被“注册”该承诺的属性。 任务成功或error handling程序在完成时被任务调用:成功完成后,成功处理程序将被调用将其结果传递给成功处理程序的参数结果 。 否则,当任务失败时,它将原因传递给error handling程序 – 通常是一个NSError对象。

Promise的基本用法如下:

 Promise* promise = [self asyncTasks]; // register handler blocks with "then": Promise* handlerPromise = promise.then( <success handler block>, <error handler block> ); 

成功处理程序块具有idtypes的参数结果 。 error handling程序块有一个NSErrortypes的参数。

请注意, promise.then(...)自身返回一个promise,它代表任何一个处理程序的结果,当“父”承诺已被成功或错误parsing时,该处理程序将被调用。 处理程序的返回值可以是“直接结果”(某个对象)或“最终结果” – 表示为Promise对象。

在下面的代码片段(包括复杂的error handling)中显示了OP的问题的评论样本:

 - (void) foo { [self webRequestWithCompletion] // returns a "Promise" object which has a property "then" // when the task finished, then: .then(^id(id result) { // on succeess: // param "result" is the result of method "webRequestWithCompletion" return [self showAlertViewWithResult:result]; // note: returns a promise }, nil /*error handler not defined, fall through to the next defined error handler */ ) // when either of the previous handler finished, then: .then(^id(id userAnswer) { NSLog(@"User answered with: %@", userAnswer); return nil; // handler's result not used, thus nil. }, nil) // when either of the previous handler finished, then: .then(nil /*success handler not defined*/, ^id(NEError* error) { // on error // Error handler. Last error handler catches all errors. // That is, either a web request error or perhaps the user cancelled (which results in rejecting the promise with a "User Cancelled" error) return nil; // result of this error handler not used anywhere. }); } 

代码当然需要更多的解释。 有关详细和更全面的描述,以及如何在任何时间点完成取消操作,可以查看RXPromise库 – 一个实现“Promise”的Objective-C类。 披露:我是RXPromise图书馆的作者。

当子类化NSOperation时,我强烈build议只有重写main,除非你真的知道你在做什么,因为这很容易搞砸线程安全。 虽然文档说操作不会并发,但通过在NSOperationQueue中运行它们的动作通过在单独的线程上运行它们自动使它们并发。 非并发票据只适用于你自己调用NSOperation的start方法。 您可以通过logging每个NSLog行包含的线程ID来validation这一点。 例如:

2013-09-17 22:49:07.779 AppNameGoesHere [58156:ThreadIDGoesHere]你的日志消息在这里。

重写主要的好处是,在改变操作的状态时,你不必处理线程的安全性。NSOperation为你处理所有这些。 序列化代码的主要事件是将maxConcurrentOperationCount设置为1的行。这意味着队列中的每个操作都将等待下一个运行(所有这些操作都将在由NSOperationQueue确定的随机线程上运行)。 在每个操作中调用dispatch_async的行为也会触发另一个线程。

如果你死了使用NSOperation的子类,那么只能覆盖主,否则我会build议使用NSBlockOperation,这似乎是你有点复制在这里。 真的,尽pipe我完全避免使用NSOperation,API已经开始显示年龄,很容易出错。 作为替代,我会build议像RXPromise或我自己的尝试解决这个问题, FranticApparatus 。