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