IOS underlying principles + reverse article summary

This article mainly introduces common locks and the underlying analysis of synchronized, NSLock, recursive lock, and conditional lock

The lock

Refer to a lock performance data comparison chart, as shown belowIt can be seen that the lock performance in the figure from the highest to the lowest is:OSSpinLock -> dispatch_semaphone -> pthread_mutex -> NSLock -> NSCondition -> OSSpinLock -> dispatch_semaphone -> pthread_mutex -> NSLock Pthread_mutex -> NSRecursiveLock -> NSRecursiveLock -> NSConditionLock -> synchronized

The locks in the figure are roughly divided into the following categories:

  • 1. Spin-lock: In a spin-lock, the thread repeatedly checks whether a variable is available. Because threads consistently execute during this process, it is a busy wait. Once a spin lock is acquired, the thread holds the lock until it is explicitly released. Spin-locking avoids the scheduling overhead of the process context and is therefore effective in cases where threads will only block for a short time. The iOS attribute modifier atomic comes with a spinlock

    • OSSpinLock

    • atomic

  • 2. Mutex: Mutex is a mechanism used in multithreaded programming to prevent two threads from simultaneously reading and writing to the same common resource (such as global variables) by cutting the code into critical sections

    • @synchronized

    • NSLock

    • pthread_mutex

  • [3, conditional lock] : Conditional lock is a condition variable. When some resource requirements of the process are not met, it enters hibernation, that is, locked. When resources are allocated, conditional lock is opened, and the process continues to run

    • NSCondition

    • NSConditionLock

  • [4] Recursive lock: A recursive lock is a lock that can be applied N times by the same thread without raising a deadlock. A recursive lock is a special kind of mutex, that is, a mutex with recursive properties

    • pthread_mutex(recursive)

    • NSRecursiveLock

  • Semaphores: Semaphores are a more advanced synchronization mechanism. A mutex is a special case of semaphore with only 0/1 values. Semaphores have more value space and can be used for more complex synchronization than just mutual exclusion between threads

    • dispatch_semaphore
  • 6. Read-write lock: Read-write lock is actually a special type of spin lock. The access to the shared resource is divided into readers and writers. The readers can only read the shared resource, and the writers need to write the shared resource. This lock improves concurrency compared to spin locks

    • A read/write lock can have only one writer or more readers at the same time, but cannot have both readers and writers. A read/write lock is preempted during lock retention

    • If the read-write lock currently has no readers and no writers, then the writer can immediately acquire the read-write lock, otherwise it must spin there until there are no writers or readers. If the read-write lock has no writer, then the reader can set

There are three basic types of locks: spin-locks, mutex locks, read-write locks, and other locks such as conditional locks, recursive locks, and semaphores that are encapsulated and implemented at the top level.

1. OSSpinLock

Since OSSpinLock had security issues, it has been deprecated since iOS10. Spin-locks are unsafe because after the lock is acquired, the thread is kept busy waiting, causing priority inversion of the task.

The busy wait mechanism may cause that tasks of high priority are running and occupy the time slice, while tasks of low priority cannot preempt the time slice. As a result, the task cannot be completed and the lock is not released

After OSSpinLock is deprecated, the alternative is to internally encapsulate OS_UNfair_LOCK, and os_Unfair_LOCK will be hibernated when locked, rather than the busy state of spin-locks

2. Atomic lock

Atomic applies to attributes in OC and comes with a spinlock, but this is mostly nonatomic

In the previous article, we mentioned that setter methods call different methods depending on the modifier, and that the final call is the reallySetProperty method, which has both atomic and non-atomic operations

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { ... id *slot = (id*) ((char*)self + offset); . if (! OldValue = *slot; *slot = newValue; } else {// lock spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); }... }Copy the code

Osspinlock_t is used for the atomic properties. Osspinlock_t is used for the atomic properties, but OSSpinLock is not used. And in order to prevent hash collisions, we still add salt

using spinlock_t = mutex_tt<LOCKDEBUG>; class mutex_tt : nocopy_t { os_unfair_lock mLock; . }Copy the code

The handling of atomic in a getter method is pretty much the same as handling atomic in a setter

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

3. Synchronized

  • Start assembly debugging, found@synchronizedIn the process of execution, it will go to the bottomobjc_sync_enterobjc_sync_exitmethods

  • You can also go throughclangTo view the underlying compiled code

  • Based on theobjc_sync_enterMethod symbol breakpoints, look at the source library where the underlying source, through breakpoints found in objC source code, i.elibobjc.A.dylib

Objc_sync_enter & ObjC_synC_exit analysis

  • Enter theobjc_sync_enterThe source code to achieve
    • If obj exists, it passesid2dataMethod to get the correspondingSyncDataforThreadCount, lockCountforincreasingoperation
    • If obj does not exist, callobjc_sync_nilSo, by the notation breakpoint, we know that this method doesn’t do anything, it just returns, right

int objc_sync_enter(id obj) { int result = OBJC_SYNC_SUCCESS; If (obj) {// pass in nil SyncData* data = id2data(obj, ACQUIRE); / / key ASSERT (data); data->mutex.lock(); // @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
  • Enter theobjc_sync_exitThe source code to achieve
    • If obj exists, callid2dataMethod to get the corresponding SyncData, yesThreadCount, lockCountfordiminishingoperation
    • If the obj isnilDo nothing
// End synchronizing on 'obj'. End synchronization of obj. // Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR int objC_sync_exit (id obj) {int result =  OBJC_SYNC_SUCCESS; If (obj) {//obj is not nil SyncData* data = id2data(obj, RELEASE); if (! data) { result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR; } else { bool okay = data->mutex.tryUnlock(); // Unlock if (! okay) { result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR; // @synchronized(nil) does nothing} return result; // synchronized(nil) does nothing} return result; }Copy the code

Through the comparison of the above two implementation logic, it is found that they have one thing in common: when OBJ exists, SyncData is obtained through id2data method

  • Enter theSyncDataThe definition of theta is oneThe structure of the bodyIs used mainly to indicate aThread data, similar to theChain table structure, is pointed to by next and encapsulatedrecursive_mutex_tProperty can be confirmed@synchronizedIt really is aRecursive mutex
typedef struct alignas(CacheLineSize) SyncData { struct SyncData* nextData; DisguisedPtr< objC_object > object; // Similar link structure DisguisedPtr< objC_object > object; int32_t threadCount; // number of THREADS using this block recursive_mutex_t mutex; // Recursion lock} SyncData;Copy the code
  • Enter theSyncCacheIs also a structure used to store threads in whichlist[0]saidThe current thread's linked list data, mainly used for storageSyncDataandlockCount
typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;
Copy the code

Id2data analysis

  • Enter theid2dataSource code, from the above analysis, it can be seen that this method isLock and unlockBoth reuse methods
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 // TLS (Thread Local Storage, // Check per-thread single-entry fast cache for matching object bool fastCacheOccupied = NO; // 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) {// find a match in fast cache. Uintptr_t lockCount; // 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: {//objc_sync_enter; Tls_set_direct (SYNC_COUNT_DIRECT_KEY, (void*)lockCount); / / set break; } case RELEASE: //objc_sync_exit = RELEASE -- 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; // 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:// ACQUIRE item->lockCount++; break; Case RELEASE:// unlock item->lockCount--; If (item->lockCount == 0) {if (item->lockCount == 0) {// remove from per-thread 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. All caches cannot find lockp->lock(); { SyncData* p; SyncData* firstUnused = NULL; for (p = *listp; p ! = NULL; P = p->nextData) {if (p->object == object) {result = p; / / / / assignment atomic because may collide with concurrent RELEASE OSAtomicIncrement32Barrier (& result - > threadCount); ThreadCount = ++ goto done; } if ( (firstUnused == NULL) && (p->threadCount == 0) ) firstUnused = p; } / / no SyncData currently associated with the object without object associated with the current SyncData if ((according to = = RELEASE) | | (according to = = 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) {// Determine if cache on the stack is supported, // Save in fast thread cache TLS_set_direct (SYNC_DATA_DIRECT_KEY, result); // Save in fast thread cache TLS_set_direct (SYNC_DATA_DIRECT_KEY, result); tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1); //lockCount = 1} else #endif {// Save in thread cache if (! cache) cache = fetch_cache(YES); // The thread is bound to cache->list[cache]. Data = result; cache->list[cache->used].lockCount = 1; cache->used++; } } return result; }Copy the code
  • [Step 1] First look in the TLS thread cache.

    • In the TLs_get_direct method, the thread is taken as the key and the SyncData bound to it is obtained through KVC, that is, thread data. Where TLS () represents the local local thread cache,

    • Determine whether the obtained data exists, and determine whether the corresponding object can be found in the data

    • If both are found, obtain the lockCount in the TLs_get_direct method as KVC, which is used to record how many times the object has been locked (that is, how many times the lock has been nested).

    • If threadCount in data is less than or equal to 0, or lockCount is less than or equal to 0, it crashes

    • The type of operation is determined by the why passed in

      • If ACQUIRE means lock, then lockCount++ is performed and stored in the TLS cache

      • If RELEASE indicates RELEASE, then lockCount– and save to the TLS cache. If lockCount equals 0, remove thread data from TLS

      • If it’s CHECK, do nothing

  • [Step 2] If TLS does not exist, search in the cache

    • The fetch_cache method is used to check whether there are threads in the cache

    • If so, the total cache table is traversed to read and fetch the SyncCacheItem corresponding to the thread

    • The data is retrieved from the SyncCacheItem and the subsequent steps are consistent with the TLS match

  • [Step 3] If it is not in the cache, that is, the first time, create SyncData, and store in the corresponding cache

    • If a thread is found in cache and is equal to an object, proceedThe assignment, as well asthreadCount++
    • If it is not found in cache, thenthreadCountIs equal to the1

Therefore, in the ID2Data method, there are mainly three cases

  • [First time in, no lock] :
    • threadCount = 1

    • lockCount = 1

    • Stored in the TLS

  • [Not the first time in, and is the same thread]
    • If there is data in TLS, lockCount++

    • Stored in the TLS

  • [Not the first time in, and a different thread]
    • Global thread space for finding threads

    • threadCount++

    • lockCount++

    • Stored in the cache

TLS and cache table structureFor TLS and cache, the underlying table structure is as follows

  • A hash table structure in which multiple threads are assembled through the SyncList structure

  • SyncData assembles the current reentrant situation in the form of a linked list

  • The lower layer is processed by TLS thread cache and cache cache

  • There are two main things at the bottom: lockCount and threadCount, which solve recursive mutex and nested reentrant

@ synchronized pit

What’s wrong with writing the following code like this?

- (void)cjl_testSync{ _testArray = [NSMutableArray array]; for (int i = 0; i < 200000; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ @synchronized (self.testArray) { self.testArray = [NSMutableArray array]; }}); }}Copy the code

Run the result found, run on the crashThe main cause of the crash istestArrayAt some point it becomes nil, and we know from the underlying @synchronized process that if the locked object becomesnilIs not locked, equivalent to the following situation, the block inside the retain, release, in a moment before the last one is ready to release, the next is ready to release, this will lead to the creation of wild pointer

_testArray = [NSMutableArray array];
for (int i = 0; i < 200000; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        _testArray = [NSMutableArray array];
    });
}
Copy the code

Can be opened according to the above codeedit scheme -> run -> DiagnosticsThe admissionZombie ObjectsTo check whether it is a zombie object, the result is as follows

We usually use @synchronized (self), mainly because the owner of _testArray is self

Note: Wild pointer vs transition release

  • Wild pointer: Refers to the pointer produced by the transition release is still in operation

  • Transition release: Retain and release each time

conclusion

  • What @synchronized encapsulates underneath is a recursive lock, so this lock is a recursive mutex

  • @synchronized reentrant, nesting, mainly due to lockCount and threadCount

  • The reason @synchronized uses a linked list is to facilitate the insertion of the next data,

  • However, the underlying linked list query, cache lookup and recursion are very memory and performance intensive, resulting in low performance, so in the previous article, this lock is ranked last

  • However, the lock is still used frequently, mainly because it is convenient and simple, and does not need to be unlocked

  • You cannot use a non-OC object as a locking object because its object parameter is ID

  • @synchronized (self) is good for scenarios with fewer nesting times. The locked object here is not always self, so you need to be aware of that

  • If the lock is nested many times, that is, the lock self is too much, it will cause the underlying search is very troublesome, because the underlying search is linked list, so it will be relatively troublesome, so you can use NSLock, semaphore, etc

4, NSLock

NSLock is an encapsulation of the underlying pthread_mutex, as follows

 NSLock *lock = [[NSLock alloc] init];
[lock lock];
[lock unlock];
Copy the code

Enter the NSLock definition directly, which follows the NSLocking protocol, and explore the underlying implementation of NSLock

NSLock underlying analysis

  • Break points by adding signslockAnalysis, found its source inFoundationFramework,

  • As a result of the OCFoundationThe framework is not open source, so here’s the helpSwiftOpen source frameworkFoundationTo analyze the underlying implementation of NSLock, its principle and OC is roughly the same

Through the source code implementation can be seen, the bottom is throughpthread_mutexMutex implementation. And in the init method, you do something else, so you need to use init initialization when you’re using NSLock

Going back to the previous performance graph, you can see that NSLock is very close to pthread_mutex in performance

Use disadvantages

What is the problem with the following code where blocks are nested?

for (int i= 0; i<100; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ static void (^testMethod)(int); testMethod = ^(int value){ if (value > 0) { NSLog(@"current value = %d",value); testMethod(value - 1); }}; testMethod(10); }); }Copy the code
  • Before the lock is unlocked, there are many current=9, 10, causing data chaos, mainly due to multithreading

  • What’s the problem if you lock it like this?
NSLock *lock = [[NSLock alloc] init]; for (int i= 0; i<100; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ static void (^testMethod)(int); testMethod = ^(int value){ [lock lock]; if (value > 0) { NSLog(@"current value = %d",value); testMethod(value - 1); }}; testMethod(10); [lock unlock]; }); }Copy the code

The result is as followsThere’s always waiting, mainly becauseNested recursion used, the use ofNSLock (simple mutex, if it doesn't come back, it will sleep and wait)That is, the lock is added until the unlock is blocked

Therefore, in this case, you can use the following ways to solve

  • use@synchronized
for (int i= 0; i<100; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ static void (^testMethod)(int); testMethod = ^(int value){ @synchronized (self) { if (value > 0) { NSLog(@"current value = %d",value); testMethod(value - 1); }}}; testMethod(10); }); }Copy the code
  • Use recursive locksNSRecursiveLock
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
 for (int i= 0; i<100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        [recursiveLock lock];
        testMethod = ^(int value){
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
            [recursiveLock unlock];
        };
        testMethod(10);
    });
}
Copy the code

5, the pthread_mutex

Pthread_mutex is the mutex itself. Instead of waiting while the lock is occupied and another thread requests a lock, it blocks and sleeps

use

#import <pthread.h> // Declare the global mutex pthread_mutex_t _lock; Pthread_mutex_init (&_lock, NULL); / / lock pthread_mutex_lock (& _lock); Pthread_mutex_unlock (&_lock); Pthread_mutex_destroy (&_lock);Copy the code

6, NSRecursiveLock

NSRecursiveLockAt the bottom, tooPthread_mutexes encapsulation, can be passedswifttheFoundationThe source code to view

contrastNSLockNSRecursiveLock, the underlying implementation is almost exactly the same, except when you init it,NSRecursiveLockThere’s a signPTHREAD_MUTEX_RECURSIVEAnd theNSLockIs the default

Recursive locking is primarily used to address a form of nesting, where cyclic nesting is predominant

7, NSCondition

NSCondition is a condition lock that is used less frequently in everyday development and is somewhat similar to a semaphore: thread 1 must satisfy condition 1 before it moves down, otherwise it will block and wait until the condition is met. The classic model is the production-consumer model

The object of NSCondition actually acts as a lock and a thread inspector

  • The lock is designed to protect the data source when a condition is detected and perform the task that the condition raises

  • The thread inspector mainly determines whether to continue running the thread based on a condition, that is, whether the thread is blocked

use

NSCondition *condition = [[NSCondition alloc] init] NSCondition *condition = [[NSCondition alloc] init] [condition lock] is accessible only when the unlock is reached. // Use [condition unlock] with lock; // Put the current thread in the wait state [condition wait]; [condition signal]; // The CPU sends a signal telling the thread not to wait.Copy the code

Analysis of the underlying

View the underlying implementation of NSCondition through the Foundation source code for Swift

open class NSCondition: NSObject, NSLocking { internal var mutex = _MutexPointer.allocate(capacity: 1) internal var cond = _ConditionVariablePointer.allocate(capacity: Public override init() {pthread_mutex_init(nil) pthread_cond_init(nil); Pthread_mutex_destroy (mutex) pthread_cond_destroy(cond) mutex. Deinitialize (count: 1) cond.deinitialize(count: 1) mutex.deallocate() cond.deallocate()} // Open func lock() {pthread_mutex_lock(mutex)} // Open func unlock() { Pthread_mutex_unlock (mutex)} // Wait for open func wait() {pthread_cond_wait(cond, mutex)} Date) -> Bool { guard var timeout = timeSpecFrom(date: Limit) else {return false} return pthread_cond_timedwait(cond, mutex, &timeout) == 0} else {return false} pthread_cond_timedwait(cond, mutex, &timeout) == 0} Open func signal() {pthread_cond_signal(cond)} pthread_cond_broadcast(cond) // wait signal } open var name: String? }Copy the code

The underlying layer is also an encapsulation of the underlying pthread_mutex

  • NSCondition is a wrapper around mutex and cond (cond is a pointer to access and manipulate specific types of data)

  • The WAIT operation blocks the thread and puts it into hibernation until it times out

  • The signal action is to wake up a dormant waiting thread

  • Broadcast will wake up all waiting threads

8 NSConditionLock.

NSConditionLock is a conditional lock. Once one thread acquies the lock, other threads must wait

NSCondition is more cumbersome to use than NSConditionLock, so NSConditionLock is recommended, which is used as follows

// Initialize NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2]; / / said conditionLock expects the lock, and if there are no other thread lock (no need to judge the internal condition) of the following code, it can perform visit if you have other thread lock (lock may be conditions, or unconditional lock), the waiting, ConditionLock lock until another thread unlocks [conditionLock lock]; // If no other thread has acquired the lock, but the condition inside the lock is not equal to A, it still cannot acquire the lock and still waits. If the internal condition is equal to condition A, and no other thread has acquired the lock, the code area is entered, and it is set to acquire the lock, and any other thread will wait for its code to complete until it unlocks. [conditionLock lockWhenCondition:A condition]; [conditionLock unlockWithCondition: condition]; // indicates that the thread is no longer blocked if it is locked (no lock is acquired) and the time exceeds that. Return = [conditionLock lockWhenCondition:A condition :A condition :A time]; // The condition is an integer, and the condition is compared internally by integerCopy the code

NSConditionLock, which is essentially NSCondition + Lock, and here’s the underlying implementation of its Swift,

open class NSConditionLock : NSObject, NSLocking { internal var _cond = NSCondition() internal var _value: Int internal var _thread: _swift_CFThreadRef? public convenience override init() { self.init(condition: 0) } public init(condition: Int) { _value = condition } open func lock() { let _ = lock(before: Date.distantFuture) } open func unlock() { _cond.lock() _thread = nil _cond.broadcast() _cond.unlock() } open var condition: Int { return _value } open func lock(whenCondition condition: Int) { let _ = lock(whenCondition: condition, before: Date.distantFuture) } open func `try`() -> Bool { return lock(before: Date.distantPast) } open func tryLock(whenCondition condition: Int) -> Bool { return lock(whenCondition: condition, before: Date.distantPast) } open func unlock(withCondition condition: Int) { _cond.lock() _thread = nil _value = condition _cond.broadcast() _cond.unlock() } open func lock(before limit: Date) -> Bool { _cond.lock() while _thread ! = nil { if ! _cond.wait(until: limit) { _cond.unlock() return false } } _thread = pthread_self() _cond.unlock() return true } open func lock(whenCondition condition: Int, before limit: Date) -> Bool { _cond.lock() while _thread ! = nil || _value ! = condition { if ! _cond.wait(until: limit) { _cond.unlock() return false } } _thread = pthread_self() _cond.unlock() return true } open var name: String? }Copy the code

You can see it in the source code

  • NSConditionLock is a wrapper around NSCondition

  • NSConditionLock can set the lock condition, the condition value, while NSCondition is just a notification of the signal

Debug verification

Take the following code as an example to debug the underlying NSConditionLock process

- (void)cjl_testConditonLock{// semaphore NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [conditionLock lockWhenCondition:1]; // conditoion = 1 internal Condition matching // -[NSConditionLock lockWhenCondition: beforeDate:] NSLog(@" thread 1"); [conditionLock unlockWithCondition:0]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ [conditionLock lockWhenCondition:2]; Sleep (0.1); NSLog (@ "thread 2"); // self.myLock.value = 1; [conditionLock unlockWithCondition:1]; // _value = 2 -> 1 }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [conditionLock lock]; NSLog (@ "thread 3"); [conditionLock unlock]; }); }Copy the code
  • inconditionLockPart on the response breakpoint, run (need inA:Run on: The emulator runs Intel instructions, while the real machine runs ARM instructions)

  • Stop and start assembly debugging

  • register readReads the register wherex0Is the receiverselfx1iscmd

  • inobjc_msgSendAdd a breakpoint at, and read register x0 againregister read x0At this point, the execution is completed [conditionLock lockWhenCondition:2];

  • Read the x1, namelyregister read x1And I can’t read it because x1 is storingselIs not an object type and can be converted by a strong conversionSELread

  • Signed break point- [NSConditionLock lockWhenCondition:], and [NSConditionLock lockWhenCondition: beforeDate:]Then check the jumps such as BL, B, etc
    • Read registers X0, x2 are currentlockWhenCondition:beforeDate:The parameter that actually goes is [conditionLock lockWhenCondition:1];

    • Through assembly,x2Moved tox21

At this point, we debug for two main purposes: NSCondition + Lock and condition matching value

NSCondition + Lock validation

  • Continue, break at BL, read registerx0In this case, jump toNSCondition

  • I’m going to read x1, which is thetapo (SEL)0x00000001c746e484

So we can verify that NSConditionLock is calling the LOCK method of NSCondition underneath

Condition matches the value of value

  • Go ahead and jump toldrThat is, through a method, we get the value of the attribute of condition 2 and store it tox8In the
    • register read x19
    • Po (SEL) 0x0000000283D0D220 — Address of X19 +0x10

– register read x8,此时的x8中存储的是 2 cmp x8, x21“, which means that x8 is matched with x21, that is, 2 is matched with 1, but does not match

  • The second timecmp x8, x21In this case, x8 and x21 are matched, i.e [conditionLock lockWhenCondition:2];

In this case, x8 and x21 match, which is also reflected by the breakpoint

Summary of Demo analysis

  • Thread 1 calls [NSConditionLock lockWhenCondition:] to enter the waiting state because the current condition is not met. When the current condition is in waiting, the current mutex is released.

  • At this point the current thread 3 calls [NSConditionLock lock:], which essentially calls [NSConditionLock lockBeforeDate:]. There is no need to compare the conditional values, so thread 3 prints

  • Next thread 2 executes [NSConditionLock lockWhenCondition:], and since the condition value is met, thread 2 prints, [NSConditionLock unlockWithCondition:] is called when the print is complete, setting the value to 1 and sending a Boradcast, at which point thread 1 receives the current signal and wakes up to execute and print.

  • From here the current print is thread 3-> thread 2 -> thread 1

  • [NSConditionLock lockWhenCondition:]; If the Value of condition is not equal to the Value of Value passed in, the thread pool will be blocked, otherwise the code will continue to execute [NSConditionLock unlockWithCondition:]: The current value is changed and then broadcast to wake up the current thread

The performance summary

  • OSSpinLock has been deprecated since iOS10 due to security issues. The underlying implementation of OSSpinLock is being replaced by OS_unfair_lock

    • With OSSpinLock and as shown, you will be in a busy wait state

    • Os_unfair_lock is hibernated

  • The atomic lock comes with a spin-lock, which is thread-safe only for setters and getters, and is more nonatomic in everyday development

    • Atomic: When setters and getters of properties are called, osspinlock is added to ensure that only one thread can read or write the property at a time. Because the mutex code is automatically generated by the underlying compiler, it is relatively inefficient

    • Nonatomic: When setters and getters are called on properties, they are not spin-locked. Because the compiler does not automatically generate the mutex code, this improves efficiency

  • @synchronized maintains a hash table to store thread data, and a linked list to represent reentrant (i.e., nesting) features. Although performance is low, it is used frequently due to its simplicity and ease of use

  • NSLock and NSRecursiveLock encapsulate pthread_mutex

  • NSCondition and NSConditionLock are conditional locks that encapsulates a pthread_mutex and operate only when a certain condition is met. They are similar to those of the “dispatch_semaphore”

Use scenarios of locks

  • For simple use, such as when thread safety is involved, use NSLock

  • For loop nesting, @synchronized is recommended, mainly because the performance with recursive locks is not as good as with @synchronized (since it doesn’t matter how much you reenter in synchronized, and NSRecursiveLock may crash).

  • In loop nesting, if you have a good grasp of recursion locking, you are advised to use recursion locking because of good performance

  • @synchronized is recommended for nested loops and multi-threaded effects, such as waits and deadlocks