【问题标题】:Serializing asynchronous tasks in objective C在目标 C 中序列化异步任务
【发布时间】:2023-03-25 00:20:02
【问题描述】:

我希望能够“真正地”序列化异步方法,例如:

  • 发出网络请求
  • 显示 UIAlertView

这通常是一项棘手的工作,大多数串行队列示例在 NSBlockOperation 的块中显示“休眠”。这不起作用,因为只有在回调发生时操作才完成。

我已经尝试通过继承 NSOperation 来实现它,这里是实现中最有趣的部分:

+ (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];

注意,我也使用了睡眠,但确保它们在后台线程中执行以模拟网络调用。日志内容如下

1...
1
2...
2

这是所希望的。这种方法有什么问题?有什么我应该注意的警告吗?

【问题讨论】:

  • 你想调用 web 方法并且 web 调用的 onfinish 要显示警报吗??
  • 您的子类与 NSBlockOperation 等内置类相比有什么优势? - 如果一切都“运行良好”并且“符合预期”,您的问题是什么?
  • @armaan,不,它比这更通用。
  • @Martin,NSBlockOperation 不能这样做,因为该块非常快地完成执行 - 一旦调度异步调用 - 它几乎立即为操作设置 isFinished。上面的重点是等到回调,以便异步操作真正序列化。
  • 您可能想重新表述您的问题。序列化通常是指storing something on disk

标签: ios objective-c multithreading nsoperation


【解决方案1】:

“序列化”异步任务实际上将被命名为“继续”(另请参阅这篇 wiki 文章 Continuation

假设,您的任务可以定义为带有完成处理程序的异步函数/方法,其参数是异步任务的最终结果,例如:

typedef void(^completion_handler_t)(id result);

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

有了blocks 可用,“继续”可以通过从上一个任务的完成块中调用下一个异步任务轻松完成

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

请注意,方法 foo 会“被“异步”感染;)

也就是说,这里foo方法的最终效果,即将用户的回答打印到控制台,实际上又是异步的。

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

使用完成块实现“继续”将增加每个任务的完成处理程序的缩进。此外,实现一种让用户在任何状态下取消任务的方法,以及实现处理错误情况的代码,代码很快就会变得混乱、难以编写和难以理解。

实现“延续”以及取消和错误处理的更好方法是使用Futures or Promises 的概念。 Future 或 Promise 表示异步任务的最终结果。基本上,这只是向呼叫站点“发出最终结果信号”的另一种方法。

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

当使用这样的Promise时,你会定义你的任务如下:

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

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

通过Promise,异步任务的“结果”将通过“成功”或“错误”处理程序获得,该处理程序将使用then 属性“注册”承诺。任务完成时调用成功或错误处理程序:当它成功完成时,将调用成功处理程序并将其结果传递给成功处理程序的参数result。否则,当任务失败时,它会将原因传递给错误处理程序 - 通常是 NSError 对象。

Promise 的基本用法如下:

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

成功处理程序块有一个id 类型的参数result。错误处理程序块有一个NSError 类型的参数。

请注意,promise.then(...) 语句给自身返回一个代表任一处理程序结果的承诺,当“父”承诺以成功或错误解决时被调用。处理程序的返回值可以是“立即结果”(某个对象)或“最终结果”——表示为 Promise 对象。

OP 问题的注释示例显示在以下代码 sn-p 中(包括复杂的错误处理):

- (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 库 - 一个实现“承诺”的 Objective-C 类。披露:我是 RXPromise 库的作者。

【讨论】:

    【解决方案2】:

    乍一看这是可行的,但有些部分缺少“适当的” NSOperation 子类。

    您不应对“取消”状态,您应该在启动时检查isCancelled,如果返回YES("responding to the cancel command")则不要启动

    isConcurrent 方法也需要被覆盖,但也许你为了简洁而忽略了它。

    【讨论】:

    • 谢谢 Pieter - 我可以添加已取消的支票。我的印象是我不必实现 isConcurrent - 它只是默认返回 false。
    【解决方案3】:

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

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

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

    如果你对使用子类化 NSOperation 不以为然,那么只覆盖 main,否则我建议使用 NSBlockOperation,这看起来像是你在这里复制的东西。真的,虽然我会完全避免使用 NSOperation,但 API 已经开始显示其年龄并且很容易出错。作为替代方案,我会建议类似RXPromise 或我自己解决此问题的尝试FranticApparatus

    【讨论】:

      猜你喜欢
      • 2014-11-15
      • 2022-10-18
      • 2014-07-24
      • 2017-09-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-20
      相关资源
      最近更新 更多