我遇到了这样一段代码:
id target = self.target;
SEL selector = self.selector;
if([target respondToSelector:@SEL(selector)])
{
[target performSelector:@SEL(selector)];
}
并且作者给出了这样的解释:
我们创建了两个局部变量,以避免在以下可能的执行顺序中出现竞争条件:
在某个主题A中调用
[target respondsToSelector:selector]
。在某个线程B中更改选择器的任一目标。
- 醇>
在主题A中调用
[target performSelector:selector]
。使用此代码,即使更改了选择器的任一目标,也会在右侧目标和选择器上调用performSelector。
我想知道的是:正在竞争哪种资源?这两个局部变量如何帮助避免竞争条件?我真的不知道比赛条件在哪里。提前谢谢!
答案 0 :(得分:2)
通常,竞争条件是多线程
的任何情况竞争条件问题通常很难显现,因为与许多其他类型的错误不同,表现异常行为所必需的事件序列是非确定性的,并且可能很少发生。但正如苹果在他们的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可以安全地使用其局部变量,从而消除了这种特殊的竞争条件。
但这并不意味着这真正的线程安全:
首先,如果多个线程确实对这两个属性执行了非同步访问,那么它们必须是绝对最小的原子属性。 (我不会因为我稍后会描述的原因而这样做,但在您的代码段中执行某些操作时,这是最低限度的。)
其次,这两个属性之间存在二次竞争条件。想象一下,self.target
和self.selector
分别引用一些Foo
对象和实例方法。让我们假设线程A获得Foo
目标,但在它有机会检索选择器之前,线程B滑入并将self.target
和self.selector
更改为某个内容完全不同的对象,比如一个Bar
对象。然后,线程A然后检索self.selector
,现在引用一些Bar
实例方法。当然,如果问题中的代码使用局部变量来保存target
和selector
(因为respondsToSelector
方法的Bar
,那么您的问题中的代码可能会崩溃Foo
目标可能会优雅地失败),但它也不会做你想做的事情。而这往往同样糟糕。
目标对象本身也存在其他线程安全注意事项。例如,如果该目标对象不是线程安全的,则线程B可能在改变目标对象的过程中途,并且当线程A尝试调用选择器方法时,它可能处于内部不一致状态。因此,您必须确认target
对象本身是否是线程安全的。
由于这些原因,虽然原始代码段中的模式解决了一个可能由一个狭窄的竞争条件导致的特定崩溃,但它并没有解决更广泛的其他问题。因此,典型的解决方案是编写synchronizes所有访问这些单独属性的代码(也可能是其他东西)。您可以使用GCD队列执行此操作(串行队列或读写器模式)或锁定或@synchronized
指令。
回到我之前关于原子属性的评论,如果你采用一些更广泛的同步模式来实现更强大的线程安全解决方案,那通常就不需要原子属性。