在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)
}
}
这种方法的好处似乎是:
- 保留执行顺序。 如果调用者不在主线程,我们将重定向到主线程,然后在 return 之前执行相同的函数-- 因此保留了从主线程调用原始函数时会发生的正常执行顺序,因为在主线程(或任何其他线程)上调用的函数默认情况下是同步执行的。
- 能够在没有编译器警告的情况下隐式引用 self。 在 Xcode 11.4 中,同步执行此调用也使编译器满意,可以隐式保留 self,因为调度上下文将在原始函数调用 returns 之前进入然后退出——所以我们不会从这种方法中得到任何新的编译器警告。很好很干净。
- 通过减少缩进更集中的差异。它避免将整个函数体包装在一个闭包中(就像你通常看到的那样,如果使用
Dispatch.main.async { ... }
,其中整个正文现在必须缩进一个更深的层次,在你的 PR 中产生空白差异,这会导致恼人的合并冲突,并使审阅者更难区分 GitHub 的 PR 差异视图中的显着元素。
同时,替代方案 DispatchQueue.main.async
似乎具有以下缺点:
- 可能会更改预期的执行顺序。 该函数会在执行分派的闭包之前 return,这反过来意味着
self
可能会在它之前解除分配运行。这意味着我们必须显式保留 self(或弱化它)以避免编译器警告。这也意味着,在此示例中,present(...)
不会在函数 return 之前被调用。这可能会导致模态窗口在调用站点后 一些其他代码之后弹出,从而导致意外行为。
- 弱化或显式保留的要求
self
。这并不是一个真正的缺点,但它在风格上不如隐式保留自我那么干净。
所以问题是:这些假设都是正确的,还是我遗漏了什么?
审查 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()
检查 isMainThread
是 false
是不够的,严格来说,要知道同步分派到主线程是否安全。
但这不是真正的问题。你显然在某个地方有一些例程认为它是 运行 在主线程上,但实际上不是。就个人而言,我会担心代码在这种误解下运行时还做了什么(例如,不同步的模型更新等)。
您的解决方法只是隐藏问题,而不是解决问题的根本原因。作为一般规则,我不建议围绕代码库中其他地方引入的错误进行编码。你真的应该弄清楚你从后台线程调用这个例程的位置并解决它。
关于如何找到问题,希望与崩溃相关的堆栈跟踪会告诉您。我还建议通过在方案设置中单击它旁边的小箭头来为主线程检查器添加一个断点:
然后运行应用程序,如果它遇到这个问题,它会在有问题的行暂停执行,这对于跟踪这些问题非常有用。这通常比从堆栈跟踪中进行逆向工程要容易得多。
在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)
}
}
这种方法的好处似乎是:
- 保留执行顺序。 如果调用者不在主线程,我们将重定向到主线程,然后在 return 之前执行相同的函数-- 因此保留了从主线程调用原始函数时会发生的正常执行顺序,因为在主线程(或任何其他线程)上调用的函数默认情况下是同步执行的。
- 能够在没有编译器警告的情况下隐式引用 self。 在 Xcode 11.4 中,同步执行此调用也使编译器满意,可以隐式保留 self,因为调度上下文将在原始函数调用 returns 之前进入然后退出——所以我们不会从这种方法中得到任何新的编译器警告。很好很干净。
- 通过减少缩进更集中的差异。它避免将整个函数体包装在一个闭包中(就像你通常看到的那样,如果使用
Dispatch.main.async { ... }
,其中整个正文现在必须缩进一个更深的层次,在你的 PR 中产生空白差异,这会导致恼人的合并冲突,并使审阅者更难区分 GitHub 的 PR 差异视图中的显着元素。
同时,替代方案 DispatchQueue.main.async
似乎具有以下缺点:
- 可能会更改预期的执行顺序。 该函数会在执行分派的闭包之前 return,这反过来意味着
self
可能会在它之前解除分配运行。这意味着我们必须显式保留 self(或弱化它)以避免编译器警告。这也意味着,在此示例中,present(...)
不会在函数 return 之前被调用。这可能会导致模态窗口在调用站点后 一些其他代码之后弹出,从而导致意外行为。 - 弱化或显式保留的要求
self
。这并不是一个真正的缺点,但它在风格上不如隐式保留自我那么干净。
所以问题是:这些假设都是正确的,还是我遗漏了什么?
审查 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()
检查 isMainThread
是 false
是不够的,严格来说,要知道同步分派到主线程是否安全。
但这不是真正的问题。你显然在某个地方有一些例程认为它是 运行 在主线程上,但实际上不是。就个人而言,我会担心代码在这种误解下运行时还做了什么(例如,不同步的模型更新等)。
您的解决方法只是隐藏问题,而不是解决问题的根本原因。作为一般规则,我不建议围绕代码库中其他地方引入的错误进行编码。你真的应该弄清楚你从后台线程调用这个例程的位置并解决它。
关于如何找到问题,希望与崩溃相关的堆栈跟踪会告诉您。我还建议通过在方案设置中单击它旁边的小箭头来为主线程检查器添加一个断点:
然后运行应用程序,如果它遇到这个问题,它会在有问题的行暂停执行,这对于跟踪这些问题非常有用。这通常比从堆栈跟踪中进行逆向工程要容易得多。