简体   繁体   中英

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. To do this I create a class Person which has first and last name.

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

In another class, here my AppDelegate, I have a nonatomic property which is an instance of 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.

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?

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

That said, what IS unpredictable about your code is what person object will be in self.p at any instant. You should see the values displayed alternate between Bob Sponge and Jack Frost unpredictably.

A better test would be something like this:

(Assume each TestObject's x1 and x2 values should always be kept the same.)

@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. It delays for a small random interval between setting the x1 and x2 property of each object. 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.

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

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

A property being atomic means that all actions performed by a read, and all actions performed by a write, are done atomically. (This is completely independent of consistency between two separate properties, as in your example, which cannot be achieved simply by adding (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 operations that ARC performs when you store a new value, and the implicit 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:

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

And for the Data struct, the assertion will occasionally fail:

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'

(Notice that the value of xSquared , 3128960610774769, is actually 55937113 2 rather than 55937112 2 .)

Making the properties (atomic) rather than (nonatomic) avoids both of these problems, at the cost of slightly slower execution.


Side note: the same problem occurs even in Swift, because there is no notion of atomic properties:

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 ?

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

// 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. How do they show up in the output? Because I'm writing a 64-bit number using 32-bit code. There is no single machine instruction that will write all 64-bits at the same time. 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. atomic prevents this by locking the memory until it writes all of the words.

A different problem can happen with objects. It was particularly problematic prior to ARC but can still happen. Consider the following very common ObjC-1 code (ie before properties):

@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. This is basically the implementation of nonatomic today. 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. You'd get the old value of _something , which had already been released, and possibly already deallocated. So you may be looking at invalid memory, and you'll crash.

One common solution was the 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). 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.

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 ). If all you want is single-property atomicy, a GCD accessor is usually better. If you need a fully atomic transaction (which you often do), then the GCD accessor can be adapted pretty easily to that, too.

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/ . The short answer is that it is very rare that atomic is actually helpful. 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.

Making atomic the default was one of the great mistakes Apple made in ObjC2.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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