iOS– Related locks in iOS

Article directory

  • spin lock
    • 1. OS Spin Lock
    • 2. os_unfair_lock
    • 3. atomic
  • mutex
    • pthread_mutex
    • @synchronized
      • objc_sync_enter
      • objc_sync_exit
      • Precautions
    • NSLock
    • NSRecursiveLock
    • signal
    • conditional lock
      • NSCondition
      • NSConditionLock
  • read-write lock
  • Summarize

Locks are used as a non-enforced mechanism to ensure thread safety. Before each thread accesses data or resources, it must first acquire (Acquire) the lock, and release (Release) the lock after the access is completed. If the lock is already occupied, other threads trying to acquire the lock will wait until the lock is available again

Note: Do not put too many other operation codes in the lock, otherwise the other thread will be waiting when one thread executes, and the function of multi-threading will not be able to be played

There are only three basic types of locks in iOS: mutual exclusion locks, spin locks, and read-write locks. Other possibilities such as: conditional locks, recursive locks, and semaphores are all encapsulated and implemented by the upper layer.

I have learned some lock knowledge in my previous study, and I have used it in many places. When I looked at the source code of AFNetworking last week, I also saw the two types of NSLock and @synchronized The lock has been used many times.

Spinlock

We have learned about spin locks in the implementation principle of weak. There is a spin lock in the middle of each SideTable, and a separate lock is also used to lock a single SideTable.

Spinlock: A thread repeatedly checks whether a lock variable is available. Since the thread keeps executing during this process, it is a kind of busy waiting. Once a spinlock is acquired, a thread holds the lock until it is explicitly released.

1. OSSpinLock

Deprecated since OSSpinLock had security issues. The reason why the spin lock is not safe is that when the spin lock acquires the lock, the thread will always be in a deadlock state of busy waiting, resulting in the inversion of task priority

The OSSpinLock busy waiting mechanism may cause high priority tasks to running wait all the time, occupying CPU time slices, while low priority tasks cannot seize time slices and become delayed completion , without releasing the lock.

// initialization
spinLock = OS_SPINKLOCK_INIT;
// lock
OSSpinLockLock( & amp; spinLock);
// unlock
OSSpinLockUnlock( & amp; spinLock);

2.os_unfair_lock

This is what the weak implementation part of the spin lock uses. The spin lock is no longer safe. Apple launched os_unfair_lock, which solves the problem of priority inversion.

//Create a lock
    os_unfair_lock_t unfairLock;
//initialization
    unfairLock = &(OS_UNFAIR_LOCK_INIT);
    // lock
    os_unfair_lock_lock(unfairLock);
    //unlock
    os_unfair_lock_unlock(unfairLock);

3.atomic

In the actual application of spin lock, the automatically generated setter method will call different methods according to different modifiers, and finally call the reallySetProperty method uniformly, and there is a paragraph about atomic Modifier code.

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{<!-- -->
    if (offset == 0) {<!-- -->
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {<!-- -->
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {<!-- -->
        newValue = [newValue mutableCopyWithZone:nil];
    } else {<!-- -->
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {<!-- -->
        oldValue = *slot;
        *slot = newValue;
    } else {<!-- -->
        spinlock_t & slotlock = PropertyLocks[slot];
        slotlock. lock();
        oldValue = *slot;
        *slot = newValue;
        slotlock. unlock();
    }

    objc_release(oldValue);
}

Compare the logical branches of atomic:

  • The atomically modified attribute has been locked by spinlock
  • Except for non-atomic properties, the logic is the same as that of atomic

As mentioned earlier, os_unfair_lock replaced OSSpinLock, so OSSpinLock is also used above.

using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {<!-- -->
    os_unfair_lock mLock;
    ...
}

The same is true for the getter method: the property modified by atomic is locked.

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);
}

Atomic can only guarantee the thread safety of setter and getter methods, but cannot guarantee data safety.

If multiple threads call the setter at the same time, there will be no situation where another thread starts to execute the setter before one thread executes all the statements of the setter, which is equivalent to adding a lock at the beginning and end of the function. nonatomic does not guarantee primitive lines for setters/getters, so you may get incomplete stuff. For example, if two member variables are changed in the setter function, if you use nonatomic, the getter may get the state when only one of the variables is changed. atomic is thread-safe, nonatomic is thread-unsafe. If it is only a single-threaded operation, it is best to use nonatomic, because the latter is more efficient.

Mutex

A lock that performs mutual exclusion operations prevents two threads from reading and writing the same common resource (such as a global variable) at the same time.

  • Mutual exclusion lock: If the shared data is already locked by other threads, the thread will go to sleep and wait for the lock. Once the accessed resource is unlocked, the thread waiting for the resource will be woken up.
  • Spin lock: If the shared data is already locked by other threads, the thread will wait in an endless loop. Once the accessed resource is unlocked, the thread waiting for the resource will execute immediately
  • Spinlocks are more efficient than mutexes. But we should pay attention to that the thread holding the spin lock should release the spin lock as soon as possible because the CPU is not released during spin, otherwise the thread waiting for the spin lock will always spin there, wasting CPU time.

Mutex locks are further divided into:

  • Recursive lock: reentrant lock, the same thread puts money in the key to acquire the lock again, that is, it can be called recursively
  • Non-recursive lock: non-reentrant, you must wait for the lock to be released before acquiring the lock again

For recursive locks, we should pay attention to the deadlock problem when using them. The code before and after waiting for each other will deadlock
For non-recursive locks, if we force recursion, it will cause blockage instead of deadlock.

pthread_mutex

pthread_mutex is the mutex itself – when the lock is occupied and other threads apply for the lock, instead of using busy waiting, the thread is blocked and sleeps

// import header file
#import <pthread.h>
// Declare the mutex globally
pthread_mutex_t_lock;
// Initialize the mutex
pthread_mutex_init( & amp;_lock, NULL);
// lock
pthread_mutex_lock( & amp;_lock);
// Doing here requires thread-safe operations
//...
// unlock
pthread_mutex_unlock( & amp;_lock);
// release the lock
pthread_mutex_destroy( & amp;_lock);

@synchronized

@synchronized may be a kind of mutex that is often used in daily development, because its use is relatively simple, but @synchronized can not be used in any scenario, and its performance is low.
@synchronized requires a parameter, which is equivalent to a semaphore

// initialization
@synchronized(lock object){<!-- -->

}
/*
The PTHREAD_MUTEX_RECURSIVE mode of pthread_mutex encapsulated at the bottom layer,
Lock object to indicate whether it is the same lock
*/

Let’s take a brief look at its underlying implementation:

static void _I_MyPerson_run(MyPerson * self, SEL _cmd) {<!-- -->
    {<!-- -->
    id _rethrow = 0; id _sync_obj = (id)self;
    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);

        NSLog((NSString *) &__NSConstantStringImpl__var_folders_rx_h53wjns9787gpxxz8tg94y6r0000gn_T_MyPerson_9b8773_mi_3);
    } 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);}
}
}

synchronized called try catch, internally called objc_sync_enter and objc_sync_exit.

objc_sync_enter

// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
// Start synchronizing 'obj'.
// Allocate the recursive mutex associated with 'obj', if needed.
// Return OBJC_SYNC_SUCCESS after acquiring the lock.
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;
}

BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);
  • First of all, from its comments recursive mutex can be concluded that @synchronized is a recursive lock
  • If the locked object obj does not exist, it will go to objc_sync_nil() and do nothing. This is why @synchronized is used as a recursive lock but can prevent deadlocks: in the process of continuous recursion, if the object does not exist, the recursion will be stopped to prevent deadlocks.
  • Under normal circumstances (obj exists) a SyncData object will be generated through the id2data method
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;
    • nextData refers to the next SyncData in the linked list
    • object refers to the currently locked object
    • threadCount indicates the number of threads that use this object to lock
    • mutex is the lock associated with the object

objc_sync_exit

// End synchronizing on '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) {<!-- -->
        SyncData* data = id2data(obj, RELEASE);
        if (!data) {<!-- -->
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {<!-- -->
            bool okay = data->mutex. tryUnlock();
            if (!okay) {<!-- -->
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {<!-- -->
        // @synchronized(nil) does nothing
    }
\t

    return result;
}

Notes

  • Non-OC objects cannot be used as locking conditions – the receiving parameter in id2data is of type id
  • What are the consequences of locking the same object multiple times – data will be obtained from the cache, so the object will only be locked once
  • It is said that @synchronized has low performance – because adding, deleting, modifying and checking at the bottom consumes a lot of performance
  • The lock object cannot be nil, otherwise the lock is invalid and thread safety cannot be guaranteed

NSLock

NSLock is a non-recursive lock; NSLock is a simple wrapper for a mutex.
NSLock is used in AFURLSessionManager of AFNetworking

If you force a recursive call to a non-recursive lock, it will be blocked when the call is made. It is not a deadlock. After the first lock is locked, the recursive call is made before the lock is released. The second lock will block the thread. (because the cache will not be queried)

- (void)test {<!-- -->
    self.testArray = [NSMutableArray array];
    NSLock *lock = [[NSLock alloc] init];
    for (int i = 0; i < 200000; i ++ ) {<!-- -->
        dispatch_async(dispatch_get_global_queue(0, 0), ^{<!-- -->
            [lock lock];
            self.testArray = [NSMutableArray array];
            [lock unlock];
        });
    }
}

Please add a picture description
It is clearer from the explanation in the official documentation that calling the lock method of NSLock twice on the same thread will permanently lock the thread. At the same time, the official document mainly reminds that when sending an unlock message to an NSLock object, you must ensure that the message is sent from the same thread that sent the initial lock message.

NSRecursiveLock

NSRecursiveLock is a recursive lock

- (void)test {<!-- -->
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{<!-- -->
        static void (^block)(int);
        
        block = ^(int value) {<!-- -->
            [lock lock];
            if (value > 0) {<!-- -->
                NSLog(@"value--%d", value);
                block(value - 1);
            }
            [lock unlock];
        };
        block(10);
    });
}

If we add a for loop in the outer layer

- (void)test {<!-- -->
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    for (int i = 0; i < 10; i ++ ) {<!-- -->
        dispatch_async(dispatch_get_global_queue(0, 0), ^{<!-- -->
            static void (^block)(int);
            
            block = ^(int value) {<!-- -->
                [lock lock];
                if (value > 0) {<!-- -->
                    NSLog(@"value--%d", value);
                    block(value - 1);
                }
                [lock unlock];
            };
            block(10);
        });
    }
}

The program crashes.

Because the for loop performs multiple lock operations on the same object inside the block, until there are N locks hanging on this resource, and finally everyone cannot unlock it at one time, that is, there is no way to unlock it.

That is, lock 1 is added in thread 1, and lock 2 is added in thread 2 at the same time -> unlock 1 waits for unlock 2 -> unlock 2 waits for unlock 1 -> unlocking cannot be completed – a deadlock is formed

At this time, we can lock the object through @synchronized, and first check whether there is a lock syncData from the cache. If there is, return directly without locking to ensure the uniqueness of the lock.

A lock that the same thread can acquire multiple times without causing a deadlock.

Semaphore

Semaphore (semaphore): It is a more advanced synchronization mechanism. The mutex can be said to be a special case of semaphore when it only takes the value 0/1. Semaphores can have more value space to achieve more complex synchronization, not just mutual exclusion between threads.

// initialization
dispatch_semaphore_t semaphore_t = dispatch_semaphore_create(1);
// lock
dispatch_semaphore_wait(semaphore_t, DISPATCH_TIME_FOREVER);
// unlock
dispatch_semaphore_signal(semaphore_t);
/*
Note: The other two functions of dispatch_semaphore
1. It can also play the role of blocking threads.
2. The timer function can be realized, so I won’t introduce too much here.
*/

Condition lock

It is a condition variable. When certain resource requirements of the process are not met, it will go to sleep, that is, it will be locked. When the resource is allocated, the condition lock is opened and the process continues to run.
Under certain conditions, let it wait for dormancy and release the lock, and when it receives a signal or broadcast, it will reawaken the thread and re-lock it, like NSCondition encapsulates pthread_mutex For the above functions, NSConditionLock encapsulates NSCondition.

NSCondition

NSCondition is a conditional lock, which may not be used often, but it is similar to a semaphore: thread 1 needs to wait until condition 1 is met before going down, otherwise it will block and wait until the condition is met.

// initialization
NSCondition *_condition= [[NSCondition alloc]init];
// lock
[_condition lock];
// unlock
[_condition unlock];
/*
Other functional interfaces
wait enters the waiting state
waitUntilDate: Let a thread wait for a certain time
signal wakes up a waiting thread
broadcast wakes up all waiting threads
*/

We can see that:

  • NSCondition is an encapsulation of mutex and cond (cond is a pointer for accessing and manipulating specific types of data)
  • The wait operation will block the thread and make it sleep until it times out
  • The signal operation is to wake up a thread that is sleeping and waiting
  • broadcast will wake up all waiting threads

NSConditionLock

// initialization
NSConditionLock *_conditionLock = [[NSConditionLock alloc]init];
// lock
[_conditionLock lock];
// unlock
[_conditionLock unlock];
// Try to lock, if it can be locked, lock it immediately and return YES, otherwise return NO
[_conditionLock tryLock];
/*
Other functional interfaces
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER; //Initialize the incoming condition
- (void)lockWhenCondition:(NSInteger)condition;//The condition is established to trigger the lock
- (BOOL)tryLockWhenCondition:(NSInteger)condition;//Try condition to trigger lock
- (void)unlockWithCondition:(NSInteger)condition;//The condition is established to unlock
- (BOOL)lockBeforeDate:(NSDate *)limit;//Trigger lock within the waiting time
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;//Trigger lock when the condition is met and within the waiting time
*/
  • NSConditionLock is an encapsulation of NSCondition plus the number of threads
  • NSConditionLock can set lock conditions, while NSCondition is just a notification signal

Read-write lock

A read-write lock is actually a special spin lock, which divides the visitors to shared resources into readers and writers. Readers only have read access to shared resources, and writers need to write to shared resources. Compared with spin locks, this kind of lock can improve concurrency, because in a multiprocessor system, it allows multiple readers to access shared resources at the same time, and the maximum possible number of readers is the actual number of CPUs.
Writers are exclusive. A read-write lock can only have one writer or multiple readers (related to the number of CPUs) at the same time, but it cannot have both readers and writers at the same time. Preemption is also invalid during the read-write lock hold
If there are currently no readers and no writers for the read-write lock, then the writer can immediately acquire the read-write lock, otherwise it must spin there until there are no writers or readers. If there is no writer for the read-write lock, then the reader can obtain the read-write lock immediately, otherwise the reader must stay there until the writer releases the read-write lock.

// import header file
#import <pthread.h>
// normal initialization
// Globally declare a read-write lock
pthread_rwlock_t lock;
// Initialize read-write lock
pthread_rwlock_init( & amp; lock, NULL);
//Macro definition initialization
pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;

// read operation - lock
pthread_rwlock_rdlock( & amp; lock);
// read operation - try to lock
pthread_rwlock_tryrdlock( & lock);
// write operation - lock
pthread_rwlock_wrlock( & amp; lock);
// write operation - try to lock
pthread_rwlock_trywrlock( & amp; lock);
// unlock
pthread_rwlock_unlock( & amp; lock);
// release the lock
pthread_rwlock_destroy( & amp; lock);

Summary

  • OSSpinLock is no longer safe, and the underlying layer is replaced by os_unfair_lock
  • atomic can only guarantee thread safety for setters and getters, so nonatomic is used more to modify
  • Read-write locks are more implemented using fence functions
  • @synchronized maintains a hash list at the bottom layer for data storage, using recursive_mutex_t for locking
  • The underlying layers of NSLock, NSRecursiveLock, NSCondition and NSConditionLock are encapsulation of pthread_mutex
  • NSCondition and NSConditionLock are conditional locks, which can only be operated when a certain condition is met, similar to the semaphore dispatch_semaphore
  • Common scenarios involving thread safety can use NSLock
  • Use NSRecursiveLock when looping
  • When calling circularly and affected by threads, please pay attention to deadlock. If there is a deadlock problem, please use @synchronized