NSOpenPanel.runModal + NSAlert.runModal 超过 GCD 导致挂起

NSOpenPanel.runModal + NSAlert.runModal over GCD cause a hang

在我的 Cocoa 应用程序中,我在后台完成了一些计算。后台工作 运行 与 DispatchQueue.global(qos: .utility).async 相结合。 此后台任务可能会通过 DispatchQueue.main.async.

显示模态 NSAlert 来报告错误

此外,在我的应用程序中,用户可以 运行 NSOpenPanel 打开一些文件(使用 NSOpenPanel.runModal)。

问题是,如果用户打开NSOpenPanel,同时后台任务显示NSAlert,应用程序可能会挂起。

最小代码示例(测试 IBaction 被绑定为主要按钮的操作 window)

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!
   
    @IBAction func test(_ sender: Any) {
        //run some work in background
        DispatchQueue.global(qos: .utility).async
        {
            sleep(1) //some work

            //report errors in the main thread.
            DispatchQueue.main.async {
                let alert = NSAlert();
                alert.informativeText = "Close NSOpen panel before this alert to reproduct the hang."
                alert.runModal()
            }
        }
        
        //user want to open a file and opens the open file dialog
        let dlg = NSOpenPanel();
        dlg.runModal();
    }
}

那么,这段代码有什么问题,为什么它会在特定用例中导致挂起?我怎样才能防止这种挂起?

附加说明:我发现,如果我将 dlg.runModal() 替换为 NSApp.RunModal(for: dlg)(这与 Apple 文档完全相同),这将修复上述用例中的挂起问题。 但它仍然会在关闭 NSOpenPanel 后立即自动关闭 NSAlert。而且我仍然不明白为什么会这样。

更新

我更新了上面的代码以包含最小可重现应用程序的 AppDelegate class 的完整代码。要重现该案例,只需在 XCode 中创建新的 SwiftApp,替换 AppDelegate 代码,在主 window 中添加按钮并使用 test 函数连接按钮的操作。我还在 github 上放置了完整的准备编译项目:https://github.com/snechaev/hangTest

配置 NSOpenPanel 和 NSAlert 的代码及其结果处理被排除在外,因为这样的代码不会影响挂起。

我认为您正在使主队列死锁,因为 runModal() 同时在代码的两个位置阻塞了主队列。如果你的代码是可重入的,那就是你得到的。

可能的解决方案:

  1. 避免使用应用模态 windows 并改用 window 模态 windows a.k.a. sheets。要了解如何将 NSOpenPanel 用作 sheet,请将其附加到它所属的 window。举个例子,请看这个答案:

  1. 如果显示警报,您可以设置一个标志来阻止用户打开 NSOpenPanel,但这很丑陋并且不能解决任何可能导致其他死锁的未来问题,因为很可能是您的代码是可重入的。

除了@jvarela 的回答,我还想补充一些细节,并制作一些关于我的问题的简历。

  1. 看起来没有办法解决将 NSPanel/NSAlert 作为模态 windows 并阻塞调用者线程(使用 运行Modal)的问题。
  2. non-modal(NSPanel.begin())或模态non-blocking(NSPanel.beginSheet,NSPanel.beginSheet模态) 不会导致挂起,但如果用户尝试在 NSAlert 之前关闭 NSPanel,仍​​然会导致意外自动关闭 NSAlert。此外,要使用这种 non-blocking 方法,您将被迫重构整个代码库以使用 callbacks/completion 处理程序而不是在使用 NSPanel 时阻塞操作。
  3. 我没有找到 NSPanel 未被阻止的原因,并且当我在其上显示模态 NSAlert 时继续接收用户输入。我怀疑这是因为安全机制 运行 NSPanel 在一个单独的进程中,但我没有这方面的证据。我仍然对此感兴趣。
  4. 对于我当前的项目,我决定放弃使用 NSPanel 的阻塞方式,因为我的代码库很大,很难立即更改所有代码以使用完成处理程序。对于 NSPanel + NSAlert 的特殊情况,我只是不允许用户在这个特定的后台工作正在进行时打开 NSPanel。用户现在应该等待后台工作完成或手动取消工作才能 运行 打开文件功能。