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

鳞片状的

在 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)
    }
}

这种方法的好处似乎是:

  • 保留执行顺序。如果调用者不在主线程上,我们将重定向到主线程,然后在返回之前执行相同的函数——从而保留从主线程调用原始函数时会发生的正常执行顺序,因为在主线程(或任何其他线程)上调用的函数默认是同步执行的。
  • 能够在没有编译器警告的情况下隐式引用 self。在 Xcode 11.4 中,同步执行此调用也满足编译器的要求,即隐式保留 self 是可以的,因为在原始函数调用返回之前将进入然后退出调度上下文——因此我们不会从这种方法中得到任何新的编译器警告. 那很好很干净。
  • 通过更少的缩进更集中的差异。它避免将整个函数体包装在一个闭包中(就像你通常看到的那样,如果Dispatch.main.async { ... }使用了,整个函数体现在必须缩进一个更深的层次,在你的 PR 中产生空白差异,这可能导致恼人的合并冲突并使其成为审阅者更难区分 GitHub 的 PR 差异视图中的显着元素)。

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

  • 可能会改变预期的执行顺序。该函数将在执行分派的闭包之前返回,这反过来意味着它self可能在它运行之前已经释放。这意味着我们必须显式地保留 self (或弱化它)以避免编译器警告。这也意味着,在这个例子中,present(...)在函数返回给调用者之前不会被调用。这可能会导致模式在调用站点之后的一些其他代码之后弹出,从而导致意外行为。
  • 弱化或明确保留的要求self这并不是一个真正的缺点,但它在风格上并不像能够隐含地保留自我那样干净。

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

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

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

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

这种模式绝对不是“完全”安全的。人们可以很容易地制造出僵局:

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

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

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

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


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

主线程断点

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

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章