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
,应用程序可能会挂起。
- 用户打开模式
NSOpenPanel
- 后台任务在
NSOpenPanel
之上打开模态 NSAlert
- 用户在
NSOpenPanel
中单击 关闭(它确实可以访问 NSOpenPanel
,尽管存在更多模式 NSAlert
)。
NSAlert
和 NSOpenPanel
都被关闭,应用程序挂起,主线程在 NSOpenPanel.runModal()
中阻塞
- 如果用户先关闭
NSAlert
然后 NSOpenPanel
,应用程序将不会挂起。
最小代码示例(测试 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()
同时在代码的两个位置阻塞了主队列。如果你的代码是可重入的,那就是你得到的。
可能的解决方案:
- 请避免使用应用模态 windows 并改用 window 模态 windows a.k.a. sheets。要了解如何将
NSOpenPanel
用作 sheet,请将其附加到它所属的 window。举个例子,请看这个答案:
- 如果显示警报,您可以设置一个标志来阻止用户打开
NSOpenPanel
,但这很丑陋并且不能解决任何可能导致其他死锁的未来问题,因为很可能是您的代码是可重入的。
除了@jvarela 的回答,我还想补充一些细节,并制作一些关于我的问题的简历。
- 看起来没有办法解决将 NSPanel/NSAlert 作为模态 windows 并阻塞调用者线程(使用 运行Modal)的问题。
- non-modal(NSPanel.begin())或模态non-blocking(NSPanel.beginSheet,NSPanel.beginSheet模态) 不会导致挂起,但如果用户尝试在 NSAlert 之前关闭 NSPanel,仍然会导致意外自动关闭 NSAlert。此外,要使用这种 non-blocking 方法,您将被迫重构整个代码库以使用 callbacks/completion 处理程序而不是在使用 NSPanel 时阻塞操作。
- 我没有找到 NSPanel 未被阻止的原因,并且当我在其上显示模态 NSAlert 时继续接收用户输入。我怀疑这是因为安全机制 运行 NSPanel 在一个单独的进程中,但我没有这方面的证据。我仍然对此感兴趣。
- 对于我当前的项目,我决定放弃使用 NSPanel 的阻塞方式,因为我的代码库很大,很难立即更改所有代码以使用完成处理程序。对于 NSPanel + NSAlert 的特殊情况,我只是不允许用户在这个特定的后台工作正在进行时打开 NSPanel。用户现在应该等待后台工作完成或手动取消工作才能 运行 打开文件功能。
在我的 Cocoa 应用程序中,我在后台完成了一些计算。后台工作 运行 与 DispatchQueue.global(qos: .utility).async
相结合。
此后台任务可能会通过 DispatchQueue.main.async
.
NSAlert
来报告错误
此外,在我的应用程序中,用户可以 运行 NSOpenPanel
打开一些文件(使用 NSOpenPanel.runModal
)。
问题是,如果用户打开NSOpenPanel
,同时后台任务显示NSAlert
,应用程序可能会挂起。
- 用户打开模式
NSOpenPanel
- 后台任务在
NSOpenPanel
之上打开模态 - 用户在
NSOpenPanel
中单击 关闭(它确实可以访问NSOpenPanel
,尽管存在更多模式NSAlert
)。 NSAlert
和NSOpenPanel
都被关闭,应用程序挂起,主线程在NSOpenPanel.runModal()
中阻塞
- 如果用户先关闭
NSAlert
然后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()
同时在代码的两个位置阻塞了主队列。如果你的代码是可重入的,那就是你得到的。
可能的解决方案:
- 请避免使用应用模态 windows 并改用 window 模态 windows a.k.a. sheets。要了解如何将
NSOpenPanel
用作 sheet,请将其附加到它所属的 window。举个例子,请看这个答案:
- 如果显示警报,您可以设置一个标志来阻止用户打开
NSOpenPanel
,但这很丑陋并且不能解决任何可能导致其他死锁的未来问题,因为很可能是您的代码是可重入的。
除了@jvarela 的回答,我还想补充一些细节,并制作一些关于我的问题的简历。
- 看起来没有办法解决将 NSPanel/NSAlert 作为模态 windows 并阻塞调用者线程(使用 运行Modal)的问题。
- non-modal(NSPanel.begin())或模态non-blocking(NSPanel.beginSheet,NSPanel.beginSheet模态) 不会导致挂起,但如果用户尝试在 NSAlert 之前关闭 NSPanel,仍然会导致意外自动关闭 NSAlert。此外,要使用这种 non-blocking 方法,您将被迫重构整个代码库以使用 callbacks/completion 处理程序而不是在使用 NSPanel 时阻塞操作。
- 我没有找到 NSPanel 未被阻止的原因,并且当我在其上显示模态 NSAlert 时继续接收用户输入。我怀疑这是因为安全机制 运行 NSPanel 在一个单独的进程中,但我没有这方面的证据。我仍然对此感兴趣。
- 对于我当前的项目,我决定放弃使用 NSPanel 的阻塞方式,因为我的代码库很大,很难立即更改所有代码以使用完成处理程序。对于 NSPanel + NSAlert 的特殊情况,我只是不允许用户在这个特定的后台工作正在进行时打开 NSPanel。用户现在应该等待后台工作完成或手动取消工作才能 运行 打开文件功能。