首页 > 解决方案 > 为什么 DispatchSemaphore.wait() 会阻止这个完成处理程序?

问题描述

所以我一直在玩 NetworkExtension 来实现玩具 VPN,但我遇到了完成处理程序/异步运行代码的问题。我将引导您完成我的思路/实验,并感谢您在我犯错的领域以及如何解决此问题的任何指示!

这是最小的可重现代码(显然你需要import NetworkExtension):

let semaphore = DispatchSemaphore(value: 0)
NETunnelProviderManager.loadAllFromPreferences { managers, error in
    print("2 during")
    semaphore.signal()
}
print("1 before")
semaphore.wait()
print("3 after")

根据我对信号量和异步代码的理解,我希望打印输出按顺序发生:

1 before
2 during
3 after

但是程序挂在"1 before"。如果我删除该semaphore.wait()行,打印输出按预期的顺序发生:1、3、2 因为闭包稍后运行)。

因此,在使用调试器进行一些挖掘之后,看起来信号量陷阱循环正在阻塞执行。这促使我阅读了一些队列,我发现将其更改为以下工作:

// ... as before
DispatchQueue.global().async {
    semaphore.wait()
    print("3 after")
}

这是有道理的,因为阻塞.wait()调用现在在单独的线程中被异步调用。但是,这个解决方案对我来说并不是我想要的,因为在我的实际实现中,我实际上是从闭包中捕获结果并稍后返回它们,看起来像这样:

let semaphore = DispatchSemaphore(value: 0)
var results: [NETunnelProviderManager]? = nil
NETunnelProviderManager.loadAllFromPreferences { managers, error in
    print("2 during")
    results = managers
    semaphore.signal()
}
print("1 before")
// DispatchQueue.global().async {
    semaphore.wait()
    print("3 after")
// }
return results

显然我不能从async闭包中返回数据,并且将返回移出它会使它失效。此外,添加另一个信号量以使事情同步表现出与之前在链中移动问题相同的问题。

因此,我决定尝试将.loadAllFromPreferences()调用和完成处理程序放在一个async闭包中,并将其他所有内容保留在原始代码片段中:

// ...
DispatchQueue.global().async {
    NETunnelProviderManager.loadAllFromPreferences { loadedManagers, error in
        print("2 during")
        semaphore.signal()
    }
}
// ...

但是,这不起作用,并且.wait()调用从未通过 - 和以前一样。我假设信号量仍然以某种方式阻塞线程并且不允许执行任何操作,这意味着系统中管理队列的任何内容都没有运行异步块?然而,我在这里抓着稻草,担心我最初的结论可能不正确。

这是我开始超出我的深度的地方,所以我想知道实际发生了什么,以及你建议什么分辨率.loadAllFromPreferences()以同步方式获得结果?

谢谢!

标签: swiftasynchronousclosuresgrand-central-dispatchdispatchsemaphore

解决方案


从文档中NETunnelProviderManager loadAllFromPreferences

该块会在加载操作完成后在调用者的主线程上执行

所以我们知道完成处理程序在主线程上。

我们也知道调用DispatchSemaphore wait将阻塞它正在运行的任何线程。鉴于此证据,您必须从主线程调用所有这些代码。由于您的调用wait阻塞了主线程,因此永远无法调用完成处理程序,因为主线程被阻塞了。

您尝试调用wait一些全局后台队列时可以清楚地说明这一点。这允许调用完成块,因为您的使用wait不再阻塞主线程。

而且您尝试loadAllFromPreferences从全局后台队列调用不会改变任何事情,因为它的完成块仍然在主线程上调用,并且您的调用wait仍然在主线程上。

完全阻塞主线程是个坏主意。正确的解决方案是重构此代码所在的任何方法以使用其自己的完成处理程序,而不是尝试使用正常的返回值。


推荐阅读