在Swift中,如果Thread.current.isMainThread == false,那么递归一次DispatchQueue.main.sync安全吗?

In Swift, if Thread.current.isMainThread == false, then is it safe to DispatchQueue.main.sync recursively once?

在Swift中,如果Thread.current.isMainThread == false,那么递归一次DispatchQueue.main.sync是否安全?

我问的原因是,在我公司的应用程序中,我们发生了一次崩溃,结果证明是由于从主线程调用了某些 UI 方法,例如:

public extension UIViewController {
    func presentModally(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        // some code that sets presentation style then:
        present(viewControllerToPresent, animated: flag, completion: completion)
    }
}

由于这是从许多地方调用的,其中一些有时会从后台线程调用它,所以我们时不时地遇到崩溃。

修复所有调用站点是不可行的,因为该应用程序有超过一百万行代码,所以我对此的解决方案只是检查我们是否在主线程上,如果不在,则重定向调用主线程,像这样:

public extension UIViewController {
    func presentModally(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        guard Thread.current.isMainThread else {
            DispatchQueue.main.sync { 
                presentModally(viewControllerToPresent, animated: flag, completion: completion) 
            }
            return
        }

        // some code that sets presentation style then:
        present(viewControllerToPresent, animated: flag, completion: completion)
    }
}

这种方法的好处似乎是:

同时,替代方案 DispatchQueue.main.async 似乎具有以下缺点:

所以问题是:这些假设都是正确的,还是我遗漏了什么?

审查 PR 的我的同事似乎觉得使用 "DispatchQueue.main.sync" 在某种程度上本质上是不好的和有风险的,并且可能导致僵局。虽然我意识到从主线程使用它确实会死锁,但在这里我们明确避免使用 guard 语句来确保我们不首先在主线程上。

尽管提出了上述所有基本原理,并且尽管无法向我解释死锁实际上是如何发生的,因为只有当函数从主线程开始被调用时才会发生分派,但我的同事们仍然对这种模式有很深的保留意见,认为它可能会导致死锁或以意想不到的方式阻塞 UI。

这些恐惧有根据吗?还是这种模式绝对安全?

我同意关于您的代码在结构上存在一些困难的评论。

但有时我需要在主线程上 运行 编写代码,但我不知道我是否已经在主线程上。这种情况经常发生,我为此写了一个 ExecuteOnMain() 函数:

dispatch_queue_t MainSequentialQueue( )
{
    static dispatch_queue_t mainQueue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
#if HAS_MAIN_RUNLOOP
        // If this process has a main thread run loop, queue sequential tasks to run on the main thread
        mainQueue = dispatch_get_main_queue();
#else
        // If the process doesn't execute in a run loop, create a sequential utility thread to perform these tasks
        mainQueue = dispatch_queue_create("main-sequential",DISPATCH_QUEUE_SERIAL);
#endif
        });
    return mainQueue;
}

BOOL IsMainQueue( )
{
#if HAS_MAIN_RUNLOOP
    // Return YES if this code is already executing on the main thread
    return [NSThread isMainThread];
#else
    // Return YES if this code is already executing on the sequential queue, NO otherwise
    return ( MainSequentialQueue() == dispatch_get_current_queue() );
#endif
}

void DispatchOnMain( dispatch_block_t block )
{
    // Shorthand for asynchronously dispatching a block to execute on the main thread
    dispatch_async(MainSequentialQueue(),block);
}

void ExecuteOnMain( dispatch_block_t block )
{
    // Shorthand for synchronously executing a block on the main thread before returning.
    // Unlike dispatch_sync(), this won't deadlock if executed on the main thread.
    if (IsMainQueue())
        // If this is the main thread, execute the block immediately
        block();
    else
        // If this is not the main thread, queue the block to execute on the main queue and wait for it to finish
        dispatch_sync(MainSequentialQueue(),block);
}

这种模式绝对不是“绝对”安全的。可以很容易地设计出一个死锁:

let group = DispatchGroup()
DispatchQueue.global().async(group: group) {
    self.presentModally(controller, animated: true)
}
group.wait()

检查 isMainThreadfalse 是不够的,严格来说,要知道同步分派到主线程是否安全。

但这不是真正的问题。你显然在某个地方有一些例程认为它是 运行 在主线程上,但实际上不是。就个人而言,我会担心代码在这种误解下运行时还做了什么(例如,不同步的模型更新等)。

您的解决方法只是隐藏问题,而不是解决问题的根本原因。作为一般规则,我不建议围绕代码库中其他地方引入的错误进行编码。你真的应该弄清楚你从后台线程调用这个例程的位置并解决它。


关于如何找到问题,希望与崩溃相关的堆栈跟踪会告诉您。我还建议通过在方案设置中单击它旁边的小箭头来为主线程检查器添加一个断点:

然后运行应用程序,如果它遇到这个问题,它会在有问题的行暂停执行,这对于跟踪这些问题非常有用。这通常比从堆栈跟踪中进行逆向工程要容易得多。