In previous installations, we explored GCD, a multi-threaded technology that iOS uses a lot. In this installment, we explore an important concept of locking in multi-threading.

The classification of the lock

There are two main categories of locks: spin locks and mutex locks.

spinlocks

In a spinlock, the thread double-checks that a variable is available. This is a busy wait because the thread keeps executing consistently throughout the process. Once a spinlock is acquired, the thread holds it until it explicitly releases the spinlock. Spin-locks avoid the scheduling overhead of the process context and are therefore effective in situations where threads block only for short periods of time. For the iOS property modifier atomic, it comes with a spin lock

The mutex

Mutex is a mechanism used in multithreaded programming to prevent two threads from reading or writing to the same common resource (such as a global variable) at the same time. This is achieved by cutting code into critical sections.

Read-write lock

A read/write lock is a special mutex lock that divides visitors to a shared resource into readers and writers. Readers only read the shared resource, while writers write the shared resource. This type of lock improves concurrency over a spin lock because in a multiprocessor system it allows multiple readers to access a shared resource at the same time, with the maximum possible number of readers being the actual number of logical cpus. Writers are exclusive; a read/write lock can have only one writer or more readers at a time (depending on the number of cpus), but not both readers and writers. Preemption also fails during read/write lock holding.

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

When a write/write lock is in the write/lock state, all threads attempting to lock the lock are blocked until the lock is unlocked. When a read-write lock is in read-lock state, all threads attempting to lock it in read mode gain access, but if a thread wishes to lock it in write mode, it must wait until all threads release the lock. Generally, when the read-write lock is in the read-mode lock state, if another thread tries to lock in write mode, the read-write lock will block the subsequent read-mode lock request. In this way, the read-mode lock will not be occupied for a long time, while the waiting write mode lock request will be blocked for a long time. Read/write locks are suitable for situations where data structures are read much more often than written. The read-write lock is also called shared-exclusive lock because it can be shared while the write lock means exclusive lock.

Performance comparison of several locks

We compare the performance of various locks by code printing:

int js_runTimes = 100000; /** OSSpinLock performance */ {OSSpinLock js_spinlock = OS_SPINLOCK_INIT; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { OSSpinLockLock(&js_spinlock); / / unlock OSSpinLockUnlock (& js_spinlock); } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"OSSpinLock: %f ms",(js_endTime - js_beginTime)*1000); } /** dispatch_semaphoRE_t performance */ {dispatch_semaphore_t js_sem = dispatch_semaphore_create(1); double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { dispatch_semaphore_wait(js_sem, DISPATCH_TIME_FOREVER); dispatch_semaphore_signal(js_sem); } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"dispatch_semaphore_t: %f ms",(js_endTime - js_beginTime)*1000); } /** OS_UNFAIR_lock_lock Performance */ {os_UNfair_lock js_unfairlock = OS_UNFAIR_LOCK_INIT; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { os_unfair_lock_lock(&js_unfairlock); os_unfair_lock_unlock(&js_unfairlock); } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"os_unfair_lock_lock: %f ms",(js_endTime - js_beginTime)*1000); } /** pthread_mutex_t performance */ {pthread_mutex_t js_metext = PTHREAD_MUTEX_INITIALIZER; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { pthread_mutex_lock(&js_metext); pthread_mutex_unlock(&js_metext); } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"pthread_mutex_t: %f ms",(js_endTime - js_beginTime)*1000); } /** NSlock performance */ {NSlock *js_lock = [NSlock new]; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { [js_lock lock]; [js_lock unlock]; } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"NSlock: %f ms",(js_endTime - js_beginTime)*1000); {NSCondition *js_condition = [NSCondition new]; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { [js_condition lock]; [js_condition unlock]; } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"NSCondition: %f ms",(js_endTime - js_beginTime)*1000); } /** recursive function */ {pthread_mutex_t js_metext_recurive; pthread_mutexattr_t attr; pthread_mutexattr_init (&attr); pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&js_metext_recurive, &attr); double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { pthread_mutex_lock(&js_metext_recurive); pthread_mutex_unlock(&js_metext_recurive); } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"PTHREAD_MUTEX_RECURSIVE: %f ms",(js_endTime - js_beginTime)*1000); } /** NSRecursiveLock */ {NSRecursiveLock = [NSRecursiveLock new]; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { [js_recursiveLock lock]; [js_recursiveLock unlock]; } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"NSRecursiveLock: %f ms",(js_endTime - js_beginTime)*1000); } /** NSConditionLock performance */ {NSConditionLock *js_conditionLock = [NSConditionLock new]; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { [js_conditionLock lock]; [js_conditionLock unlock]; } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"NSConditionLock: %f ms",(js_endTime - js_beginTime)*1000); } /** @synchronized performance */ {double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { @synchronized(self) {} } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"@synchronized: %f ms",(js_endTime - js_beginTime)*1000); }Copy the code

The result printed in iPhone 12Pro emulator is:

The results on the iPhone12 mini are as follows:

It can be seen that the performance of @synchronized lock is relatively poor on the simulator, but the performance of the series 12 (XR has not been improved after testing) mobile phone has been greatly improved, which will be common in our project, so we will start from @synchronized.

@synchronized

We’ll write a lock in our main.m file:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        @synchronized (appDelegateClassName) {
       
        }
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
Copy the code

Compile it into a.cpp file using the xcrun command

Xcrun – SDK iphoneos clang -arch arm64e-rewrite-objc main.m Locate the main function at the bottom of the main. CPP file and locate the @synchronized block

After formatting the code:

{ id _rethrow = 0; id _sync_obj = (id)appDelegateClassName; objc_sync_enter(_sync_obj); try { struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {} ~_SYNC_EXIT() {objc_sync_exit(sync_exit); } id sync_exit; } _sync_exit(_sync_obj); } catch (id e) {_rethrow = e; } { struct _FIN { _FIN(id reth) : rethrow(reth) {} ~_FIN() { if (rethrow) objc_exception_throw(rethrow); } id rethrow; } _fin_force_rethow(_rethrow); }}Copy the code

For successful locking we only need to focus on the try block and above. After our simplified code:

id _sync_obj = (id)appDelegateClassName; 
objc_sync_enter(_sync_obj);
objc_sync_exit(sync_exit);
Copy the code

You can see that basically two functions, objc_sync_Enter and objc_sync_exit, are executed.

Libobjc source code analysis

By breaking the sign breakpoint objc_sync_enter, we know that objc_sync_enter is in the libobJC source code.

Next, we search the libobJC source for objc_sync_enter globally:

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

If obj is passed as nil, nothing will be done. Data ->mutex.lock() is an instance of SyncData, so let’s explore the structure of the SyncData type:

SyncData
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
  • nextData: The next node in the linked list structure
  • object: Refer to the structure of the associated object, hash table
  • threadCount: Number of threads that use blocks
  • mutex: Recursive locking (using multiple threads alone causes problems)

The id2Data function is used to initialize SyncData instances, which we’ll explore next.

Id2data function

The id2data function has 150+ lines. Let’s hide the code block and have an overview of the structure:

The first two lines of the function have two macros defined as follows:

#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
Copy the code

SDataLists are a static hash table structure whose data structure we use LLDB to look at:

After breakpoint debugging, the first code to come in is:

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;Copy the code

One detail here is that listP uses header interpolation to add new elements to the list, and its implementation of recursion depends on the use of this data structure.

summary

  • Synchronized data structure is hash table, using the zip method to deal with hash conflict

  • SDataLists Arrary keys are object-specific, and elements in a zipper list are locks for the same object.

  • Objc_sync_enter and objc_Sync_Exit are symmetric; it is a recursive lock.

  • There are two storage structures: TLS and Catch

  • SyncData is accessed for the first time using the header list structure tag threadCount = 1

  • And then on subsequent visits, it’s going to determine if it’s the same object, the same object lockcount++, not the same object threadCount++1.

  • Synchronized is a reentrant, recursive, multithreaded lock, for a reason

    • TLS guarantees that threadCount can be locked by multiple threads
    • Lock++ will keep track of how many total locks were locked.

NSLock and NSRecursiveLock are used

Let’s use an example to analyze the differences between the two locks:

for (int i= 0; i<10; 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

The above code has a multi-thread conflict problem, and the printed result is out of order, which is not what we want

useNSLockTo solve the problem

The solution to using NSLock is simply to lock the testMethod before and after we call it.

We’ll look at the print and see if it works.

NSLock works on the outermost layer of the lock, so if we can only write code that operates on testMethod, then NSLock won’t work.

Using NSRecursiveLock

We know that NSLock won’t solve the problem in testMethod, so we’ll try NSRecursiveLock.

Finding NSRecursiveLock does not solve the problem and occasionally crashes. NSRecursiveLock is a recursive lock, but it does not support multithreaded recursion.

Using the @ synchronized

Using @synchronized solves the problem in the business code, showing that @synchronized is a recursive lock that supports multiple threads.

NSCondition

The object of NSCondition actually acts as a lock and a thread inspector: the lock is mainly used to protect the data source and perform the tasks triggered by the condition when the condition is detected; The thread checker mainly decides whether to continue running a thread based on conditions, that is, whether the thread is blocked. It has four main methods

  • [Condition lock] : Allows multiple threads to access and modify the same data source at the same time. The data source can be accessed and modified only once at the same time. Commands of other threads must wait outside the LOCK until the unlock is accessed
  • [condition unlock]; // Use with lock
  • [condition wait]; // Make the current thread wait
  • [condition signal]; // The CPU signals the thread to stop waiting and continue executing

One of its application scenarios is the producer-consumer model. That is, to produce and sell products through multiple threads. When the number of products is 0, we can only wait. The specific code is as follows:

- (void)js_testConditon{ _testCondition = [[NSCondition alloc] init]; For (int I = 0; i < 50; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self js_producer]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self js_consumer]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self js_consumer]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self js_producer]; }); } } - (void)js_producer{ [_testCondition lock]; Self.ticketcount = self.ticketCount + 1; NSLog(@" produce an existing count %zd",self.ticketCount); [_testCondition signal]; // signal [_testCondition unlock]; } - (void)js_consumer{ [_testCondition lock]; If (self.ticketCount == 0) {NSLog(@" wait for count %zd",self.ticketCount); [_testCondition wait]; Self.ticketcount -= 1; self.ticketcount -= 1; NSLog(@" consume a remaining count %zd ",self.ticketCount); [_testCondition unlock]; }Copy the code

Foundation source code look at the lock package

Let’s explore swift-Corelibs-Foundation source code. We can see from the source code that LOCKS such as NSLock implement a protocol called NSLocking

public protocol NSLocking {
    func lock()
    func unlock()
}
Copy the code

These are encapsulation of pThreads

NSRecursiveLock, similarly, is distinguished from NSLock by pthread_mutexattr_setType (attrs, Int32(PTHREAD_MUTEX_RECURSIVE))

NSConditionLock

  • NSConditionLock is a lock, and once one thread has acquired the lock, the other threads must wait
  • The lock function: indicates that the object expects to acquire the lock. If no other thread has acquired the lock (regardless of the internal condition), it can execute this line of code. If another thread has acquired the lock (either conditionally or unconditionally), it will wait until the other thread unlocks
  • [xx lockWhenCondition: :A condition] Method: if no other thread has acquired the lock and the condition inside the lock is not equal to condition A, it still cannot acquire the lock and is still waiting. If the internal condition is equal to the condition A, and no other thread acquires the lock, the code area is entered, while it is set to acquire the lock, and any other thread will wait for its code to complete until it unlocks.
  • [XXX unlockWithCondition:A condition]; Represents the release of the lock and sets the internal condition to condition A
  • Return = [XXX lockWhenCondition:A conditional beforeDate:A time]; Indicates that the thread is no longer blocked if the lock is not acquired and the time elapsed. Note, however, that the value returned is NO, which does not change the state of the lock. The purpose of this function is to allow processing in both states.

The fence function implements read/write locks

Read/write locks provide the following functions:

  • Multiple read single write function.
  • Write and write are mutually exclusive.
  • Read and write are mutually exclusive.
  • Write cannot block main thread task execution.
@interface ViewController ()
​
@property (nonatomic, strong) dispatch_queue_t js_currentQueue;
@property (nonatomic, strong) NSMutableDictionary *mDict;
@end
​
@implementation ViewController
​
- (void)viewDidLoad {
    [super viewDidLoad];
    self.js_currentQueue = dispatch_queue_create("jscurrent", DISPATCH_QUEUE_CONCURRENT);
    self.mDict = [[NSMutableDictionary alloc] init];
    [self js_safeSetter:@"123" time:10];
    [self js_safeSetter:@"456" time:5];
    [self js_safeSetter:@"789" time:3];
    
}
​
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    for (int i = 0; i < 5; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"读取,name = %@ thread---%@",[self js_safeGetter],[NSThread currentThread]);
        });
    }
}
​
- (void)js_safeSetter:(NSString *)name time:(int)time{
    dispatch_barrier_async(self.js_currentQueue, ^{
        sleep(time);
        [self.mDict setValue:name forKey:@"name"];
        NSLog(@"写入,name = %@ thread---%@",name,[NSThread currentThread]);
    });
    
}
​
- (NSString *)js_safeGetter{
    __block NSString *result;
    dispatch_sync(self.js_currentQueue, ^{
        result = self.mDict[@"name"];
    });
    return result;
}
@end
Copy the code

When the program is running, we start to click the screen (read operation) and finally see the print result: