【问题标题】:Thread safety of local static variable on Objective-CObjective-C上局部静态变量的线程安全
【发布时间】:2014-09-08 21:01:35
【问题描述】:

因为下面的代码是 Objective-C 上非常常见的模式,用于创建实例并确保它是线程安全的。 然而,这个线程安全基于一个重要条件,即编译器保证本地静态变量是线程安全的,这意味着静态_sharedCache 指针将保证以线程安全的方式创建,但是我不能找出有关此的任何文档。有谁能给我更自信的证据吗? (由于这里的人们一直关注我一开始使用的可变集,所以我只是将其更改为 NSCache,这真的不是重点。我在这里谈论的是关于创建本地静态指针的线程安全(不是这个指针指向的实例)

+ (NSCache*)sharedCache {
  static NSCache* _sharedCache = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _sharedCache = [[NSCache alloc] init];
  });
  return _sharedCache;
}

万一我没有把竞态条件描述清楚,考虑两个线程同时调用这个API,同时检查这个静态_sharedCache指针是否存在,如果不存在,它们会自己创建一个新的静态指针。那么这里可能是 NSMutableSet 的两个静态指针,即使 dispatch_once 保证实例初始化只发生一次,也就是只针对这两个静态指针之一,然后在 dispatch_once 块之后,第一个调用者得到一个满意的结果,但是第二个调用者只得到一个 nil 指针;

考虑这种情况,A和B是两个线程,同时进入这段代码,A查看这里的静态指针“_sharedSet”,发现这里没有这个指针,(指针是unsigned long的实例),所以创建一个并将0分配给它,现在这个指针存储在内存地址(0xAAAAAAAA)中,同时B做了同样的行为,在堆中创建一个静态指针但内存地址(0xBBBBBBBB),然后dispatch_once阻塞两个线程直到它完成,所以它为创建的 NSSet 实例分配了一个新的指针值,并将该值分配给地址 0xAAAAAAAA,但是地址 0xBBBBBBBB 上的值仍然为 0,因为没有人更新它,所以线程 B 只返回一个 nil。所以基本上我的问题不是怀疑 dispatch_once,而是关于创建局部静态变量的线程安全性,这是 C++ 上的一个有效问题,直到 C11 解决它。我只是想知道 Clang 是否也注意到了这个问题。

而且这个问题真的不是dispatch_once,是局部静态变量,是具体的非对象变量,就像这里提到的指针,或者换个方式问,如果下面的代码在竞争线程中调用,是否保证这个 "_intBuffer" 在堆中只有一个实例?

    +(int)sharedIntBuffer {
        static int _intBuffer = 0;
        return _intBuffer;
    }

【问题讨论】:

  • NSMutableSet 不是线程安全的。所以,不,上面的代码不是线程安全的,但不是出于您担心的原因。请参阅下面@Caleb 的回答。
  • 非常感谢您的重播,但这不是我的意思,在开头列出不正确的演示是我的错。我刚刚更新了我的问题以避免进一步的误解

标签: objective-c multithreading race-condition static-variables


【解决方案1】:

您的代码更大的潜在问题是您共享一个可变集。正如 gnasher 解释的那样,由于该答案中解释的原因,您显示的使用 dispatch_once() 分配集合的代码很好。但是一旦完成,您可能会跨线程共享一个可变集。除非您采取措施将 access 同步到该集合,否则您很容易遇到两个或多个线程同时更改该集合的问题。防止此类问题的最简单、最可靠的方法是将集合封装在一个类中,以确保以线程安全的方式访问集合,例如使用串行调度队列。

【讨论】:

  • 是的,这是一个已知问题,但不是重点,这里只是为了演示。
【解决方案2】:

这就是 dispatch_once 的全部意义所在。 dispatch_once 代码百分百保证准确执行once,这就是它被称为 dispatch_once 的原因。并且百分百保证第二次及以后的调用将等到第一次调用完成,并且所有内存写入都已完成。

【讨论】:

  • 考虑这种情况,A和B是两个线程,同时进入这段代码,A查看这里的静态指针“_sharedSet”,发现这里没有这个指针,(指针是unsigned long的一个实例) ,所以它创建一个并分配0给它,现在这个指针存储在内存地址(0xAAAAAAAA)中,同时B做了同样的行为,在堆中创建一个静态指针但内存地址(0xBBBBBBBB),然后dispatch_once阻塞了两个线程直到完成,所以它为创建的 NSSet 实例分配了一个新的指针值,并将该值分配给地址 0xAAAAAAAA,
  • 但是地址 0xBBBBBBBB 上的值仍然为 0,因为没有人更新它,所以线程 B 只返回一个 nil。所以基本上我的问题不是怀疑 dispatch_once,而是关于创建局部静态变量的线程安全性,这是 C++ 上的一个有效问题,直到 C11 解决它。我只是想知道 Clang 是否也注意到了这个问题,
【解决方案3】:

和一个懂编译器的人聊过之后,我才明白,和全局静态变量一样,局部静态变量也是在main()开始之前初始化的。只有它不能被访问,因为没有参考来表明它。所以回到这个问题上。即使两个线程同时进入这些+(NSCahce*)sharedCache,也会只有一个静态_sharedCache 指针的实例。这意味着比赛条件在这里无效。

【讨论】:

    猜你喜欢
    • 2014-12-24
    • 1970-01-01
    • 1970-01-01
    • 2011-04-27
    • 1970-01-01
    • 1970-01-01
    • 2017-03-16
    • 1970-01-01
    相关资源
    最近更新 更多