1. Lead

This is the source code of the atomic, the source code is located here

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

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

A cursory look at it shows that a lock called SpinLock was added to the getter and setter to keep them thread safe

2. So what is SpinLock?

Learn about SpinLock***

When one thread acquires a lock, the other threads loop around to see if the lock has been released. Therefore, this lock is more suitable for the lock holder to save a short time.

In OC it is OSSpinLock, and the OS prefix indicates that it is an API for MacOS

The question is, why does Apple use spin locks and not other locks? Assignment operation is a very fast operation, if the use of mutex, let wait outside the thread sleep, and then wake up, is a painful thing ah, not easy to fall asleep, sleep a few milliseconds have to get up, there will be wake up gas! To get up is to consume CPU resources

3. Are atomic safe?

This is a classic interview question! -_ –

Look at the following code

@property (atomic, assign) NSInteger plus; // There is an atomic IntCopy the code

There is an increment method that loops 10 times. To simulate a thread grab scenario, I let the current thread sleep for 1 second before executing the increment and then print the current thread and the increment value

- (void)plusMethod
{
    for (NSInteger i = 0; i < 10; i++) {
        sleep(1);
        
        self.plus++;
        NSLog(@"%@ plus:%ld", NSThread.currentThread, self.plus); }}Copy the code

Finally, two threads are created to manipulate the increment method

- (void)gcd { queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ [self plusMethod]; }); dispatch_async(queue, ^{ [self plusMethod]; }); } think of the printed result, is it 0,1,2... 19Copy the code

Printed result

<NSThread: 0x604000276080>{number = 3, name = (null)} plus:2
<NSThread: 0x60000047cc40>{number = 4, name = (null)} plus:2
<NSThread: 0x60000047cc40>{number = 4, name = (null)} plus:4
<NSThread: 0x604000276080>{number = 3, name = (null)} plus:3
<NSThread: 0x60000047cc40>{number = 4, name = (null)} plus:5
<NSThread: 0x604000276080>{number = 3, name = (null)} plus:6
<NSThread: 0x604000276080>{number = 3, name = (null)} plus:8
<NSThread: 0x60000047cc40>{number = 4, name = (null)} plus:8
<NSThread: 0x604000276080>{number = 3, name = (null)} plus:9
<NSThread: 0x60000047cc40>{number = 4, name = (null)} plus:10
<NSThread: 0x60000047cc40>{number = 4, name = (null)} plus:11
<NSThread: 0x604000276080>{number = 3, name = (null)} plus:12
<NSThread: 0x604000276080>{number = 3, name = (null)} plus:13
<NSThread: 0x60000047cc40>{number = 4, name = (null)} plus:14
<NSThread: 0x60000047cc40>{number = 4, name = (null)} plus:15
<NSThread: 0x604000276080>{number = 3, name = (null)} plus:15
<NSThread: 0x60000047cc40>{number = 4, name = (null)} plus:16
<NSThread: 0x604000276080>{number = 3, name = (null)} plus:17
<NSThread: 0x604000276080>{number = 3, name = (null)} plus:18
<NSThread: 0x60000047cc40>{number = 4, name = (null)} plus:18
Copy the code

It did print 20 times, but not the 1 to 20 we expected, which meant that this code was thread-safe. So I used to think that being atomic is not thread-safe, so I recommend managing thread-safety yourself, using nonatomic, but is that really the case? To modify the increment method: Add a lock, I also added a spin lock, in this case ***, regardless of performance, what lock will have the same effect

- (void)plusMethod
{
    for(NSInteger i = 0; i < 10; i++) { sleep(1); OSSpinLockLock(&_spinLock); // <---- self.plus++; NSLog(@"%@ plus:%ld", NSThread.currentThread, self.plus); OSSpinLockUnlock(&_spinLock); / / < -- -- -- -- --}}Copy the code

The printed result is not pasted, the result is what we want, 1~20, thread safe! Analyze:

OSSpinLockLock(&_spinLock); // <---- self.plus++; OSSpinLockUnlock(&_spinLock); // <-----Copy the code

This code simply adds a lock to the outer layer of self.plus. This is a repeat operation, regardless of performance

        NSLog(@"%@ plus:%ld", NSThread.currentThread, self.plus);
Copy the code

Let’s exaggerate each instruction A little bit. Let’s think of this as A pleasant trip to try the toilet. There are two people A and B who have bad stomachs because of eating. The operation of NSLog is like flushing water, which takes 3 seconds. It comes in A hurry and goes in A rush. At the beginning of the procedure, A’s stomach turbine starts to rotate and needs to go to the toilet. At this time B turbine also turned up and wanted to take A large ah, A person, he was very anxious ah, he lingered in the door, waiting for ah A to come out! A was very bored in there. No one told it that it must be finished before it could flush, so it pressed the toilet and the toilet began to flush and explode. A was very comfortable and gave out A hearty laugh. B is very uncomfortable, why not out, anxious to death, it saw the door there is a flush button, so it also went to press a flush; 5 seconds passed. A came out and opened the door. B went in, locked the door, began to unload, also pressed the door to flush; As soon as A came out, he found his stomach hurt again, so he was anxious to wait outside. He also pressed the flush button again and again.

Er… So in order to prevent the flushing from being damaged, we had to design the flushing button inside the door,

- (void)plusMethod
{
    for(NSInteger i = 0; i < 10; i++) { sleep(1); OSSpinLockLock(&_spinLock); // <---- self.plus++; NSLog(@"%@ plus:%ld", NSThread.currentThread, self.plus); OSSpinLockUnlock(&_spinLock); / / < -- -- -- -- --}}Copy the code

This is why saying that atomic only guarantees read and write safety does not guarantee real thread safety. In the project, there is a lot of logic that needs to be synchronized before plus++ is added, which makes thread safety unsafe! In fact, atomic can guarantee the security of data!

4. Are you sure the atomic is safe?

In fact, atomic data is safe! , ask again now, sprinkle! There’s nothing wrong with Apple’s code, but what if there’s something wrong with the lock? YY da posted a blog post in 2016, which YY da blog did not want to click on. I have captured the key content as follows

High-priority threads always execute before low-priority threads, and a thread is not disturbed by threads of lower priority than itself. This thread scheduling algorithm breaks the Spin lock by creating potential priority inversion problems. Specifically, if a low-priority thread acquires a lock and accesses a shared resource, and a high-priority thread attempts to acquire the lock, it will be in a busy state of spin lock and thus consume a lot of CPU. The low-priority thread cannot compete with the high-priority thread for CPU time, resulting in the task being delayed and unable to release the lock. This isn’t just a theoretical problem, libobjc has encountered this problem so many times that apple engineers disabled OSSpinLock. The bottom line is that all types of spin locks in iOS will no longer be available unless the developer can ensure that all threads accessing the lock are of the same priority.

*** OS_UNfair_lock ***

'OSSpinLock' is deprecated: first deprecated inIOS 10.0 - Use OS_unfair_lock () from < OS /lock.hCopy the code

You need to import this library if you really want to use it

#import <libkern/OSAtomic.h>
Copy the code

5.os_unfair_lock

This library needs to be introduced

#import <os/lock.h>
Copy the code

Use the API

os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT); Os_unfair_lock_lock (unfairLock); / / lock os_unfair_lock_unlock (unfairLock); BOOL B = OS_UNFAIR_LOCK_tryLOCK (unfairLock); // Try lockingCopy the code

6. Digression

The Rx framework in Swift also has SpinLock, but it is a macro, a recursive lock