Objective-C 中原子/非原子的证据

Evidence of atomic / nonatomic in Objective-C

阅读Apple's documentation后,我尝试在Objective-C中加入属性的证据原子性或非原子性。为此,我创建了一个 class 具有名字和姓氏的人。

Person.h

@interface Person : NSObject
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;

- (instancetype)initWithFirstName:(NSString *)fn lastName:(NSString *)ln;
@end

Person.m

@implementation Person

- (instancetype)initWithFirstName:(NSString *)fn lastName:(NSString *)ln {
    if (self = [super init]) {
        self.firstName = fn;
        self.lastName = ln;
    }
    return self;
}

- (NSString *)description {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}

@end

在另一个 class 中,这里是我的 AppDelegate,我有一个非原子 属性,它是 Person 的一个实例。

@property (strong, nonatomic) Person *p;

在实现文件中,我创建了三个并发队列。在第一个队列中,我读取了 属性,在另外两个队列中,我写入了不同的 person 值。

据我所知,我可以在我的日志中输出 Bob FrostJack Sponge,因为我声明了我的 属性作为非原子。但那并没有发生。我不明白为什么。我是不是遗漏了什么或误解了什么?

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.

    Person *bob = [[Person alloc] initWithFirstName:@"Bob" lastName:@"Sponge"];
    Person *jack = [[Person alloc] initWithFirstName:@"Jack" lastName:@"Frost"];
    self.p = bob;

    dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue3 = dispatch_queue_create("queue3", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue1, ^{
        while (YES) {
            NSLog(@"%@", self.p);
        }
    });

    dispatch_async(queue2, ^{
        while (YES) {
            self.p = bob;
        }
    });

    dispatch_async(queue3, ^{
        while (YES) {
            self.p = jack;
        }
    });

    return YES;
}

具有非原子属性使得部分写入的可能性成为可能,但绝不是确定的。

在您的 Person class 中,设置名字和姓氏的唯一方法是在 init 方法中,然后设置名字,然后紧接着设置姓氏。设置名字和姓氏将非常接近彼此,另一个线程几乎没有机会在操作之间搞砸。

此外,在 运行 并发操作之前,您在主线程中创建了 Person 对象。到您当前的代码 运行 时,对象已经存在并且您不再更改它们的名称值,因此不会出现竞争条件或使用名称值进行部分写入。您只是在 2 个对象之间更改 self.p,这些对象一旦创建就不会更改。

就是说,您的代码无法预测的是 self.p 中随时会有什么 person 对象。您应该看到显示的值在 Bob Sponge 和 Jack Frost 之间不可预测地交替出现。

更好的测试应该是这样的:

(假设每个 TestObject 的 x1 和 x2 值应始终保持相同。)

@interface TestObject : NSObject
@property (nonatomic, assign) int x1;
@property (nonatomic, assign) int x2;
@end

@interface AppDelegate
@property (nonatomic, strong) TestObject *thing1;
@property (nonatomic, strong) TestObject *thing2;
@property (nonatomic, strong) NSTimer *aTimer;
@property (nonatomic, strong) NSTimer *secondTimer;
@end

然后像这样编码:

#include <stdlib.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
  dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
  dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);

  self.thing1 = [[TestObject alloc] init];
  self.thing2 = [[TestObject alloc] init];

  dispatch_async(queue1, ^
  {
    for (int x = 0; x < 100; x++) 
    {
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      int thing1Val = arc4random_uniform(10000);
      int thing2Val = arc4random_uniform(10000);
      _thing1.x1 = thing1Val;
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      _thing2.x1 = thing2Val;
      _thing1.x2 = thing1Val; //thing1's x1 and x2 should now match
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      _thing2.x2 = thing2Val; //And now thing2's x1 and x2 should also both match
    }
  });


  //Do the same thing on queue2
  dispatch_async(queue2, ^
  {
    for (int x = 0; x < 100; x++) 
    {
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      int thing1Val = arc4random_uniform(10000);
      int thing2Val = arc4random_uniform(10000);
      _thing1.x1 = thing1Val;
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      _thing2.x1 = thing2Val;
      _thing1.x2 = thing1Val; //thing1's x1 and x2 should now match
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      _thing2.x2 = thing2Val; //And now thing2's x1 and x2 should also both match
    }
  });

  //Log the values in thing1 and thing2 every .1 second
  self.aTimer = [NSTimer scheduledTimerWithTimeInterval:.1
    target:self
    selector:@selector(logThings:)
    userInfo:nil
    repeats:YES];

  //After 5 seconds, kill the timer.
  self.secondTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
    target:self
    selector:@selector(stopRepeatingTimer:)
    userInfo:nil
    repeats:NO];
  return YES;
}

- (void)stopRepeatingTimer:(NSTimer *)timer 
{
  [self.aTimer invalidate];
}

- (void)logThings:(NSTimer *)timer 
{
  NSString *equalString;
  if (_thing1.x1 == _thing1.x2) 
  {
    equalString = @"equal";
  }
    else 
  {
    equalString = @"not equal";
  }
  NSLog(@"%@ : thing1.x1 = %d, thing1.x2 = %d", 
    equalString, 
    _thing1.x1, 
    _thing1.x2);

  if (_thing2.x1 == _thing2.x2) 
    {
      equalString = @"equal";
    }
  else 
    {
      equalString = @"not equal";
    }
  NSLog(@"%@ : thing2.x1 = %d, thing2.x2 = %d", 
    equalString, 
    _thing2.x1, 
    _thing2.x2);
 }

在上面的代码中,每个队列创建一系列随机值,并在重复循环中将几个对象的 x1 和 x2 属性设置为这些随机值。它在设置每个对象的 x1 和 x2 属性 之间延迟一个小的随机间隔。该延迟模拟后台任务需要花费一些时间来完成本应是原子的工作。它还引入了一个 window,其中另一个线程可以在当前线程能够设置第二个值之前更改第二个值。

如果您运行上面的代码,您几乎肯定会发现 thing1 和 thing2 的 x1 和 x2 值有时是不同的。

上面的代码不会得到原子属性的帮助。您需要在设置每个对象的 x1 和 x2 属性 之间断言某种锁(可能使用 @synchronized 指令)。

(请注意,我在论坛编辑器中将上面的代码拼在一起。我没有尝试编译它,更不用说调试了。肯定有一些错别字。)

(注意 2,对编辑我的代码的人:代码格式是风格和个人品味的问题。我使用 "Allman indentation." 的变体. 不要将你的风格强加到我的代码上。

A 属性 是 atomic 意味着读取执行的所有操作以及写入执行的所有操作都是原子完成的。 (这完全独立于 两个单独的 属性之间的一致性,如在您的示例中,这不能简单地通过添加 (atomic) 来实现。)

这在两种情况下尤为重要:

  1. 对于对象指针,ARC 在存储新值时执行的隐式 [_property release]; [newValue retain]; _property = newValue 操作,以及在加载值时发生的隐式 value = _property; [value retain];

  2. 实际值不能原子化的大型数据类型 loaded/stored,无论 retain/release 语义如何。

这是一个说明这两个潜在问题的示例:

typedef struct {
    NSUInteger x;
    NSUInteger xSquared;  // cached value of x*x
} Data;


@interface Producer : NSObject

@property (nonatomic) Data latestData;
@property (nonatomic) NSObject *latestObject;

@end


@implementation Producer

- (void)startProducing
{
    // Produce new Data structs.
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (NSUInteger x = 0; x < NSUIntegerMax; x++) {
            Data newData;
            newData.x = x;
            newData.xSquared = x * x;

            // Since the Data struct is too large for a single store,
            // the setter actually updates the two fields separately.
            self.latestData = newData;
        }
    });

    // Produce new objects.
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (true) {
            // Release the previous value; retain the new value.
            self.latestObject = [NSObject new];
        }
    });

    [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(logStatus) userInfo:nil repeats:YES];
}

- (void)logStatus
{
    // Implicitly retain the current object for our own uses.
    NSObject *o = self.latestObject;
    NSLog(@"Latest object: %@", o);

    // Validate the consistency of the data.
    Data latest = self.latestData;
    NSAssert(latest.x * latest.x == latest.xSquared, @"WRONG: %lu^2 != %lu", latest.x, latest.xSquared);
    NSLog(@"Latest data: %lu^2 = %lu", latest.x, latest.xSquared);
}

@end



int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[Producer new] startProducing];
        [[NSRunLoop mainRunLoop] run];
    }
    return 0;
}

使用 nonatomic,对于对象 属性,您偶尔会遇到 EXC_BAD_ACCESS 崩溃,并记录如下消息:

AtomicTest[2172:57275] Latest object: <NSObject: 0x100c04a00>
objc[2172]: NSObject object 0x100c04a00 overreleased while already deallocating; break on objc_overrelease_during_dealloc_error to debug

而对于数据结构,断言偶尔会失败:

AtomicTest[2240:59304] *** Assertion failure in -[Producer logStatus], main.m:58
AtomicTest[2240:59304] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'WRONG: 55937112^2 != 3128960610774769'

(请注意,xSquared 的值 3128960610774769 实际上是 559371132 而不是 559371122。)

将属性设置为 (atomic) 而不是 (nonatomic) 可以避免这两个问题,但代价是执行速度稍慢。


旁注:即使在 Swift 中也会出现同样的问题,因为没有原子属性的概念:

class Object { }
var obj = Object()

dispatch_async(dispatch_get_global_queue(0, 0)) {
    while true {
        obj = Object()
    }
}

while true {
    // This sometimes crashes, and sometimes deadlocks
    let o = obj
    print("Current object: \(o)")
}

From what I understand, I could have Bob Frost or Jack Sponge output in my log, since I declared my property as nonatomic. But that didn't happened. I don't understand why. Am I missing something or misunderstanding something ?

如果您触发了竞争条件,则不会发生这种情况。几乎肯定会发生的是你会崩溃或者你会得到一些真的令人惊讶的东西。

Atomic 意味着你总是会得到一个 consistent 值,我的意思是 "a value you actually put in the property." 如果没有 atomic,就有可能得到一个不是什么的值任何 线程已写。考虑这个程序,它必须针对 32 位架构进行编译(这也意味着必须禁用 ARC,并且您需要声明您的 ivars 才能使其在 Mac 上运行;或者您可以在 32 位架构上测试它-bit iPhone).

// clang -arch i386 -framework Foundation atomic.m -o atomic ; ./atomic
#import <Foundation/Foundation.h>

@interface MyObject : NSObject {
    long long i;
}
@property (nonatomic) long long i;
@end

@implementation MyObject
@synthesize i;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queue3 = dispatch_queue_create("queue3", DISPATCH_QUEUE_CONCURRENT);

        MyObject *obj = [MyObject new];

        long long value1 = 0;
        long long value2 = LLONG_MAX;

        dispatch_async(queue2, ^{
            while (YES) {
                obj.i = value1;
            }
        });

        dispatch_async(queue3, ^{
            while (YES) {
                obj.i = value2;
            }
        });
        while (YES) {
            long long snapshot = obj.i;
            if (snapshot != value1 && snapshot != value2) {
                printf("***PANIC*** Got %lld (not %lld or %lld)\n", snapshot, value1, value2);
            }
        }
    }
    return 0;
}

如果你 运行 这超过几秒钟,你会收到很多消息,例如:

***PANIC*** Got 4294967295 (not 0 or 9223372036854775807)
***PANIC*** Got 9223372032559808512 (not 0 or 9223372036854775807)

您会注意到程序中的任何地方都没有显示 4294967295 和 9223372032559808512。它们如何出现在输出中?因为我正在使用 32 位代码编写 64 位数字。没有一条机器指令可以同时写入所有 64 位。先写一半的数字,然后是另一半。如果另一个队列同时写入,您可以从一个写入的顶部 32 位和另一个写入的底部 32 位结束。 atomic 通过锁定内存直到它写入所有单词来防止这种情况。

对象可能会发生不同的问题。这在 ARC 之前尤其成问题,但仍然可能发生。考虑以下非常常见的 ObjC-1 代码(即在属性之前):

@interface MyObject : NSObject {
    id _something;
}
- (id)something;
- (void)setSomething:(id)newSomething;
@end

@implementation MyObject

- (id)something {
    return _something;
}

- (void)setSomething:(id)newSomething {
    [newSomething retain];
    [_something release];
    _something = newSomething;
}

@end

这是一种非常常见的编写访问器的方法。在设置期间处理 retain-new/release-old。只是 return 获取期间的条形指针。这就是今天nonatomic的基本实现。问题是内存管理不是线程安全的。考虑一下您是否刚刚在一个线程上调用了 [_something release],然后在另一个线程上调用了 getter。你会得到 _something 的旧值,它已经被释放,并且可能已经被释放。所以你可能正在查看无效内存,然后你会崩溃。

一个常见的解决方案是 retain/autorelease getter:

- (id)something {
    return [[_something retain] autorelease];
}

这确保了 _something 指向的任何内容至少会存在到当前自动释放池的末尾(如果您想要超出此范围,无论如何保留它是您的责任)。这比普通的 getter 慢很多。 atomic 还通过确保在设置过程中无人捕获来解决此问题。

综上所述,虽然在某些情况下这可能很有价值,但几乎总是在跨多个队列访问数据时,atomic 是不够的,而且速度很慢(至少它使用过是;我没有介绍最近的版本,因为我从不使用 atomic)。如果你想要的只是单属性 原子性,GCD accessor 通常更好。如果您需要一个完全原子的事务(您经常这样做),那么 GCD 访问器也可以很容易地适应它。

最好的讨论可能是 bbum 的博客 post:http://www.friday.com/bbum/2008/01/13/objectivce-c-atomic-properties-threading-andor-custom-settergetter/。简短的回答是 atomic 很少有实际帮助。如果您认为您需要 atomic,您通常需要的比它给您的要多,通常可以使用 GCD 访问器以更便宜的价格获得它。

atomic 设置为默认值是 Apple 在 ObjC2 中犯下的重大错误之一。