这些局部变量如何避免竞争条件?

How does these local variables works to avoid race condition?

我遇到了这样一段代码:

id target = self.target;
SEL selector = self.selector;
if([target respondToSelector:@SEL(selector)])
{
  [target performSelector:@SEL(selector)];
}

作者给出了这样的解释:

We created two local variables to avoid a race condition arising during the following possible sequence of execution:

  1. Invoking [target respondsToSelector:selector] in some thread A.

  2. Changing either target of the selector in some thread B.

  3. Invoking [target performSelector:selector] in thread A.

With this code, even if either target of the selector is changed, performSelector will be called on the right target and selector.

我想知道的是:正在争夺哪个资源?这两个局部变量如何帮助避免竞争条件?我真的不知道比赛条件在哪里。提前致谢!

一般来说,竞争条件是指多线程

  • 正在争夺一些共享资源;
  • 这些线程没有同步它们对这些资源的访问;因此
  • 最终的行为取决于各个线程中事件的精确计时或顺序。

竞争条件问题通常很难表现出来,因为与许多其他类型的错误不同,表现异常行为所需的事件顺序是 non-deterministic 并且很少发生。但是,尽管这些竞争条件看起来不太可能,正如 Apple 在其 Thread Sanitizer and Static Analyzer 视频中所说的那样,并不存在所谓的良性竞争条件。 (幸运的是,该视频中描述的 Thread Sanitizer 工具,又名 TSan,可以识别多种类型的数据竞争。)

回到您的示例,假设以下情况是否发生在线程 A 上:

if ([self.target respondToSelector:@SEL(self.selector)]) {
    [self.target performSelector:@SEL(self.selector)];
}

在此实现中,有可能在线程 A 确定 respondsToSelector 成功和它尝试 performSelector 之间,线程 B 可能已经滑入并更改了选择器或其他东西的目标。更糟糕的是,如果线程 B 将这些属性中的任何一个更改为无法执行的内容,应用程序可能会崩溃。

通过在局部变量中复制这些引用,如您的原始代码片段所示,开发人员消除了这种可能性。因为线程 A 中的这个例程 运行 具有目标和选择器引用的副本,所以现在 B 在 A 检查 respondsToSelector 和 A 调用 performSelector 之间是否更改属性并不重要。线程 A 可以安全地使用其局部变量,消除这种特殊的竞争条件。

但这并不代表这真的thread-safe:

  • 首先,如果多个线程确实对这两个属性执行非同步访问,那么它们至少必须是原子属性。 (我不会这样做的原因我稍后会描述,但在你的代码片段中做类似的事情时这是最低限度的。)

  • 其次,这两个属性之间存在次要竞争条件。想象一下 self.targetself.selector 分别引用一些 Foo 对象和实例方法。假设线程 A 获得了 Foo 目标,但在它有机会检索选择器之前,线程 B 悄悄进入并将 self.targetself.selector 更改为一个完全不同的对象,说一个 Bar 对象。然后线程 A 然后检索 self.selector 现在引用一些 Bar 实例方法。当然,如果您的问题中的代码使用局部变量来保存 targetselector(因为 Bar 方法的 respondsToSelector Bar Foo 目标可能会优雅地失败),但它也不会做你想做的事。而这往往同样糟糕。

  • 目标对象本身显然还有其他 thread-safety 考虑因素。例如,如果该目标对象不是 thread-safe,则线程 B 在改变目标对象的过程中可能是 half-way,并且当线程 A 尝试调用选择器方法时,它可能处于内部不一致状态.因此,您必须确认 target 对象本身是否是 thread-safe.

由于这些原因,虽然您原始代码片段中的模式解决了一个可能由一种狭窄的竞争条件导致的特定崩溃,但它没有解决更广泛的其他问题。因此,典型的解决方案是编写 synchronizes 可以访问这些单独属性(以及其他可能的属性)的代码。您可以使用 GCD 队列(串行队列或 reader-writer 模式)或锁或 @synchronized 指令来执行此操作。

并且,回到我之前关于原子属性的评论,如果您采用更广泛的同步模式来实现更强大的 thread-safe 解决方案,通常可以避免对原子属性的需求。