In Objective-C, nonatomic is often used to describe attributes when declaring them, but when we go to an interview, we are always asked about atomic, so we don’t know where to start, or we have to simply say that the attributes modified by atomic are “atomic” and cannot be read and written at the same time. This article explores the principles behind atomic.

1. Find the entrance

Because the syntax of declaring attributes is OC specific, basically can determine that the operation of attributes is implemented in objC source code, so, for easy exploration, we directly in objC4-756.2 source code to find the code handling attributes. Open objC-756.2 and search the setProperty keyword globally:

Tracing its implementation, we see that all methods that assign values to attributes end up in the reallySetProperty() function:

Create a new Person class in the project. For research purposes, Person has only one name attribute. Note that it is atomic:

// Person.h
@interface Person : NSObject

@property (atomic, copy) NSString *name;

@end
Copy the code

We then instantiate a Person in the main() function and assign the name attribute:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        p.name = @"Objc";
    }
    return 0;
}
Copy the code

Then set a breakpoint on the reallySetProperty() entry, run the program, and verify that setting the property leads to this function:

As you can see, the reallySetProperty() function is indeed called from -[Person setName:].

2. Source code analysis

2.1 the assignment

ReallySetProperty ()

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { if (offset == 0) { object_setClass(self, newValue); return; } id oldValue; id *slot = (id*) ((char*)self + offset); if (copy) { newValue = [newValue copyWithZone:nil]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:nil]; } else { if (*slot == newValue) return; newValue = objc_retain(newValue); } if (! atomic) { oldValue = *slot; *slot = newValue; } else { spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } objc_release(oldValue); }Copy the code

Parameter analysis:

  • selfB: Here it ismain()Instantiated in a functionPersonobjectp
  • -cmdB: Here it issetName:
  • newValue: This is the new value to be assigned"Objc"
  • offset: Indicates the offset of the pointer. objectpPointer to is its first memberisaThe address of ourPersonThere’s only one property, and the system will rely on thatpA pointer to the +offsetTo find the corresponding member variable of the property_nameThe memory address of.isaIt’s a pointer, so hereoffset = 8
  • atomic: Is atomic, here isYES
  • copy: Indicates whether to copyYES
  • mutableCopy: Indicates whether to copy the mutable typeNO

Source code analysis:

  • ifoffset = 0, is to modifyisa, so callobject_setClass()
  • Gets the address of the attribute member based on the offsetslot
  • According to whethercopyormutableCopyTo copynewValueorobjc_retain(newValue)
  • recordoldValueRelease the old value after assigning the new valueobjc_release(oldValue)
  • If you areatomicIs directly assigned to the attribute
  • If it isatomic, first lock, in the assignment, and then unlock

The spinlock_t used for locking was mentioned in the previous article and will not be described in this article. As you can see, the so-called atomic modified property is only assigned with a lock.

2.2 the values

The value is called objc_getProperty() :

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { if (offset == 0) { return object_getClass(self);  } // Retain release world id *slot = (id*) ((char*)self + offset); if (! atomic) return *slot; // Atomic retain release world spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); id value = objc_retain(*slot); slotlock.unlock(); // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock. return objc_autoreleaseReturnValue(value); }Copy the code
  • If it is notatomicReturns the value directly from the pointer and offset
  • If it isatomicIs still “lock fetch”, and carried outobjc_retain()Operation, returns an automatic release objectobjc_autoreleaseReturnValue()

3. Precautions

Atomic modifiers add locks only inside setters and getters to ensure thread-safety for values/assignments, but do not guarantee thread-safety if used as follows:

@interface RootViewController () @property (atomic) NSInteger number; @end @implementation RootViewController - (void)viewDidLoad { [super viewDidLoad]; // thread 1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{for (int I = 0; i < 1000; i++) { self.number = self.number + 1; NSLog(@"number: %ld", self.number); }}); Thread 2 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{for (int I = 0; i < 1000; i++) { self.number = self.number + 1; NSLog(@"number: %ld", self.number); }}); } @endCopy the code

The attribute is atomic and should be thread-safe. Two threads each loop through number 1000 times +1. The final numer value should be 2000, but the output value is not the expected 2000.

2020-03-07 22:37:21.713683+0800 TestObjC[23813:2171198] number: 1986 2020-03-07 22:37:21.714004+0800 TestObjC[23813:2171198] number: 1987 2020-03-07 22:37:21.714267+0800 TestObjC[23813:2171198] number: 1988 2020-03-07 22:37:21.714541+0800 TestObjC[23813:2171198] number: 1989 2020-03-07 22:37:21.714844+0800 TestObjC[23813:2171198] number: 1990 2020-03-07 22:37:21.715027+0800 TestObjC[23813:2171198] number: 1991 2020-03-07 22:37:21.715442+0800 TestObjC[23813:2171198] number: 1992Copy the code

These are the last few lines of output, and the final value is only added up to 1992. This is because two threads are calling the setter and getter concurrently, and they lock the inside of the setter and getter, but they don’t lock the inside of the getter when they do the +1 operation, so at some point, thread one calls the getter and gets the value, thread two just calls the getter and gets the same value. Then the two threads call the setter so that the setters are assigned the same value.

  • So usingatomicWhether or not operations on attributes are thread-safe when modifying them is not guaranteed by locking them internally.