【问题标题】:Evidence of atomic / nonatomic in Objective-CObjective-C 中原子/非原子的证据
【发布时间】:2015-12-20 19:15:56
【问题描述】:

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

人.h

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

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

人.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

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

@property (strong, nonatomic) Person *p;

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

据我了解,我的日志中可能有 Bob FrostJack Sponge 输出,因为我将我的属性声明为 nonatomic。但那并没有发生。我不明白为什么。我是否遗漏了什么或误解了什么?

- (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;
}

【问题讨论】:

    标签: ios objective-c macos atomic


    【解决方案1】:

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

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

    此外,在运行并发操作之前,您可以在主线程中创建您的 Person 对象。当您当前的代码运行时,对象已经存在并且您不再更改它们的名称值,因此不会出现竞争条件或使用名称值进行部分写入。您只是在两个对象之间更改 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 属性之间延迟一个小的随机间隔。该延迟模拟了一个后台任务,需要花费一些时间来完成应该是原子的工作。它还引入了一个窗口,在该窗口中,另一个线程可以在当前线程能够设置第二个值之前更改第二个值。

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

    原子属性对上面的代码没有帮助。您需要在设置每个对象的 x1 和 x2 属性之间声明某种锁定(可能使用@synchronized 指令)。

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

    (注 2,给编辑我的代码的人:代码格式是风格和个人品味的问题。我使用“Allman 缩进”的变体。我很欣赏错别字更正,但我鄙视 K&R 风格的缩进。唐'不要将 your 风格强加于 my 代码。

    【讨论】:

    • 首先,感谢 Duncan 的时间和回答。我明白我们在这里想要做什么。但我不明白的是为什么这里的代码不会受到 x1x2 上的原子属性的帮助?我认为这就是使用原子属性的意义所在,不是吗?
    • 原子属性只确保读取或写入单个属性是“一次性”完成的(这就是“原子”的意思)说不是将值分配给 int,而是在复制10k 内存块。这将需要数千个时钟周期。如果另一个线程修改了相同的内存缓冲区,那么两组更改可能会“相互影响”。如果您使用原子属性,编译器将添加锁,以便在下一次执行之前完成一次写入,从而防止出现乱码。
    • 在您的示例和我的示例中,这两个值位于不同的属性中,但是没有什么可以阻止在写入第一个属性的同时写入第二个属性。 (Atomic 仅保护对单个属性的写入)
    • 对不起,我不是有意将我的风格强加于你的代码。我只是复制/粘贴您的代码,以便尝试在 Xcode 中编译和运行它。当我粘贴它时,由于缺少一些括号,缩进很乱,所以我像以前一样重新缩进。代码编译后我把它复制回来。
    【解决方案2】:

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

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

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

    2. 无法以原子方式加载/存储实际值的大型数据类型,无论保留/释放语义如何。

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

    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] 最新对象:
    objc[2172]:NSObject 对象 0x100c04a00 在已经释放时过度释放;打破 objc_overrelease_during_dealloc_error 进行调试

    对于 Data 结构,断言偶尔会失败:

    AtomicTest[2240:59304] *** -[Producer logStatus] 中的断言失败,main.m:58
    AtomicTest[2240:59304] *** 由于未捕获的异常“NSInternalInconsistencyException”而终止应用程序,原因:“错误: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)")
    }
    

    【讨论】:

      【解决方案3】:

      据我了解,我的日志中可以包含 Bob Frost 或 Jack Sponge 输出,因为我将我的属性声明为非原子属性。但那并没有发生。我不明白为什么。我是否遗漏了什么或误解了什么?

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

      Atomic 意味着您将始终获得一个 一致 值,我的意思是“您实际放入属性中的值”。如果没有原子性,就有可能返回一个不是 any 线程所写的值。考虑这个程序,它必须针对 32 位架构进行编译(这也意味着必须禁用 ARC,并且您需要声明您的 ivars 才能使其在 Mac 上工作;或者您可以在 32 位 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
      

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

      一个常见的解决方案是保留/自动释放 getter:

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

      这确保了_something 指向的任何内容至少会存在到当前自动释放池结束之前(如果您希望它超出此范围,则无论如何保留它是您的责任)。这比普通的 getter 慢很多。 atomic 也解决了这个问题,确保在设置过程中没有人被抓到。

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

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

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

      【讨论】:

      • 看起来你和我同时在研究类似的答案。但是,我认为它们很好地互补:)
      • @jtbandes 同意。您的结构示例比我的长示例更容易演示。
      • long long 示例非常有价值,它指出即使是简单的预定义类型也不能始终以原子方式加载/存储——32 位的使用非常好!
      猜你喜欢
      • 2012-12-25
      • 1970-01-01
      • 2017-03-26
      • 1970-01-01
      • 1970-01-01
      • 2012-04-09
      • 1970-01-01
      • 2011-01-16
      相关资源
      最近更新 更多