The introduction

In the iOS development process, we use async and multithreading to improve the running performance of the program. At the same time, multithreading security has become a problem we must face. In terms of safety, resources should be avoided as far as possible between threads to reduce the interaction between threads. When using locks, be careful not to cause deadlocks that may cause the program to fail. This article focuses on the underlying implementation principles of several locks commonly used in iOS development and how to use them.


  • Common locks

  1. @synchronized // synchronized
  2. Pthread_mutex // mutex
  3. NSLock // mutex
  4. NSRecursiveLock // Recursive lock
  5. NSCondition // Conditional lock
  6. NSConditionLock // conditionlock
  7. Dispatch_semaphore // semaphore (not technically a lock)
  8. OSSpinLock // spin lock

In terms of functions, the above locks can be divided into two types:

1. Mutex:

A mechanism used in multithreaded programming to prevent two threads from simultaneously reading or writing to the same common resource (such as a global variable). This is done by slicing code into critical sections. A critical region is a piece of code that accesses a common resource, not a mechanism or algorithm. A program, process, or thread can have multiple critical regions, but mutex is not always applied.

If the calling thread finds that the lock is already held by another thread when it attempts to acquire the lock resource, the calling thread will go to sleep, and the CPU will perform other threads’ tasks until the locked resource releases the lock. At this point, the sleeping thread will be awakened, so that the preemptive strategy does not occupy CPU resources. But because CPU context switches are involved, there is a time consumption.

Mutex: @synchronized, NSLock, pthread_mutex, NSConditionLock, NSCondition, NSRecursiveLock

2. The spin lock

A spin lock is a type of lock that is synchronized by multiple threads, where the thread repeatedly checks whether the lock variable is available. Because the thread keeps executing during this process, it is a busy wait. Once a spin lock is acquired, the thread holds it until it explicitly releases the spin lock. 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.

Spin lock: OSSpinLock


  • Specific usage introduction

  1. @synchronized is a mutex recursive lock:

Recursive locks are also known as reentrant locks. Mutex can be divided into non-recursive locks and recursive locks. The main differences are as follows: The same thread can repeatedly acquire recursive locks without deadlocks. A deadlock occurs when the same thread repeatedly acquires a non-recursive lock.

Usually used as follows:

@synchronized (obj) {
    // Add operation
} 
Copy the code

The actual execution process of the above code is as follows:

  1. Objc_sync_enter (id obj
  2. The operation to lock
  3. Objc_sync_exit (id obj

@synchronized is a mutex -> recursive lock with nil inside to prevent deadlocks. Locked objects are stored through the structure of the table, and objects inside the table are hashed.

Objc_sync_enter (id obj)

int objc_sync_enter(id obj) { int result = OBJC_SYNC_SUCCESS; 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

Objc_sync_exit (id obj)

int objc_sync_exit(id obj) { int result = OBJC_SYNC_SUCCESS; If (obj) {SyncData* data = id2data(obj, RELEASE); if (! data) { result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR; Bool okay = data-> mutex.tryunLock (); if (! okay) { result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR; } } } else { // @synchronized(nil) does nothing } return result; }Copy the code

Note:

  1. When multiple threads asynchronously operate on the same object at the same time, because recursive locking will not stopobjc_sync_enter(id obj)In some special situationsobjThe object might benil, at a time whenobjc_sync_enter(id obj)It’s going to be judged internally ifobj == nil, the lock is no longer held, resulting in thread access conflicts.
  2. If you have multiple threads@synchronized (obj)The incomingobjDifferent is a new oneobj, the thread will own its lock each time and continue processing without being blocked by another thread.

Id2data (OBj, ACQUIRE). Before introducing the code call process, let’s take a look at some structures:

typedef struct alignas(CacheLineSize) SyncData { struct SyncData* nextData; // Next SyncData DisguisedPtr<objc_object> object; // Lock object int32_t threadCount; // Number of THREADS using this block recursive_mutex_t mutex; } SyncData; 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; struct SyncList { SyncData *data; spinlock_t lock; constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { } };Copy the code
using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>;
template <bool Debug>
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)
    { }
};
Copy the code

Recursive_mutex_t is a mutex recursion lock that is based on the os_UNFAIR_RECURsiVE_LOCK mutex wrapper (earlier, the pthread_mutex_t wrapper). Os_unfair_recursive_lock is an os_UNFAIR_RECURSIVE_LOCK package

#define OS_UNFAIR_RECURSIVE_LOCK_AVAILABILITY \ __OSX_AVAILABLE(10.14) __IOS_AVAILABLE(12.0) \ __TVOS_AVAILABLE(12.0) __WATCHOS_AVAILABLE (5.0)Copy the code

Os_unfair_recursive_lock is not available until iOS 12.0. @synchronized is a mutex recursive lock that wraps OS_UNFAIR_LOCK.

Next, we return to the call of ID2data, because the source content is more, here is only the core part of the introduction, roughly divided into four steps:

Static SyncData* ID2data (id object, enum usage why) {// SyncData *data = (SyncData *) tlS_get_direct (SYNC_DATA_DIRECT_KEY); if (data) { if (data->object == object) { //... Omit the return result; SyncCache *cache = fetch_cache(NO); SyncCache *cache = fetch_cache(NO); if (cache) { //... Omit the return result; Lockp ->lock(); lockp->lock(); {/ /... Ellipsis} 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; // If all goes well, fast cache is supported and stored in fast cache; otherwise, it is stored in thread cache for quick lookup next time. If there is an error, an exception is thrown; done: lockp->unlock(); if (result) { //... Omit} return result; }Copy the code

Here’s what you did in each step.

  1. Step 1: Fast caching

If fast cache is supported, the matching object is searched from the fast cache first. If a matching object is found, the corresponding operation is performed based on the Usage enumeration value, and result is returned.

#if SUPPORT_DIRECT_THREAD_KEYS // Check per-thread single-entry fast cache for matching objects 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"); } // Update TSL cache lock count according to enumeration enum usage {ACQUIRE, RELEASE, CHECK} switch(why) {case ACQUIRE: {// get lock once 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) {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; } } #endifCopy the code
  1. Step 2: Thread caching

If no object is found in the cache, it is searched from the thread cache, and if a match is found, the corresponding operation is performed based on the Usage enumeration value, and result is returned.

// Check per-thread cache of already-owned locks for matching objects // Get a SyncCache from the current thread cache 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 (1); case RELEASE: item->lockCount--; If (item->lockCount == 0) {if (item->lockCount == 0) { 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; }}Copy the code
  1. Step 3: No cache

If the current object has no cache, the list in use is iterated to find a matching object, and the corresponding operation is performed.

lockp->lock(); // 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; } // Create SyncData 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
  1. Step 4:donesaveSyncDataobject
Done: // unlock(); if (result) { // Only new ACQUIRE should get here. // All RELEASE and CHECK and recursive ACQUIRE are // handled by the // RELEASE if (why == RELEASE) {// Probably some thread is no longer part of the object is held by another thread. return nil; } // Instead of getting a lock, call '_objc_fatal' if (why! = ACQUIRE) _objc_fatal("id2data is buggy"); // Call '_objc_fatal' if (result->object! = object) _objc_fatal("id2data is buggy"); #if SUPPORT_DIRECT_THREAD_KEYS #if SUPPORT_DIRECT_THREAD_KEYS 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++; }} // SyncData return result;Copy the code

In the Debug environment of _objc_fatal, App will not crash, but will print logs by calling _objc_syslog, and then calling _Exit(1) to exit program execution normally.

In a Release environment, _objc_crashlog is called to add information to the crashlog, and abort_with_reason is called to break the program.

2. Pthread_mutex is a mutex

In Portable Operating System Interface (POSIX), pthread_MUtex is a set of MUtex locks for multi-threaded synchronization. Like the name, it is very simple to use and has high performance. Rather than using busy waits, pthread_mutex, like a semaphore, blocks the thread and waits, and is called with a thread context switch.

#import <pthread.h> // declare muthread_mutex_t _lock; // Declare the mutex attribute object pthread_mutexattr_t attr; // Initialize the attribute object pthread_mutexattr_init(&attr); // Initialize the mutex pthread_mutex_init(&lock, &attr); pthread_mutex_init(&lock, NULL); / / lock pthread_mutex_lock (& _lock); / / unlock pthread_mutex_unlock (& _lock); Pthread_mutex_destroy (&_lock); // Set the mutex type attribute pthread_mutexattr_setType (&attr, PTHREAD_MUTEX_NORMAL); * * Mutex protocol Attributes */ #define PTHREAD_PRIO_NONE 0 #define PTHREAD_PRIO_INHERIT 1 #define /* * Mutex type Attributes */ #define PTHREAD_MUTEX_NORMAL 0 #define PTHREAD_MUTEX_ERRORCHECK 1 #define PTHREAD_MUTEX_RECURSIVE 2 #define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMALCopy the code

If you’re interested in using pthread_mutex in more detail, read the mutex properties to learn more.

3. NSLock is the mutex

The underlying NSLock is a wrapper around pthread_MUtex, which performs slightly worse than pthread_MUtex. But because of the cache, multiple calls don’t have much of an impact on performance parameters.

The NSLock source code is in the CoreFundation framework and cannot be viewed, but the full definition of NSLock can be found in the Swift open source CoreFoundation.

private typealias _MutexPointer = UnsafeMutablePointer<pthread_mutex_t> private typealias _RecursiveMutexPointer = UnsafeMutablePointer<pthread_mutex_t> private typealias _ConditionVariablePointer = UnsafeMutablePointer<pthread_cond_t>  open class NSLock: NSObject, NSLocking { internal var mutex = _MutexPointer.allocate(capacity: 1) private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1) private var timeoutMutex = _MutexPointer.allocate(capacity: 1) public override init() {// Initialize the mutex, Pthread_mutexattr_t pthread_mutex_init(mutex, nil) pthread_cond_init(timeoutCond, Nil) pthread_mutex_init(timeoutMutex, nil)} deinit {// Muthread_mutex_destroy (mutex) mutex.deinitialize(count: 1) mutex.deallocate() deallocateTimedLockData(cond: timeoutCond, mutex: TimeoutMutex)} open func lock() {pthread_mutex_lock(mutex)} open func unlock() {// pthread_mutex Pthread_mutex_unlock (mutex) Wakeup any threads waiting in lock(before:) pthread_mutex_lock(timeoutMutex Broadcast (timeoutCond) pthread_mutex_unlock(timeoutMutex)} Only false open func 'try' () -> Bool {return pthread_mutex_trylock(mutex) == 0} // Keep trying to lock open func before a certain point lock(before limit: Date) -> Bool {// pthread_mutex_trylock attempts to lock, Return true if pthread_mutex_trylock(mutex) == 0 {return true} // Call pthread_mutex_trylock internally through a while loop. Return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with:) timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: TimeoutMutex)} // You can use a string name to identify the lock, and Cocoa will use this name as part of the error involving the receiver. open var name: String? }Copy the code

As can be seen from the code, NSLock also has control over timeout timeout. It should be noted that NSLock does not support recursive calls, that is, the same thread can not be locked twice, if the occurrence of recursive calls will cause deadlock. If you need to implement recursive calls, you can use NSRecursiveLock.

4. NSRecursiveLock recursive lock

Like NSLock, we can also see the full definition of NSRecursiveLock in the source nsLock. swift:

private typealias _RecursiveMutexPointer = UnsafeMutablePointer<pthread_mutex_t> open class NSRecursiveLock: NSObject, NSLocking { internal var mutex = _RecursiveMutexPointer.allocate(capacity: 1) private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1) private var timeoutMutex = _MutexPointer.allocate(capacity: 1) public override init() { super.init() var attrib = pthread_mutexattr_t() withUnsafeMutablePointer(to: &attrib) {attrs in pthread_mutexattr_init(attrs) // Initializes the pthread_mutexattr_t mutex attribute, Set the recursive type to PTHREAD_MUTEX_RECURSIVE let type = Int32(PTHREAD_MUTEX_RECURSIVE) pthread_mutexattr_settype(attrs, type) pthread_mutex_init(mutex, attrs) } pthread_cond_init(timeoutCond, nil) pthread_mutex_init(timeoutMutex, nil) } deinit { pthread_mutex_destroy(mutex) mutex.deinitialize(count: 1) mutex.deallocate() deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex) } open func lock() { pthread_mutex_lock(mutex) } open func unlock() { pthread_mutex_unlock(mutex) // Wakeup any threads waiting in lock(before:) pthread_mutex_lock(timeoutMutex) pthread_cond_broadcast(timeoutCond) pthread_mutex_unlock(timeoutMutex) } open func `try`() -> Bool { return pthread_mutex_trylock(mutex) == 0 } open func lock(before limit: Date) -> Bool { if pthread_mutex_trylock(mutex) == 0 { return true } return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex) } open var name: String? }Copy the code

As you can see, NSRecursiveLock is similar to NSLock, except that there are some differences in initialization that make NSRecursiveLock support recursive calls.

Pthread_mutexattr_t pthread_mutex_init(mutex, nil) NSRecursiveLock pthread_mutexattr_t, Set the type to recursive PTHREAD_MUTEX_RECURSIVE var attrib = pthread_mutexattr_t() withUnsafeMutablePointer(to: &attrib) {attrs in pthread_mutexattr_init(attrs) // Initializes the pthread_mutexattr_t mutex attribute, Set the recursive type to PTHREAD_MUTEX_RECURSIVE let type = Int32(PTHREAD_MUTEX_RECURSIVE) pthread_mutexattr_settype(attrs, type) pthread_mutex_init(mutex, attrs) }Copy the code

5. NSCondition Condition lock

private typealias _MutexPointer = UnsafeMutablePointer<pthread_mutex_t> private typealias _ConditionVariablePointer = UnsafeMutablePointer<pthread_cond_t> open class NSCondition: NSObject, NSLocking { internal var mutex = _MutexPointer.allocate(capacity: 1) / / condition variable pthread_cond_t internal var cond = _ConditionVariablePointer. The allocate (capacity: 1) public override init() { pthread_mutex_init(mutex, nil) pthread_cond_init(cond, nil) } deinit { 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)} open func wait() {pthread_cond_wait(cond, mutex) } open func wait(until limit: Date) -> Bool { guard var timeout = timeSpecFrom(date: Limit) else {return false} return pthread_cond_timedWait (cond, mutex, &timeout) == 0 Open func signal() {pthread_cond_signal(cond)} Wake up all waiting threads open func broadcast() {pthread_cond_broadcast(cond)} open var name: String?}Copy the code

From the point of view of the code, NSCondition also encapsulates pthread_mutex. In addition, it adds the condition variable cond, which is a variable of type pthread_cond_T, used to access and manipulate Pointers of specific types of data.

The wait operation blocks the thread, putting it to sleep, or returns false if it times out. The signal operation wakes up a dormant waiting thread. Broadcast wakes up all waiting threads.

NSConditionLock

open class NSConditionLock : NSObject, NSLocking { internal var _cond = NSCondition() internal var _value: Int // Current thread 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: whenCondition condition: Int) { let _ = lock(whenCondition: condition, before: Open func 'try' () -> Bool {return lock(before: Date.distantpast)} open func tryLock(whenCondition condition: Int) -> Bool { return lock(whenCondition: condition, before: Condition 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: 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

NSConditionLock is a further encapsulation of NSCondition with the addition of the _value condition variable of Int type. You can use NSConditionLock to implement the dependency between tasks.

7. Dispatch_semaphore semaphore

Dispatch_semaphore is used to ensure multi-threaded resource security. Its essence is based on the kernel semaphore interface to achieve, is a counter based multithread synchronization mechanism.

Semaphore is the carrier of PV operation. There are four basic operations: initialization, equisignal, signal feed, and cleanup (note: Under ARC, cleanup of Semaphore is automatically done by GCD at the bottom). The dispatch_semaphore interface is defined in the semaphore.h header file of libDispatch source code.

If value is less than 0 then NULL dispatch_semaphoRE_t dispatch_semaphoRE_create (long value); If the semaphore value of dSEMa is 0, then the thread will block. Until value is greater than 0 or timeout long dispatch_semaphore_WAIT (dispatch_semaphore_t dSEMa, dispatch_time_t timeout); // Add 1 long dispatch_semaphore_signal(dispatch_semaphore_t dsema);Copy the code

Here is a detailed introduction to the implementation of each function with the source code:

  1. dispatch_semaphore_create :
dispatch_semaphore_t dispatch_semaphore_create(long value) { dispatch_semaphore_t dsema; // If value is less than 0, If (value < 0) {// #define DISPATCH_BAD_INPUT ((void *_Nonnull)0) return DISPATCH_BAD_INPUT; } dsema = _dispatch_object_alloc(DISPATCH_VTABLE(semaphore), sizeof(struct dispatch_semaphore_s)); dsema->do_next = DISPATCH_OBJECT_LISTLESS; Dsema ->do_targetq = _dispatch_get_default_queue(false); Dsema -> value = value; _dispatch_sema4_init(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO); Dsema ->dsema_orig = value; return dsema; }Copy the code

You can see that if value is less than 0, NULL is returned. Passing a value greater than zero returns a semaphore object dSEMa of type dispatch_semaphoRE_T.

  1. dispatch_semaphore_wait :
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, Dispatch_time_t timeout) {os_atomic_DEC2O (dSEMA, DSEMA_value, acquire); Return slikely (value >= 0) {return slikely (value >= 0); } // if value is less than 0, block with _dispatch_semaphore_wait_slow, Return _dispatch_semaphoRE_wait_slow (dsema, timeout); } static long _dispatch_semaphore_wait_slow(dispatch_semaphore_t dsema, dispatch_time_t timeout) { long orig; _dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO); Switch (timeout) {default: // timeout is a specified time when _dispatch_sema4_timedWait is called if (! _dispatch_sema4_timedwait(&dsema->dsema_sema, timeout)) { break; } // Fall through and try to undo what the fast path did to // dsema->dsema_value case DISPATCH_TIME_NOW: orig = dsema->dsema_value; While (orig < 0) {os_atomic_cMPxchgvW2O (dsema, // return _DSEMA4_TIMEOUT(); // return _DSEMA4_TIMEOUT(); } } // Another thread called semaphore_signal(). // Fall through and drain the wakeup. case DISPATCH_TIME_FOREVER: _dispatch_sema4_wait(&dsema->dsema_sema); // dispatch_sema4_wait(&dsema->dsema_sema); break; } return 0; }Copy the code

Wait decrement semaphore (dSEMa is the target semaphore object), that is, the semaphore value of DSEMa decreases by one. If the semaphore value is 0, the thread will block until the value is greater than 0 or times out.

Timeout The value can be DISPATCH_TIME_NOW or DISPATCH_TIME_FOREVER. You can also use dispatch_time(dispatch_time_t when, int64_t delta) to specify the duration. If 0 is returned, the P operation succeeds. If a non-0 is returned, the P operation fails and the execution times out.

  1. dispatch_semaphore_signal :
Dispatch_semaphore_signal (dispatch_semaphore_t dsema) {dispatch_semaphore_signal(dispatch_semaphore_t dsema os_atomic_inc2o(dsema, dsema_value, release); If (likely(value > 0)) {return 0; } // Signal is unbalanced due to excessive operation, If (Unlikely (value == LONG_MIN)) {DISPATCH_CLIENT_CRASH(value, "Unbalanced call to dispatch_semaphore_signal()"); } // If value is less than or equal to 0, return _dispatch_semaphoRE_signal_slow (dsema) needs to be awakened. } long dispatch_semaphore_signal_slow(dispatch_semaphore_t dsema) {// if &dsema->dsema_sema is NULL, _dispatch_sema4_create_slow = &dsema-> dsemA_semA = _dispatch_sema4_create(&dsema-> dsemA_sema, _DSEMA4_POLICY_FIFO);  _dispatch_sema4_signal(&dsema-> dsemA_sema, 1); return 1; }Copy the code

If the value is greater than 0, return 0 directly. Otherwise, enter the _dispatch_semaphoRE_signal_slow method. This function calls the kernel’s semaphore_signal function to wake up the waiting thread.

Dispatch_semaphore is often used to implement functions:

  • It is used to synchronize threads and convert asynchronous tasks to synchronous tasks
  • To ensure thread safety, locking a thread is equivalent to a spin lock

It is worth noting that semaphores operate slightly less efficiently than spin locks and more efficiently than other locks. Therefore, semaphores are a high priority choice in high performance scenarios.

8. OSSpinLock

OSSpinLock is no longer safe. The main reason is that when a low-priority thread obtains the lock, the high-priority thread enters the busy-wait state, occupying a large amount of CPU time slices. As a result, the low-priority thread can’t get the CPU time slice, complete the task and release the lock, which causes the priority reversal of the task.

As of iOS 10/macOS 10.12, OSSpinLock has been deprecated. The alternative is an internal OS_UNFAIR_LOCK, which is dormant when locked, not busy, etc.


The performance comparison

Finally, compare the performance of various locks with YY Dashen’s performance test results:


Process knowledge

  1. What is a deadlock?

Deadlock occurs when two or more units are waiting for the other to stop executing in order to obtain system resources, but neither party exits early.

  1. What is thePVOperate?

PV operation is an effective method to realize process mutual exclusion and synchronization. PV operation is related to semaphore processing and is defined as follows:

  • P(S) : ① Reduce the value of the semaphore S by 1, that is, S = S-1; ② If S > 0, the process continues. Otherwise, the process is put into the wait state and queued.

  • V(S) : ① Add the value of semaphore S by 1, that is, S = S + 1;


conclusion

Synchronization tools are an effective way to make code thread-safe, but they are not a panacea. Overuse of locks and other types of synchronization primitives can actually degrade application threading performance compared to non-thread-safe performance. Finding the right balance between security and performance is an art that requires experience. Therefore, in the actual development process, we need to choose the appropriate implementation scheme according to the specific needs, to achieve both performance and security of the optimal solution. Above is the introduction of iOS lock related knowledge, thank you for reading.


References:

  • Threading Programming Guide

  • More than you want to know about @synchronized

  • Mutex property

  • OSSpinLock is no longer secure


About The Technical Group

The iOS technology group is mainly used to learn and share the technology used in daily development, and keep learning and making progress together. The article repository is here: github.com/minhechen/i… Wechat official account: iOS Technology group, welcome to contact and exchange, thank you for reading.