问题 如何包装一个异步方法,该方法接受一个块并在目标c中将其转换为同步


我想包装一个如下所示的异步API:

[someObject completeTaskWithCompletionHandler:^(NSString *result) {

}];

进入一个我可以像这样调用的同步方法

NSString *result = [someObject completeTaskSynchronously];

我该怎么做呢?我做了一些文档阅读和谷歌搜索,并尝试使用“dispatch_semaphore”尝试实现它,如下所示:

-(NSString *) completeTaskSynchronously {
   __block NSString *returnResult;
   self.semaphore = dispatch_semaphore_create(0);  
   [self completeTaskWithCompletionHandler:^(NSString *result) {
       resultResult = result;
       dispatch_semaphore_signal(self.semaphore);
   }];

   dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
   return resultResult;
}

但这似乎没有用,它基本上只是停在dispatch_semaphore_wait。执行永远不会到达执行_signal的内部块。任何人都有关于如何做到这一点的代码示例?我怀疑该块必须在主线程之外的其他线程上?另外,假设我无法访问异步方法背后的源代码。谢谢!


6133
2017-12-16 06:37


起源

如果在调用dispatch_semaphore_wait的同一线程上执行完成处理程序,则确实使线程死锁,因为在线程退出等待之前无法执行完成块。你想在主线程上做这个吗?最好不要长时间阻塞主线程,因为它必须不断地发送消息。 - yurish
如果像@yurish所怀疑的那样,你的处理程序已经排队到主派遣线程,你就不能等待。您必须将您的代码流构建为状态机,并在完成处理程序中执行任何需要完成的操作。 - Martin James
没有一般的方法可以做到这一点。正如其他人所说,如果异步任务的某些部分通过将事件放在运行循环上来工作,那么您将始终处于死锁状态。你是什​​么 真 试图实现?也许还有另一种构建代码的方法。 - JeremyP
@JeremyP [NSRunLoop -runMode:beforeDate:] 模式是“执行此操作的一般方法”并避免死锁。 - Aaron Brager
@AaronBrager否。如果包装的异步方法的实现使用运行循环来分派完成块,则轮询运行循环仅防止死锁。 - Nikolai Ruhe


答案:


dispatch_semaphore_wait 阻止示例中的主队列。您可以将异步任务分派到不同的队列:

__block NSString *returnResult;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0UL);
dispatch_async(queue,^{
     result = [someObject completeTaskSynchronously];
});

或者使用其他系统,如NSRunLoop:

   __block finished = NO;
   [self completeTaskWithCompletionHandler:^(NSString *result) {
       resultResult = result;
       finished = YES;
   }];
    while (!finished) {
        // wait 1 second for the task to finish (you are wasting time waiting here)
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];
    }

8
2017-12-16 10:12





使用NSRunLoop是最容易做到的。

__block NSString* result = nil;
[self completeTaskWithCompletionHandler:^(NSString *resultstring) {
    result = resultstring;
}];
while (!result) {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

6
2017-12-16 12:37



是的,我认为这很有效。我还在github上找到了一个类MTTestSemaphore,它封装了这个并使其更像信号量。谢谢。 - kawingkelvin
我想知道这与正常的事件处理有多好?核心动画交易等等 - yurish
这不能很好地工作。运行循环需要处理事件才能返回。如果没有事件,它只会在“遥远的未来”回归,即 决不。此外,访问指针 result 不是线程安全的。不能保证指针不为零并完全初始化。并且(可能)可以允许编译器优化对存储器位置的访问并将指针保持在永远不会更新的寄存器中。 - CouchDeveloper
@CouchDeveloper你是对的,这不是线程安全的。 NSRunLoops永远不会。你可以检查一下 [result count] > 0 确保初始化对象。我曾经使用过这个解决方案几次没有问题。你能提供一个好的选择吗? NSConditionLock,NSLock和dispatch_semaphore很难使用。您还可以使用NSOperation来包装异步调用,然后使用 -[NSOperation waitUntilFinished] 阻止执行当前线程。然而,这也可能导致死锁。 - orkoden


我认为更好的解决方案将是NSRunLoop,如下所示。它简单而且工作正常。

- (NSString *)getValue {

    __block BOOL _completed = NO;
    __block NSString *mValue = nil;


    [self doSomethingWithCompletionHandler:^(id __nullable value, NSError * __nullable error) {
        mValue = value;
        _completed = YES;
    }];

    while (!_completed) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }

    return mValue;
}

1
2018-01-06 17:49





您可以尝试使用NSOperations来异步执行此操作。


-2
2017-12-16 08:08