Multithreading security risks

A resource may be shared by multiple threads, that is, multiple threads may access the same resource. When multiple threads access the same resource, data corruption and data security issues can easily occur

Problem case

The two cases of selling tickets and saving money to withdraw money, see the code below

@interface BaseDemo: NSObject - (void)moneyTest; - (void)ticketTest; #pragma mark - Exposed to subclasses to use - (void)__saveMoney; - (void)__drawMoney; - (void)__saleTicket; @end @interface BaseDemo() @property (assign, nonatomic) int money; @property (assign, nonatomic) int ticketsCount; */ - (void)moneyTest {self. Money = 100; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __saveMoney]; }}); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __drawMoney]; }}); } /** save money */ - (void)__saveMoney {int oldMoney = self. Money; sleep(.2); oldMoney += 50; self.money = oldMoney; NSLog(@" save 50, remaining % ddollar - %@", oldMoney, [NSThread currentThread]); } /** get money */ - (void)__drawMoney {int oldMoney = self.money; sleep(.2); oldMoney -= 20; self.money = oldMoney; NSLog(@" take 20, remaining %d - %@", oldMoney, [NSThread currentThread]); } /** sell 1 ticket */ - (void)__saleTicket {int oldTicketsCount = self.ticketscount; sleep(.2); oldTicketsCount--; self.ticketsCount = oldTicketsCount; NSLog(@" %d tickets left - %@", oldTicketsCount, [NSThread currentThread]); */ - (void)ticketTest {self. TicketsCount = 15; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self __saleTicket]; }}); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self __saleTicket]; }}); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self __saleTicket]; }}); } @end @interface ViewController () @property (strong, nonatomic) BaseDemo *demo; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; BaseDemo *demo = [[BaseDemo alloc] init]; [demo ticketTest]; [demo moneyTest]; } @endCopy the code

The solution is to use thread synchronization techniques (synchronization, which means coordinated pacing, in a predetermined order, and a common solution is locking

Thread synchronization scheme

Thread synchronization schemes in iOS include the following

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • @synchronized

OSSpinLock

OSSpinLock is called a “spin lock”, and the thread waiting for the lock will be in busy-wait state, occupying CPU resources

Use OSSpinLock to solve the above example problem

@interface OSSpinLockDemo: BaseDemo @end #import "OSSpinLockDemo.h" #import <libkern/OSAtomic.h> @interface OSSpinLockDemo() @property (assign, nonatomic) OSSpinLock moneyLock; // @property (assign, nonatomic) OSSpinLock ticketLock; @end @implementation OSSpinLockDemo - (instancetype)init { if (self = [super init]) { self.moneyLock = OS_SPINLOCK_INIT;  // self.ticketLock = OS_SPINLOCK_INIT; } return self; } - (void)__drawMoney { OSSpinLockLock(&_moneyLock); [super __drawMoney]; OSSpinLockUnlock(&_moneyLock); } - (void)__saveMoney { OSSpinLockLock(&_moneyLock); [super __saveMoney]; OSSpinLockUnlock(&_moneyLock); } - (void)__saleTicket {// Static OSSpinLock ticketLock = OS_SPINLOCK_INIT; OSSpinLockLock(&ticketLock); [super __saleTicket]; OSSpinLockUnlock(&ticketLock); } @endCopy the code
The problem of the static

TicketLock above can also be used with the static modifier as an internal static variable

#define	OS_SPINLOCK_INIT    0
Copy the code

OS_SPINLOCK_INIT = 0; Static can only assign a definite value at compile time. It cannot assign a function value dynamically

Static OSSpinLock ticketLock = [NSString stringWithFormat:@"haha"]; static OSSpinLock ticketLock = [NSString stringWithFormat:@"haha"];Copy the code
The problem of OSSpinLock

OSSpinLock is now no longer secure and may have priority inversion issues

Since the nature of multithreading is scheduling back and forth between different threads, each thread may have different priorities for allocated resources. If the lower-priority thread locks first and is ready to execute code, the higher-priority thread loops outside waiting to lock. However, because of its high priority, the CPU may assign a large number of tasks to it, so that it cannot handle the lower priority thread; Threads with lower priority can’t continue to execute code, so they can’t unlock it, so they wait for each other again, causing deadlocks. That’s why Apple has now scrapped OSSpinLock

The solution

OSSpinLockTry is used to replace OSSpinLockLock. If the OSSpinLockTry is not locked, the code will be executed in the judgment and locked, avoiding the problem of waiting in a loop because of the lock

(void)__saleTicket {if (OSSpinLockTry(&_ticketlock)) {[super __saleTicket]; (void)__saleTicket {if (OSSpinLockTry(&_ticketlock)) { OSSpinLockUnlock(&_ticketLock); }}Copy the code
Analysis by assembly

We use breakpoints to analyze what happens when we lock

We put a breakpoint on the lock code for selling tickets and call the analysis step by step through the transfer assembly

OSSpinLockLock is called after assembly

_OSSpinLockLockSlow is called internally

At the core, _OSSpinLockLockSlow will compare, and then at the breakpoint it will jump back again to 0x7FFF5E73326F to execute the code again

So by compiling the underlying execution logic, we can see that OSSpinLock is constantly looping to call the judgment, and only executes the code after it is unlocked

The level of the lock

OSSpinLock spinlock is a high-level lock because it is called in a loop

os_unfair_lock

Os_unfair_lock is now used by apple to replace insecure OSSpinLock, only supported starting with iOS10

From underlying calls, threads waiting for os_UNFAIR_LOCK are dormant, not busy, etc

The modified sample code is as follows

#import "BaseDemo.h"

@interface OSUnfairLockDemo: BaseDemo

@end

#import "OSUnfairLockDemo.h"
#import <os/lock.h>

@interface OSUnfairLockDemo()

@property (assign, nonatomic) os_unfair_lock moneyLock;
@property (assign, nonatomic) os_unfair_lock ticketLock;
@end

@implementation OSUnfairLockDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.moneyLock = OS_UNFAIR_LOCK_INIT;
        self.ticketLock = OS_UNFAIR_LOCK_INIT;
    }
    return self;
}

- (void)__saleTicket
{
    os_unfair_lock_lock(&_ticketLock);
    
    [super __saleTicket];
    
    os_unfair_lock_unlock(&_ticketLock);
}

- (void)__saveMoney
{
    os_unfair_lock_lock(&_moneyLock);
    
    [super __saveMoney];
    
    os_unfair_lock_unlock(&_moneyLock);
}

- (void)__drawMoney
{
    os_unfair_lock_lock(&_moneyLock);
    
    [super __drawMoney];
    
    os_unfair_lock_unlock(&_moneyLock);
}

@end
Copy the code

If you do not write OS_UNFAIR_LOCK_UNLOCK, all threads in os_UNFAIR_LOCK_LOCK go to sleep and do not execute code. This is called a deadlock

Analysis by assembly

We also use breakpoints to analyze what happens after we lock

Os_unfair_lock_lock is first called

Os_unfair_lock_lock_slow is then called

__ulock_wait is then executed in os_UNFAIR_LOCK_LOCK_SLOW

At the core, code execution on Syscall will jump out of the breakpoint and stop executing the code, that is, go to sleep

Os_unfair_lock is a mutex lock that is set to sleep until it is unlocked

Syscall calls can be understood as system-level calls that go to sleep and directly freeze the thread and stop executing code

The level of the lock

Os_unfair_lock is a low-level lock that automatically goes to sleep when a lock is detected

pthread_mutex

The mutex

Mutex is called a mutex, and the thread waiting for the lock will sleep

Use the following code

@interface MutexDemo: BaseDemo @end #import "MutexDemo.h" #import <pthread.h> @interface MutexDemo() @property (assign, nonatomic) pthread_mutex_t ticketMutex; @property (assign, nonatomic) pthread_mutex_t moneyMutex; @end@implementation MutexDemo - (void)__initMutex:(pthread_mutex_t *)mutex pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // // Initializes attributes // pthread_mutexattr_t attr; // pthread_mutexattr_init(&attr); // pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT); // // initialize lock // pthread_mutex_init(mutex, &attr); PTHREAD_MUTEX_DEFAULT pthread_mutex_init(mutex, NULL); } - (instancetype)init { if (self = [super init]) { [self __initMutex:&_ticketMutex]; [self __initMutex:&_moneyMutex]; } return self; } - (void)__saleTicket { pthread_mutex_lock(&_ticketMutex); [super __saleTicket]; pthread_mutex_unlock(&_ticketMutex); } - (void)__saveMoney { pthread_mutex_lock(&_moneyMutex); [super __saveMoney]; pthread_mutex_unlock(&_moneyMutex); } - (void)__drawMoney { pthread_mutex_lock(&_moneyMutex); [super __drawMoney]; pthread_mutex_unlock(&_moneyMutex); } - (void)dealloc {// Pthread_mutex_destroy (&_moneymutex); pthread_mutex_destroy(&_ticketMutex); }Copy the code

Pthread_mutex_t is actually of type pthread_mutex *

Recursive locking

When the property is set to PTHREAD_MUTEX_RECURSIVE, it can be used as a recursive lock

Recursive locking allows the same thread to lock the same lock repeatedly. Multiple threads may not use recursive locking

- (void)__initMutex:(pthread_mutex_t *)mutex {// initialize attribute pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // Initialize lock pthread_mutex_init(mutex, &attr); } - (void)otherTest { pthread_mutex_lock(&_mutex); NSLog(@"%s", __func__); static int count = 0; if (count < 10) { count++; [self otherTest]; } pthread_mutex_unlock(&_mutex); }Copy the code
Lock according to conditions

We can set certain conditions to select calls between threads for locking to unlock, as shown in the following example

@interface MutexDemo() @property (assign, nonatomic) pthread_mutex_t mutex; @property (assign, nonatomic) pthread_cond_t cond; @property (strong, nonatomic) NSMutableArray *data; @end@implementation MutexDemo - (instanceType)init {if (self = [super init]) {pthread_mutex_init(&_mutex, NULL); Pthread_cond_init (&_cond, NULL); self.data = [NSMutableArray array]; } return self; } - (void)otherTest { [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start]; } // thread 1 // remove elements from array - (void)__remove {pthread_mutex_lock(&_mutex); NSLog(@"__remove - begin"); If (self.data.count == 0) {pthread_cond_wait(&_cond, &_mutex); if (self.data.count == 0) {pthread_cond_wait(&_cond, &_mutex); } [self.data removeLastObject]; NSLog(@" delete element "); pthread_mutex_unlock(&_mutex); } // thread 2 // add elements to array - (void)__add {pthread_mutex_lock(&_mutex); sleep(1); [self.data addObject:@"Test"]; NSLog(@" added element "); Pthread_cond_signal (&_cond); pthread_cond_signal(&_cond); pthread_cond_signal(&_cond); Pthread_cond_broadcast (&_cond); pthread_mutex_unlock(&_mutex); } - (void)dealloc { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); } @endCopy the code
Analysis by assembly

We use breakpoints to analyze what happens when we lock

Pthread_mutex_lock is executed first

Pthread_mutex_firstfit_lock_slow is then executed

Pthread_mutex_firstfit_lock_wait is then executed

__psynch_mutexwait is then executed

At the heart of __psynch_mutexwait, syscall is a breakpoint from which the code goes to sleep

Pthread_mutex and OS_UNfair_LOCK go to sleep after locking

The level of the lock

Os_unfair_lock (pthread_mutex) specifies a low-level lock.

NSLock

NSLock is a wrapper around a mutex normal lock

NSLock complies with the

protocol and supports the following two methods

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end
Copy the code

Other Common methods

// Try to unlock - (BOOL)tryLock; // set a time to wait for lock, return NO - (BOOL)lockBeforeDate:(NSDate *)limit;Copy the code

See the code below for details

@interface NSLockDemo: BaseDemo

@end

@interface NSLockDemo()
@property (strong, nonatomic) NSLock *ticketLock;
@property (strong, nonatomic) NSLock *moneyLock;
@end

@implementation NSLockDemo


- (instancetype)init
{
    if (self = [super init]) {
        self.ticketLock = [[NSLock alloc] init];
        self.moneyLock = [[NSLock alloc] init];
    }
    return self;
}

- (void)__saleTicket
{
    [self.ticketLock lock];
    
    [super __saleTicket];
    
    [self.ticketLock unlock];
}

- (void)__saveMoney
{
    [self.moneyLock lock];
    
    [super __saveMoney];
    
    [self.moneyLock unlock];
}

- (void)__drawMoney
{
    [self.moneyLock lock];
    
    [super __drawMoney];
    
    [self.moneyLock unlock];
}

@end
Copy the code
Analyzing the underlying implementation

Since NSLock is not open source, we can analyze the implementation through the GNUstep Base

If you look at nsLock. m, you can see that the initialize method creates a pthread_mutex_t object, so you can determine that NSLock is an object-oriented encapsulation of pthread_mutex

@implementation NSLock + (id) allocWithZone: (NSZone*)z { if (self == baseLockClass && YES == traceLocks) { return class_createInstance(tracedLockClass, 0); } return class_createInstance(self, 0); } + (void) initialize { static BOOL beenHere = NO; if (beenHere == NO) { beenHere = YES; /* Initialise attributes for the different types of mutex. * We do it once, since attributes can be shared between multiple * mutexes. * If we had a pthread_mutexattr_t instance for each mutex, we would * either have to store it as an ivar of our NSLock (or similar), or * we would potentially leak instances as we couldn't destroy them * when destroying the NSLock. I don't know if any implementation * of pthreads actually allocates memory when you call the * pthread_mutexattr_init function, but they are allowed to do so * (and deallocate the memory in pthread_mutexattr_destroy). */ pthread_mutexattr_init(&attr_normal); pthread_mutexattr_settype(&attr_normal, PTHREAD_MUTEX_NORMAL); pthread_mutexattr_init(&attr_reporting); pthread_mutexattr_settype(&attr_reporting, PTHREAD_MUTEX_ERRORCHECK); pthread_mutexattr_init(&attr_recursive); pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE); /* To emulate OSX behavior, we need to be able both to detect deadlocks * (so we can log them), and also hang the thread when one occurs. * the simple way to do that is to set up a locked mutex we can * force a deadlock on. */ pthread_mutex_init(&deadlock, &attr_normal); pthread_mutex_lock(&deadlock); baseConditionClass = [NSCondition class]; baseConditionLockClass = [NSConditionLock class]; baseLockClass = [NSLock class]; baseRecursiveLockClass = [NSRecursiveLock class]; tracedConditionClass = [GSTracedCondition class]; tracedConditionLockClass = [GSTracedConditionLock class]; tracedLockClass = [GSTracedLock class]; tracedRecursiveLockClass = [GSTracedRecursiveLock class]; untracedConditionClass = [GSUntracedCondition class]; untracedConditionLockClass = [GSUntracedConditionLock class]; untracedLockClass = [GSUntracedLock class]; untracedRecursiveLockClass = [GSUntracedRecursiveLock class]; }}Copy the code

NSRecursiveLock

NSRecursiveLock encapsulates a mutex recursive lock, and has the same API as NSLock

NSCondition

NSCondition is the encapsulation of mutex and COND

The specific usage code is as follows

@interface NSConditionDemo() @property (strong, nonatomic) NSCondition *condition; @property (strong, nonatomic) NSMutableArray *data; @end @implementation NSConditionDemo - (instancetype)init { if (self = [super init]) { self.condition = [[NSCondition alloc] init]; self.data = [NSMutableArray array]; } return self; } - (void)otherTest { [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start]; } // thread 1 // Delete elements from array - (void)__remove {[self.condition lock]; NSLog(@"__remove - begin"); If (self.data.count == 0) {// wait [self.condition wait]; } [self.data removeLastObject]; NSLog(@" delete element "); [self.condition unlock]; } // thread 2 // Add elements to array - (void)__add {[self.condition lock]; sleep(1); [self.data addObject:@"Test"]; NSLog(@" added element "); [self. Condition signal]; // [self.condition broadcast]; [self.condition unlock]; } @endCopy the code
Analyzing the underlying implementation

NSCondition also abides by the NSLocking protocol, indicating that it has encapsulated locking codes internally

@interface NSCondition : NSObject <NSLocking> { @private void *_priv; } - (void)wait; - (BOOL)waitUntilDate:(NSDate *)limit; - (void)signal; - (void)broadcast; @property (nullable, copy) NSString *name API_AVAILABLE(MacOS (10.5), ios(2.0), Watchos (2.0), TVOs (9.0)); @endCopy the code

GNUstep Base also wraps pthread_mutex_t in its initialization method

@implementation NSCondition + (id) allocWithZone: (NSZone*)z { if (self == baseConditionClass && YES == traceLocks) { return class_createInstance(tracedConditionClass, 0); } return class_createInstance(self, 0); } + (void) initialize { [NSLock class]; // Ensure mutex attributes are set up. } - (id) init { if (nil ! = (self = [super init])) { if (0 ! = pthread_cond_init(&_condition, NULL)) { DESTROY(self); } else if (0 ! = pthread_mutex_init(&_mutex, &attr_reporting)) { pthread_cond_destroy(&_condition); DESTROY(self); } } return self; }Copy the code

NSConditionLock

NSConditionLock is a further encapsulation of NSCondition, and you can set specific conditional values

The order of execution can be controlled depending on threads by setting conditional values, as shown in the sample code

@interface NSConditionLockDemo() @property (strong, nonatomic) NSConditionLock *conditionLock; @end@implementation NSConditionLockDemo - (instanceType)init { If (self = [super init]) {self. ConditionLock = [[NSConditionLock alloc] initWithCondition:1]; } return self; } - (void)otherTest { [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start]; } - (void) __one {/ / don't need any conditions, only no lock, lock [self. ConditionLock lock]; NSLog(@"__one"); sleep(1); [self.conditionLock unlockWithCondition:2]; } - (void) __two {/ / lock according to the corresponding condition [self. ConditionLock lockWhenCondition: 2]; NSLog(@"__two"); sleep(1); [self.conditionLock unlockWithCondition:3]; } - (void)__three { [self.conditionLock lockWhenCondition:3]; NSLog(@"__three"); [self.conditionLock unlock]; } @end // Print the sequence: 1, 2, 3Copy the code

dispatch_queue_t

We can directly use the serial queue of GCD, also can achieve thread synchronization, refer to the GCD section of the code example

dispatch_semaphore

A semaphore is called a semaphore.

The initial value of a semaphore that can be used to control the maximum number of concurrent accesses by a thread

Sample code is as follows

@interface SemaphoreDemo() @property (strong, nonatomic) dispatch_semaphore_t semaphore; @property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore; @property (strong, nonatomic) dispatch_semaphore_t moneySemaphore; @end @implementation SemaphoreDemo - (instanceType)init {if (self = [super init]) { Self. semaphore = dispatch_semaphore_create(5); self.semaphore = dispatch_semaphore_create(5); Self. ticketSemaphore = dispatch_semaphore_create(1); self.ticketSemaphore = dispatch_semaphore_create(1); self.moneySemaphore = dispatch_semaphore_create(1); } return self; } - (void)__drawMoney { dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); [super __drawMoney]; dispatch_semaphore_signal(self.moneySemaphore); } - (void)__saveMoney { dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); [super __saveMoney]; dispatch_semaphore_signal(self.moneySemaphore); } - (void)__saleTicket {// If the semaphore value is >0, then decrement by 1. Dispatch_semaphore_wait (self.ticketSemaphore, DISPATCH_TIME_FOREVER); [super __saleTicket]; // add 1 dispatch_semaphore_signal(self.ticketSemaphore); } @endCopy the code

@synchronized

@synchronized is the encapsulation of a mutex recursive lock

Sample code is as follows

@interface SynchronizedDemo: basedemo@end@implementation SynchronizedDemo - (void)__drawMoney {// @synchronized([self]) class]) { [super __drawMoney]; } } - (void)__saveMoney { @synchronized([self class]) { // objc_sync_enter [super __saveMoney]; } // objc_sync_exit } - (void)__saleTicket { static NSObject *lock; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ lock = [[NSObject alloc] init]; }); @synchronized(lock) { [super __saleTicket]; } } @endCopy the code
Source code analysis

As we can see from the program running the assembly, objc_sync_Enter is eventually called

We can analyze the corresponding source implementation of objC-sync.mm in objC4

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}
Copy the code

You can see that SyncData is found based on the obj passed in

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex; 
} SyncData;
Copy the code

Find the true type of the SyncData member variable recursive_mutex_t, which contains a recursive lock

using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>; class recursive_mutex_tt : nocopy_t { os_unfair_recursive_lock mLock; Public: constexpr recursive_mutex_tt() : mLock(OS_UNFAIR_RECURSIVE_LOCK_INIT) { lockdebug_remember_recursive_mutex(this); } constexpr recursive_mutex_tt(__unused const fork_unsafe_lock_t unsafe) : mLock(OS_UNFAIR_RECURSIVE_LOCK_INIT) { } void lock() { lockdebug_recursive_mutex_lock(this); os_unfair_recursive_lock_lock(&mLock); } void unlock() { lockdebug_recursive_mutex_unlock(this); os_unfair_recursive_lock_unlock(&mLock); } void forceReset() { lockdebug_recursive_mutex_unlock(this); bzero(&mLock, sizeof(mLock)); mLock = os_unfair_recursive_lock OS_UNFAIR_RECURSIVE_LOCK_INIT; } bool tryLock() { if (os_unfair_recursive_lock_trylock(&mLock)) { lockdebug_recursive_mutex_lock(this); return true; } return false; } bool tryUnlock() { if (os_unfair_recursive_lock_tryunlock4objc(&mLock)) { lockdebug_recursive_mutex_unlock(this); return true; } return false; } void assertLocked() { lockdebug_recursive_mutex_assert_locked(this); } void assertUnlocked() { lockdebug_recursive_mutex_assert_unlocked(this); }};Copy the code

Then we analyze the implementation method id2DATA to fetch SyncData from LIST_FOR_OBJ through obj

static SyncData* id2data(id object, enum usage why) { spinlock_t *lockp = &LOCK_FOR_OBJ(object); SyncData **listp = &LIST_FOR_OBJ(object); SyncData* result = NULL; #if SUPPORT_DIRECT_THREAD_KEYS // Check per-thread single-entry fast cache for matching object bool fastCacheOccupied = NO; SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY); if (data) { fastCacheOccupied = YES; if (data->object == object) { // Found a match in fast cache. uintptr_t lockCount; result = data; lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY); if (result->threadCount <= 0 || lockCount <= 0) { _objc_fatal("id2data fastcache is buggy"); } switch(why) { case ACQUIRE: { lockCount++; tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount); break; } case RELEASE: lockCount--; tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount); if (lockCount == 0) { // remove from fast cache tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL); // atomic because may collide with concurrent ACQUIRE OSAtomicDecrement32Barrier(&result->threadCount); } break; case CHECK: // do nothing break; } return result; } } #endif // Check per-thread cache of already-owned locks for matching object SyncCache *cache = fetch_cache(NO); if (cache) { unsigned int i; for (i = 0; i < cache->used; i++) { SyncCacheItem *item = &cache->list[i]; if (item->data->object ! = object) continue; // Found a match. result = item->data; if (result->threadCount <= 0 || item->lockCount <= 0) { _objc_fatal("id2data cache is buggy"); } switch(why) { case ACQUIRE: item->lockCount++; break; case RELEASE: item->lockCount--; if (item->lockCount == 0) { // remove from per-thread cache cache->list[i] = cache->list[--cache->used]; // atomic because may collide with concurrent ACQUIRE OSAtomicDecrement32Barrier(&result->threadCount); } break; case CHECK: // do nothing break; } return result; } } // Thread cache didn't find anything. // Walk in-use list looking for matching object // Spinlock prevents multiple threads from creating multiple // locks for the same new object. // We could keep the nodes in some hash table if we find that there are // more than 20 or so distinct locks active, but we don't do that now. lockp->lock(); { SyncData* p; SyncData* firstUnused = NULL; for (p = *listp; p ! = NULL; p = p->nextData) { if ( p->object == object ) { result = p; // atomic because may collide with concurrent RELEASE OSAtomicIncrement32Barrier(&result->threadCount); goto done; } if ( (firstUnused == NULL) && (p->threadCount == 0) ) firstUnused = p; } // no SyncData currently associated with object if ( (why == RELEASE) || (why == CHECK) ) goto done; // an unused one was found, use it if ( firstUnused ! = NULL ) { result = firstUnused; result->object = (objc_object *)object; result->threadCount = 1; goto done; } } // Allocate a new SyncData and add to list. // XXX allocating memory with a global lock held is bad practice, // might be worth releasing the lock, allocating, and searching again. // But since we never free these guys we won't be stuck in allocation very often. posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData)); result->object = (objc_object *)object; result->threadCount = 1; new (&result->mutex) recursive_mutex_t(fork_unsafe_lock); result->nextData = *listp; *listp = result; done: lockp->unlock(); if (result) { // Only new ACQUIRE should get here. // All RELEASE and CHECK and recursive ACQUIRE are // handled by the per-thread caches above. if (why == RELEASE) { // Probably some thread is incorrectly exiting // while the object is held by another thread. return nil; } if (why ! = ACQUIRE) _objc_fatal("id2data is buggy"); if (result->object ! = object) _objc_fatal("id2data is buggy"); #if SUPPORT_DIRECT_THREAD_KEYS if (! fastCacheOccupied) { // Save in fast thread cache tls_set_direct(SYNC_DATA_DIRECT_KEY, result); tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1); } else #endif { // Save in thread cache if (! cache) cache = fetch_cache(YES); cache->list[cache->used].data = result; cache->list[cache->used].lockCount = 1; cache->used++; } } return result; }Copy the code

LIST_FOR_OBJ = LIST_FOR_OBJ; LIST_FOR_OBJ = LIST_FOR_OBJ

#define LIST_FOR_OBJ(obj) sDataLists[obj]. Datalists [obj]. // Hash table implementation is to pass the object as the key, and the corresponding lock valueCopy the code

It can also be seen from the source code analysis that @synchronized internal locks are recursive locks

The comparison of the lock

Performance comparison sort

Below is a list of each lock in order of performance

  • os_unfair_lock
  • OSSpinLock
  • dispatch_semaphore
  • pthread_mutex
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSCondition
  • pthread_mutex(recursive)
  • NSRecursiveLock
  • NSConditionLock
  • @synchronized

The most selective lock

  • dispatch_semaphore
  • pthread_mutex

Mutex, spin lock comparison

When to use spinlocks
  • Threads are expected to wait a short time for locks
  • Locking code (critical sections) is often invoked, but contention rarely occurs
  • CPU resources are not tight
  • Multicore processor
When is a mutex used
  • The thread is expected to wait a long time for the lock
  • Single-core processor (minimize CPU consumption)
  • IO operations exist in critical sections (IO operations occupy CPU resources)
  • Critical section code complex or large loop
  • Critical sections are very competitive