简体   繁体   English

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

[英]Evidence of atomic / nonatomic in Objective-C

After reading Apple's documentation , I try to put in evidence atomicity or non-atomicity of a property in Objective-C. 在阅读Apple的文档后 ,我尝试在Objective-C中提供属性的原子性或非原子性。 To do this I create a class Person which has first and last name. 为此,我创建了一个具有名和姓的Person。

Person.h 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 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

In another class, here my AppDelegate, I have a nonatomic property which is an instance of Person. 在另一个类中,我的AppDelegate,我有一个非原子属性,它是Person的一个实例。

@property (strong, nonatomic) Person *p;

In the implementation file, I create three concurrent queues. 在实现文件中,我创建了三个并发队列。 In the first queue I read the property, in two other queues I write different values of person. 在第一个队列中,我读取了属性,在另外两个队列中,我写了不同的person值。

From what I understand, I could have Bob Frost or Jack Sponge output in my log, since I declared my property as nonatomic . 根据我的理解,我可以在我的日志中输出Bob FrostJack Sponge ,因为我声明我的属性是非原子的 But that didn't happened. 但那并没有发生。 I don't understand why. 我不明白为什么。 Am I missing something or misunderstanding something? 我错过了什么或误解了什么吗?

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

Having non-atomic properties makes the possibility of partial writes possible, but by no means certain. 具有非原子属性使得部分写入的可能性成为可能,但决不是确定的。

In your Person class the only way you are setting first and last names is in the init method, and then you set the first name and then the last name immediately after. 在Person类中,设置名字和姓氏的唯一方法是在init方法中,然后设置第一个名称,然后立即设置姓氏。 Setting the first name and last name will occur VERY close to each other, with little chance for another thread to mess things up between operations. 设置名字和姓氏将非常接近彼此,很少有机会让另一个线程在操作之间弄乱。

Furthermore, you create your Person objects in the main thread, before you running concurrent operations. 此外,在运行并发操作之前,在主线程中创建Person对象。 By the time your current code runs, the objects already exist and you no longer change their name values, so there's no chance of a race condition or a partial write with name values. 当您的当前代码运行时,对象已经存在,您不再更改其名称值,因此不存在竞争条件或具有名称值的部分写入的可能性。 You are simply changing self.p between 2 objects that don't change once they are created. 您只是在2个对象之间更改self.p,这些对象在创建后不会更改。

That said, what IS unpredictable about your code is what person object will be in self.p at any instant. 也就是说,你的代码无法预测的是什么人物在任何时刻都会在self.p中出现。 You should see the values displayed alternate between Bob Sponge and Jack Frost unpredictably. 您应该看到Bob Sponge和Jack Frost之间显示的值无法预测。

A better test would be something like this: 更好的测试是这样的:

(Assume each TestObject's x1 and x2 values should always be kept the same.) (假设每个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

And then code like this: 然后像这样的代码:

#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);
 }

In the code above, each queue creates a series of random values, and sets both the x1 and x2 properties of a couple of objects to those random values in a repeating loop. 在上面的代码中,每个队列都会创建一系列随机值,并将一对对象的x1和x2属性设置为重复循环中的随机值。 It delays for a small random interval between setting the x1 and x2 property of each object. 它延迟了设置每个对象的x1和x2属性之间的小的随机间隔。 That delay simulates a background task taking some amount of time to finish work that should be atomic. 该延迟模拟后台任务需要一些时间来完成应该是原子的工作。 It also introduces a window where another thread could change the second value before the current thread is able to set the second value. 它还引入了一个窗口,其中另一个线程可以在当前线程能够设置第二个值之前更改第二个值。

If you run the code above you will almost certainly find that the x1 and x2 values of thing1 and thing2 are sometimes different. 如果你运行上面的代码,你几乎肯定会发现thing1和thing2的x1和x2值有时是不同的。

The code above would not be helped by atomic properties. 上面的代码对原子属性没有帮助。 You would need to assert a lock of some sort between setting the x1 and x2 property of each object (perhaps using the @synchronized directive). 您需要在设置每个对象的x1和x2属性之间声明某种锁(可能使用@synchronized指令)。

(Note that I banged the code above together in the forum editor. I haven't tried to compile it, much less debug it. There are doubtless a few typos.) (请注意,我在论坛编辑器中将上面的代码组合在一起。我没有尝试编译它,更不用说调试它了。毫无疑问有一些错别字。)

(Note 2, to the person who edited my code: Code formatting is a matter of style and personal taste. I use a variation on "Allman indentation." I appreciate the typos corrections, but I despise K&R style indentation. Don't impose your style on my code. (注2,编辑我的代码的人:代码格式是风格和个人品味的问题。我使用“Allman缩进”的变体。我欣赏错别字修正,但我鄙视K&R风格缩进。不要强加我的代码风格。

A property being atomic means that all actions performed by a read, and all actions performed by a write, are done atomically. atomic属性意味着读取执行的所有操作以及写入执行的所有操作都是以原子方式完成的。 (This is completely independent of consistency between two separate properties, as in your example, which cannot be achieved simply by adding (atomic) .) (这完全独立于两个单独属性之间的一致性,如在您的示例中,仅通过添加(atomic)无法实现。)

This matters particularly in two cases: 这在两种情况下尤为重要:

  1. For object pointers, the implicit [_property release]; [newValue retain]; _property = newValue 对于对象指针,隐式[_property release]; [newValue retain]; _property = newValue [_property release]; [newValue retain]; _property = newValue [_property release]; [newValue retain]; _property = newValue operations that ARC performs when you store a new value, and the implicit value = _property; [value retain]; [_property release]; [newValue retain]; _property = newValue ARC在存储新值时执行的[_property release]; [newValue retain]; _property = newValue操作,隐含value = _property; [value retain]; value = _property; [value retain]; which happens when you load the value. 加载值时会发生这种情况。

  2. Large datatypes whose actual values can't be atomically loaded/stored, regardless of retain/release semantics. 无论保留/释放语义如何,其实际值都无法以原子方式加载/存储的大型数据类型。

Here is an example which illustrates both potential problems: 这是一个说明两个潜在问题的例子:

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

With nonatomic , for the object property, you'll occasionally get EXC_BAD_ACCESS crashes, and log messages like this: 对于nonatomic ,对于object属性,偶尔会出现EXC_BAD_ACCESS崩溃,并记录如下消息:

AtomicTest[2172:57275] Latest object: <NSObject: 0x100c04a00> AtomicTest [2172:57275]最新对象:<NSObject:0x100c04a00>
objc[2172]: NSObject object 0x100c04a00 overreleased while already deallocating; objc [2172]:NSObject对象0x100c04a00在已经解除分配时被过度释放; break on objc_overrelease_during_dealloc_error to debug 打破objc_overrelease_during_dealloc_error进行调试

And for the Data struct, the assertion will occasionally fail: 对于Data结构,断言偶尔会失败:

AtomicTest[2240:59304] *** Assertion failure in -[Producer logStatus], main.m:58 AtomicTest [2240:59304] ***断言失败 - [Producer logStatus],main.m:58
AtomicTest[2240:59304] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'WRONG: 55937112^2 != 3128960610774769' AtomicTest [2240:59304] ***由于未捕获的异常'NSInternalInconsistencyException'而终止应用程序,原因:'错误:55937112 ^ 2!= 3128960610774769'

(Notice that the value of xSquared , 3128960610774769, is actually 55937113 2 rather than 55937112 2 .) (请注意, xSquared的值xSquared实际上是55937113 2而不是55937112 2.

Making the properties (atomic) rather than (nonatomic) avoids both of these problems, at the cost of slightly slower execution. 使属性(atomic)而不是(nonatomic)避免了这两个问题,代价是执行速度稍慢。


Side note: the same problem occurs even in Swift, because there is no notion of atomic properties: 旁注:即使在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. 根据我的理解,我可以在我的日志中输出Bob Frost或Jack Sponge,因为我声明我的属性是非原子的。 But that didn't happened. 但那并没有发生。 I don't understand why. 我不明白为什么。 Am I missing something or misunderstanding something ? 我错过了什么或误解了什么吗?

If you triggered the race condition, this isn't what would happen. 如果你触发了竞争条件,那就不会发生这种情况。 What would almost certainly happen is you would crash or you would get something really surprising. 几乎可以肯定的是,你会崩溃,或者你会得到一些非常令人惊讶的东西。

Atomic means that you will always get a consistent value, by which I mean "a value you actually put in the property." 原子意味着您将始终获得一致的价值,我的意思是“您实际投入房产的价值”。 Without atomicy, it's possible to get a value back that isn't what any thread wrote. 如果没有atomicy,就有可能得到一个不是任何线程所写的值。 Consider this program, which must be compiled against a 32-bit architecture (which also means ARC must be disabled, and you need to declare your ivars to get this to work on Mac; or you could test this on a 32-bit iPhone). 考虑一下这个程序,它必须针对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;
}

If you run this for more than a few seconds, you will get lots of messages like: 如果你运行这个超过几秒钟,你将收到许多消息,如:

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

You'll note that neither 4294967295 nor 9223372032559808512 show up in the program anywhere. 你会注意到4294967295和9223372032559808512都没有出现在程序中的任何地方。 How do they show up in the output? 它们如何出现在输出中? Because I'm writing a 64-bit number using 32-bit code. 因为我正在使用32位代码编写64位数字。 There is no single machine instruction that will write all 64-bits at the same time. 没有单个机器指令可以同时写入所有64位。 First half the number will be written, then the other half. 上半部分将编写,然后另一半。 If another queue is writing at the same time, you can wind up with the top 32-bits from one write and the bottom 32-bits from the other. 如果另一个队列同时写入,则可以使用一次写入的前32位和另一次写入的底部32位。 atomic prevents this by locking the memory until it writes all of the words. atomic通过锁定内存直到它写入所有单词来防止这种情况。

A different problem can happen with objects. 对象可能会出现另一个问题。 It was particularly problematic prior to ARC but can still happen. 在ARC之前它特别成问题,但仍然可能发生。 Consider the following very common ObjC-1 code (ie before properties): 考虑以下非常常见的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

This was a very common way to write accessors. 这是编写访问器的一种非常常见的方法。 Handle retain-new/release-old during set. 在设置期间处理保留 - 新/释放旧。 Just return the bar pointer during get. 在get期间返回条形指针。 This is basically the implementation of nonatomic today. 这基本上是今天nonatomic的实现。 The problem is that the memory management isn't thread-safe. 问题是内存管理不是线程安全的。 Consider if you had just called [_something release] on one thread, and then on another thread call the getter. 考虑一下你是否刚刚在一个线程上调用[_something release] ,然后在另一个线程上调用getter。 You'd get the old value of _something , which had already been released, and possibly already deallocated. 你会得到_something的旧值,它已经被释放,并且可能已经被释放了。 So you may be looking at invalid memory, and you'll crash. 所以你可能会看到无效的内存,你会崩溃。

One common solution was the retain/autorelease getter: 一个常见的解决方案是retain / autorelease getter:

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

This made certain that whatever _something pointed to would exist at least until the end of the current autorelease pool (if you wanted it beyond that, it was your responsibility to retain it anyway). 这取得了一定的,无论_something指出,至少会存在,直到目前的自动释放池的结束(如果你想它超出了,这是你的责任,反正把它保留下来)。 This is quite a bit slower than the trivial getter. 这比琐碎的吸气剂慢得多。 atomic also addresses this problem by making sure no one catch get while you're in the middle of setting. atomic也通过确保在设置过程中没有人抓住来解决这个问题。

All that said, while in a few cases this can be valuable, almost always if you're accessing data across multiple queues, atomic isn't sufficient, and is slow anyway (at least it used to be; I haven't profiled recent versions because I never use atomic ). 所有这一切,虽然在少数情况下,这可能是有价值的,几乎总是如果您访问多个队列中的数据, atomic是不够的,并且无论如何都很慢(至少它曾经是;我没有描述最近版本,因为我从不使用atomic )。 If all you want is single-property atomicy, a GCD accessor is usually better. 如果你想要的只是单属性atomicy, GCD访问器通常更好。 If you need a fully atomic transaction (which you often do), then the GCD accessor can be adapted pretty easily to that, too. 如果您需要完全原子事务(您经常这样做),那么GCD访问器也可以很容易地适应它。

Probably the best discussion of this is bbum's blog post: http://www.friday.com/bbum/2008/01/13/objectivce-c-atomic-properties-threading-andor-custom-settergetter/ . 可能最好的讨论是bbum的博客文章: http ://www.friday.com/bbum/2008/01/13/objectivce-c-atomic-properties-threading-andor-custom-settergetter/。 The short answer is that it is very rare that atomic is actually helpful. 简短的回答是, atomic实际上很有用。 If you think you need need atomic , you generally need more than it gives you, and can usually get it for cheaper using GCD accessors. 如果你认为你需要atomic ,你通常需要比它更多的东西,并且通常可以使用GCD访问器以更便宜的价格获得它。

Making atomic the default was one of the great mistakes Apple made in ObjC2. atomic为默认值是Apple在ObjC2中犯下的重大错误之一。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM