Today’s operating systems are basically multi-tasking, with a large number of schedulable entities running at the same time. In a multitasking operating system, multiple tasks running at the same time may:

  • Both need to access/use the same resource
  • There are dependencies between multiple tasks, and the operation of one task depends on another task.

Synchronization: Segments of a program that run between different tasks in strict accordance with a certain order of precedence. The most basic scenario is that multiple threads run in a coordinated fashion, in a predetermined order. For example, task A depends on the data generated by task B.

Mutual exclusion: a number of program segments that run between different tasks. When one task runs one of the segments, other tasks cannot run any of the segments between them until the task is complete. The basic scenario is that a common resource can only be used by one process at a time.

We can use locks to solve the synchronization and mutual exclusion of multiple threads, the basic locks include three types: mutex spin lock read and write lock, other such as conditional lock recursive lock semaphore is the upper encapsulation and implementation.

Mutex

Mutex is 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 accomplished by slicing code into critical sections one by one.

Mutex can be classified as recursive mutex and non-recursive mutex. The only difference is that the same thread can acquire the same recursive lock multiple times without causing a deadlock. If a thread acquires the same non-recursive lock more than once, a deadlock will occur.

  1. Mutex features:
  • atomicIf one thread locks a mutex, no other thread can successfully lock the mutex at the same time.
  • uniquenessIf a thread locks a mutex, no other thread can lock the mutex until it is unlocked.
  • Non-busy waiting: If one thread locks a mutex and a second thread tries to lock it, the second thread is suspended (without CPU usage) until the first thread releases the mutex, and the second thread wakes up and continues executing, locking the mutex.
  1. How mutex works:
  • Lock the mutex before accessing the post-critical region of the shared resource;
  • Once the mutex is locked, any other thread that tries to lock the mutex again will be blocked until the lock is released.
  • Release the lock on the mutex guide after the access is complete.
  1. Commonly used mutex
  • @synchronized
  • NSLock
  • NSRecursive

1.1 the pthread_mutex

#include <pthread.h>
#include <time.h>// Initialize a mutex. int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); // Lock the mutex. If it is already locked, the caller blocks until the mutex is unlocked. int pthread_mutex_lock(pthread_mutex_t *mutex); // If the mutex is not locked, the mutex is locked and returns 0; If the mutex is locked, the function returns a failure, EBUSY. int pthread_mutex_trylock(pthread_mutex_t *mutex); The pthread_mutex_timedLock mutex allows the bound thread to block for a time when the thread is trying to acquire a locked mutex. Non-blocking locking mutex. int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout); // Unlocks the specified mutex. int pthread_mutex_unlock(pthread_mutex_t *mutex); // Destroys the specified mutex. After a mutex is used, the mutex must be destroyed to release resources. int pthread_mutex_destroy(pthread_mutex_t *mutex);Copy the code

For pthread_mutex, the key is the type of lock.

  • PTHREAD_MUTEX_NORMAL: Does not provide deadlock detection. Attempting to re-lock the mutex results in a deadlock. If the mutex that a thread is trying to unlock is not locked or unlocked by the thread, indeterminate behavior will occur.
  • PTHREAD_MUTEX_ERRORCHECK: Provides error checking. An error is returned if the mutex that a thread tried to re-lock is already locked by that thread. An error is returned if a thread attempts to unlock a mutex that is not locked or unlocked by the thread.
  • PTHREAD_MUTEX_RECURSIVE: This mutex preserves the concept of a lock count. The lock count is set to 1 the first time a thread successfully acquires a mutex. Each time the thread relocks the mutex, the lock count increases by one. Each time a thread unlocks the mutex, the lock count decreases by one. When the lock count reaches zero, the mutex is available to other threads. An error is returned if a thread attempts to unlock a mutex that is not locked or unlocked by the thread.
  • PTHREAD_MUTEX_DEFAULT: Attempting to lock the mutex recursively results in indeterminate behavior. An attempt to unlock a mutex that is not locked by the calling thread will result in indeterminate behavior. An attempt to unlock a mutex that has not yet been locked results in indeterminate behavior.

1.2 @ synchronized

A convenient way to create a mutex that does everything other mutex does.

The object used by the @synchronized(object) command is the unique identifier of the lock. Only when the synchronized(object) identifier is the same, the lock is mutually exclusive. If you pass the same identifier in different threads, the first one to get the lock locks the code block and the other thread blocks. If you pass different identifiers, the thread will not block.

- (void)synchronized
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @synchronized(self) {
            sleep(2);
            NSLog(@Thread 1 "");
        }
        NSLog(@"Thread 1 unlocked successfully");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        @synchronized(self) {
            NSLog(@Thread 2 ""); }}); } to print: 2020-04-26 17:58:14.534038+0800 lock[3891:797979] thread 1 2020-04-26 17:58:14.534250+0800 lock[3891:797979] thread 1 unlocked 2020-04-26 17:58:14.534255+0800 lock[3891:797981] thread 2Copy the code

1.2.1 @ synchronized principle

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk  main.m
Copy the code

The pseudo-code compiled from @synchronized(obj)clang is as follows:

@try {
    objc_sync_enter(obj);
    // do work
} @finally {
    objc_sync_exit(obj);    
}
Copy the code

Enter objC4-756.2 source code

The data structure

typedef struct SyncData {
    id object;
    recursive_mutex_t mutex;
    struct SyncData* nextData;
    int threadCount;
} SyncData;

typedef struct SyncList {
    SyncData *data;
    spinlock_t lock;
} SyncList;

// Use multiple parallel lists to decrease contention among unrelated objects.
#define COUNT 16
#define HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))
#define LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock
#define LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].data
static SyncList sDataLists[COUNT];
Copy the code

SyncData structure:

  • The incomingobj
  • withobjThe associatedRecursive_mutex_t lock.
  • A pointer to another SyncData objectnextData, so you can think of each SyncData structure as a node in a linked list.
  • eachsyncDataThe lock in the object will be used or waited on by some thread,threadCountThat’s the number of threads at this point.SyncData structureWill be cached,threadCount= 0Indicates that the syncData instance can be reused.

SyncList structure:

  • theSyncDataThink of it as a node in a linked list, eachSyncListThey all have a pointSyncDataA pointer to the head of a linked list of nodes, which also has a lock to prevent concurrent modification of the list by multiple threads.

SDataLists struct array:

  • An array of SyncList structures of size 16. Maps an incoming object to a subscript on an array using a defined hash algorithm. It is worth noting that this hash algorithm is cleverly designed to convert the memory address of the object pointer to an unsigned integer and shift it five places to the right, followed by a bitwise sum of 0xF, so that the result does not exceed the size of the array.
  • LOCK_FOR_OBJ(obj) and LIST_FOR_OBJ(obj) : Hash the array subscript of the object, then extract the lock or data of the corresponding element of the array.LOCK_FOR_OBJ(obj)LIST_FOR_OBJ(obj)

When objc_sync_Enter (obj) is called, it looks for the appropriate SyncData using the hash of obJ’s memory address, and then locks it.

When objc_sync_exit(obj) is called, it looks for the appropriate SyncData and unlocks it.

objc_sync_enter

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
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)
);
Copy the code
  • If obj = nil, @synchronized(nil) does nothing
  • If obj has a value, runtime will be passed inobjAssign aRecursive lockingAnd store it in a hash table
  • objthroughid2data(obj, ACQUIRE)Encapsulated intoSyncData(obj)
  • Recursive locks do not generate deadlocks when acquired repeatedly by the same thread, so recursive locks cooperate with @synchronized(nil) to ensure that they do not generate deadlocks when acquired repeatedly by the same thread. Nil does not work, but @synchronized([NSNull null]) does.

1.2.2 the interview questions

  1. Question 1: What happens when the following code runs?
- (void)synchronizedTest
{
    self.testArray = [NSMutableArray array];
    
    for(NSInteger i = 0; i < 200000; i ++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ _testArray = [NSMutableArray array]; }); }}Copy the code
  • _testArrayContinuous in different threadsretain release, there will be a time when multiple threads simultaneously pair_testArrayforrelease“, causing a crash.
  1. Question 2: Yes@synchronizingLock the_testArrayAnd crash?
- (void)synchronizedTest
{
    self.testArray = [NSMutableArray array];
    
    for(NSInteger i = 0; i < 200000; i ++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ @synchronized (_testArray) { _testArray = [NSMutableArray array]; }}); }}Copy the code
  • @synchronized(nil) = do nothing

@synchronized isn’t as good as it should be when the locked object is nil. How can you fix that? Use NSLock.

{
    self.testArray = [NSMutableArray array];
    
    NSLock *lock = [[NSLock alloc] init];
    
    for(NSInteger i = 0; i < 200000; i ++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [lock lock]; _testArray = [NSMutableArray array]; [lock unlock]; }); }}Copy the code

1.3 NSLock

NSLock the attribute of the underlying pthread_mutex_lock implementation is PTHREAD_MUTEX_ERRORCHECK. Follow the NSLocking protocol.

@protocol NSLocking

- (void)lock;   
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit; @property (nullable, copy) NSString *name API_AVAILABLE(MacOS (10.5), ios(2.0), Watchos (2.0), TVOs (9.0)); @endCopy the code
  • lock: lock
  • unlock: unlock
  • tryLock: Attempts to lock, and returns NO if it fails
  • lockBeforeDate: Attempts to lock before the specified Date, or returns NO if the lock cannot be held before the specified time
- (void)nslock { NSLock *lock = [[NSLock alloc] init]; // thread 1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{[lock lock]; NSLog(@Thread 1 "");
            sleep(2);
            [lock unlock];
            NSLog(@"Thread 1 unlocked successfully"); }); // thread 2 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{sleep(1); // to ensure that thread 2's code executes after [lock lock]; NSLog(@Thread 2 ""); [lock unlock]; }); } to print: 2020-04-26 20:27:38.474856+0800 lock[6554:889229] thread 1 2020-04-26 20:27:38.474856+0800 lock[6554:889229] thread 1 unlocked 474880+0800 lock[6554:889230] thread 2Copy the code
  • The lock in thread 1 is locked, so the lock in thread 2 fails to lock, blocking thread 2. After 2 seconds, the lock in thread 1 is unlocked, and the lock in thread 2 succeeds immediately, executing the subsequent code in thread 2.

1.4 NSRecursiveLock

  1. The bottom of NSRecursiveLock is throughpthread_mutex_lockImplemented with the property ofPTHREAD_MUTEX_RECURSIVE.
  2. NSRecursiveLockNSLockThe difference is that NSRecursiveLock can be inSame threadNSRecursiveLock keeps track of how many times the lock is locked and unlocked. When the balance is reached, the lock will be released and other threads will be able to lock successfully.
@protocol NSLocking

- (void)lock;   
- (void)unlock;

@end

@interface NSRecursiveLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end
Copy the code
  1. Application scenarios
- (void)recursiveLock
{
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    
    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);
            }
            [lock unlock];
        };
        
        testMethod(10); }); } to print: 2020-04-26 21:40:24.390756+0800 Lock [6691:924076] Current Value = 10 2020-04-26 21:40:24.390875+0800 lock[6691:924076] Current value = 9 2020-04-26 21:40:24.390956+0800 LOCK [6691:924076] Current value = 8 2020-04-26 21:40:24.391043+0800 Lock [6691:924076] current value = 7 2020-04-26 21:40:24.391131+0800 Lock [6691:924076] current value = 6 2020-04-26 21:40:24.391211+0800 lock[6691:924076] current value = 5 2020-04-26 21:40:24.391295+0800 lock[6691:924076] current value = 4 2020-04-26 21:40:24.391394+0800 Lock [6691:924076] Current value = 3 2020-04-26 21:40:24.391477+0800 Lock [6691:924076] current value = 2 2020-04-26 21:40:24.391561+0800 Lock [6691:924076] current value = 1Copy the code
  • In the example above, if NSLock is used, the lock will be locked first, but when the lock is not performed, it will enter the next level of recursion to request the lock again, blocking the thread, the thread is blocked, naturally, the following unlock code will not execute, and the deadlock is formed. The NSRecursiveLock recursive lock is designed to solve this problem.

1.5 Summary of Mutex

@synchronized NSLock NSRecursiveLock NSRecursiveLock

  • For common thread safety scenarios, useNSLockCan be
  • Same thread recursion, useNSRecursiveLock
  • Multithreaded recursion, more attention to deadlock phenomenon, recommended use@synchronized(Essentially encapsulates recursive locks, but prevents some deadlocks. When used, note that the locked object cannot be nil.)

For example, the code below creates threads in a for loop and recurses in their respective threads, using @synchronized in a multi-thread + recursion case.

- (void)test
{
    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

Spin lock

The thread repeatedly checks that 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.

  1. Spin-locks function the same as mutex except that:
  • The mutex blocks and sleeps to free the CPU,
  • A spinlock block does not yield the CPU, but keeps busy until the lock is acquired.
  1. Application Scenarios:
  • Less in user mode, more in kernel
  • The lock is held for a short time, or less than two context switches.
  1. Spinlocks have a similar API to mutex, replacing mutex in pthread_mutex_xxx() with spin, as in pthread_spin_init().

  2. Spin-locks are not currently secure and may have priority flipping problems. Assume that there are three tasks A, B, and C to be executed and the shared resource S that requires mutually exclusive access. The priorities of the three tasks are A > B > C.

  • First, C is in the running state, the CPU is being executed, and it occupies the resource S.
  • Second, A enters the ready state. Because its priority is higher than THAT of C, A gets the CPU. A enters the running state. C Enters the ready state;
  • Third: the execution process needs to use resources, and this resource is occupied by the waiting C, so A enters the blocking state, C returns to the running state;
  • Fourth: At this time, B enters the ready state, because its priority is higher than C, B obtains the CPU and enters the running state. C returns to the ready state;
  • Fifth: if B2, B3 and other tasks appear at this time, their priority is higher than C, but lower than A, then there will be A strange phenomenon that the high-priority task A cannot be executed, but the low-priority task B, B2, B3 and other tasks can be executed, and this is priority inversion.

Atomic Underlying principle

Speaking of spinlocks, we have to mention the attribute modifier atomic.

1. The underlying principle of setter methods — reallySetProperty

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) { bool copy = (shouldCopy && shouldCopy ! = MUTABLE_COPY); bool mutableCopy = (shouldCopy == MUTABLE_COPY); reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy); } void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset) { reallySetProperty(self, _cmd, newValue, offset,true.false.false);
}

void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset) {
    reallySetProperty(self, _cmd, newValue, offset, false.false.false);
}

void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset) {
    reallySetProperty(self, _cmd, newValue, offset, true.true.false);
}

void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset) {
    reallySetProperty(self, _cmd, newValue, offset, false.true.false);
}
Copy the 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) {// Replace oldValue = *slot; *slot = newValue; }else{// Replace spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } objc_release(oldValue); }Copy the code
  • If the attribute is non-atomic: directnewValuereplaceoldValue
  • If the attribute is atomic: Create onespinlock_tType of lock and salt the lock. In a locked environmentnewValuereplaceoldValue.

2. Basic principle of getter methods – objc_getProperty

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
  • If non-atomic, return the value of the salt address directly
  • If atomic, the value is set in the lock environment

3. Spinlock_t & slotlock = PropertyLocks[slot] What type of lock is it?

Spinlock_t Sounds like a spinlock, but spinlocks are no longer secure. Let’s look at the definition of spinlock_t

using spinlock_t = mutex_tt<DEBUG>;
using mutex_locker_t = mutex_tt<LOCKDEBUG>::locker;
Copy the code

It appears that Apple has replaced spinlock_t with mutex_locker_t underneath. What is mutex_locker_t?

/ *! * @typedef os_unfair_lock * * @abstract * Low-level lock that allows waiters to block efficiently on contention. * * In general, higher level synchronization primitives such as those provided by * the pthread or dispatch subsystems should be preferred. * * The values storedin the lock should be considered opaque and implementation
 * defined, they contain thread ownership information that the system may use
 * to attempt to resolve priority inversions.
 *
 * This lock must be unlocked from the same thread that locked it, attemps to
 * unlock from a different thread will cause an assertion aborting the process.
 *
 * This lock must not be accessed from multiple processes or threads via shared
 * or multiply-mapped memory, the lock implementation relies on the address of
 * the lock value and owning process.
 *
 * Must be initialized with OS_UNFAIR_LOCK_INIT
 *
 * @discussion
 * Replacement for the deprecated OSSpinLock. Does not spin on contention but
 * waits in the kernel to be woken up by an unlock.
 *
 * As with OSSpinLock there is no attempt at fairness or lock ordering, e.g. an
 * unlocker can potentially immediately reacquire the lock before a woken up
 * waiter gets an opportunity to attempt to acquire the lock. This may be
 * advantageous for performance reasons, but also makes starvation of waiters a
 * possibility.
 */
OS_UNFAIR_LOCK_AVAILABILITY
typedef struct os_unfair_lock_s {
    uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;
Copy the code

Still have to praise the apple official note, too detailed.

  • Os_unfair_lock is a low-level lock that must be created in theOS_UNFAIR_LOCK_INITInitialize.
  • In general, a higher-level synchronization tool, such as one provided by the PThread or Dispatch subsystem, should be preferred.
  • The lock contains thread ownership information to solve the priority inversion problem
  • The lock must be locked from itsThe same threadUnlocking, attempting to unlock from another thread will cause an assertion to abort the process.
  • Cannot share or multimap memory fromMultiple processes or threadsAccess the lock, whose implementation depends on the lock value and the address of the owning process.
  • Used to replace the obsoleteOSSpinLock(Deprecated on iOS 10).
  • For performance reasons, the unlocker may reacquire the lock immediately before waking up.

Are atomic thread-safe?

Atomic locks the setter and getter methods of the property, generating atomic setters and getters. Atomicity means that if there are two threads executing the getter, thread A must wait for the getter to complete before executing the setter.

In short, atomic can only guarantee that code is safe inside a getter or setter function. Once both getters and setters are present, multithreading is up to the programmer. So atomic properties are not directly related to multithreading safety using @ properties.

For example, thread A and thread B both perform 10000 + 1 operations on the property num. If thread-safe, the value of num should be 20000 at the end of the program.

@property (atomic, assign) NSInteger num;

- (void)atomicTest {
    //Thread A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10000; i ++) {
            self.num = self.num + 1;
            NSLog(@"%@ -- %ld", [NSThread currentThread], (long)self.num); }}); //Thread B dispatch_async(dispatch_get_global_queue(0, 0), ^{for (int i = 0; i < 10000; i ++) {
            self.num = self.num + 1;
             NSLog(@"%@ -- %ld", [NSThread currentThread], (long)self.num); }}); 126996+0800 lock[10384:1662304] <NSThread: 0x600000EA3C00 >{Number = 3, name = (null)} -- 19994 2020-04-28 16:35:55.127083+0800 lock[10384:1662299] <NSThread: 0x600000EECDC0 >{number = 5, name = (null)} -- 19995 2020-04-28 16:35:55.127165+0800 Lock [10384:1662304] <NSThread: 0x600000EA3C00 >{number = 3, name = (null)} -- 19996 2020-04-28 16:35:55.127250+0800 lock[10384:1662299] <NSThread: 0x600000EecDC0 >{number = 5, name = (null)} -- 19997 2020-04-28 16:35:55.127341+0800 lock[10384:1662304] <NSThread: 0x600000ea3c00>{number = 3, name = (null)} -- 19998Copy the code

Self. num = self.num + 1

  • On the left hand side of the equals sign, self.num calls the setter method, which is atomic
  • On the right-hand side of the equals sign, self.num calls the getter method for atomic properties
  • But self.num + 1 is not an atomic attribute, so there are threading problems.

In addition, atomic will consume more resources and perform less well because it locks the property, up to 20 times slower than nonatomic. So for iOS mobile development, we usually use nonatomic. But in MAC development, atomic makes sense.

Read/write lock

Read/write lock is actually a special kind of spin lock. It divides the visitors to the 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.

  1. Read/write locks are similar to mutex, but they allow for parallelism of changes, also known as shared mutex.
  • A mutex is either locked or unlocked, and only one thread can lock it at a time.
  • Read/write locks can have three states:Lock status in read mode,Write mode Lock status,Unlocked state.
  1. Read/write lock features:Read write
  • Only one thread can hold the read-write lock in write mode at a time, but more than one thread can hold the read-write lock at the same time. And because of that,
  • 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-locked state, all threads attempting to lock it in read-mode gain access, – but if a thread wishes to lock the lock in write mode, it must wait until all threads release the lock.
  1. Apis for reading and writing locks:
#include <pthread.h>Int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); Int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); Int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // Try to obtain a write lock on a read/write lock in a non-blocking manner. If any reader or writer holds the lock, return immediately on failure. int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); Int pthread_rwlock_unlock (pthread_rwlock_t *rwlock); Int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);Copy the code
  1. Application Scenarios:
  • Read/write locks are suitable for situations where data structures are read much more often than written.
@property (nonatomic, strong) dispatch_queue_t concurrent_queue; @property (nonatomic, strong) NSMutableDictionary *dataCenterDic; - (void)readWriteTest
{
    self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
    self.dataCenterDic = [NSMutableDictionary dictionary];
    
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT); // Simulate multi-threaded writingfor (NSInteger i = 0; i < 5; i ++) {
        dispatch_async(queue, ^{
            [self ak_setObject:[NSString stringWithFormat:@"akironer--%ld", (long)i] forKey:@"Key"]; }); } // Simulate reading with multiple threadsfor (NSInteger i = 0; i < 20; i ++) {
        dispatch_async(queue, ^{
            [self ak_objectForKey:@"Key"]; }); } // Simulate multi-threaded writingfor (NSInteger i = 0; i < 10; i ++) {
        dispatch_async(queue, ^{
            [self ak_setObject:[NSString stringWithFormat:@"iOS--%ld", (long)i] forKey:@"Key"]; }); }}#pragma mark - Reads data- (id)ak_objectForKey:(NSString *)key { __block id obj; // Dispatch_sync (self.concurrent_queue, ^{obj = [self.datacenterdic objectForKey:key]; NSLog(@"Reading" % @ % @", obj, [NSThread currentThread]);
        sleep(1);
    });
    return obj;
}

#pragma mark - Writes data
- (void)ak_setObject:(id)obj forKey:(NSString *) Key {// Async fence calls set data: mask synchronous dispatch_barrier_async(self.concurrent_queue, ^{[self.datacenterdicsetObject:obj forKey:key];
        NSLog(@"Write: % @ % @", obj, [NSThread currentThread]);
        sleep(1);
    });
}
Copy the code

Conditional lock

  1. Unlike mutex, conditional locks are used for waiting rather than locking. Conditional locks are used to automatically block a thread until a special condition occurs. Conditional and mutex locks are usually used together.

  2. Conditional locking is a mechanism for synchronization using global variables shared between threads. It allows us to sleep and wait for a condition to occur. It involves two actions:

  • A thread is suspended waiting for “condition of conditional lock held”;
  • Another thread makes “condition true” (gives a condition true signal).
  1. Three elements of conditional locking:
  • Mutex: Protects data sources while detecting conditions and performs tasks triggered by conditions
  • Condition variable: The basis for determining whether a condition is met
  • Conditional detection variable: Determines whether to continue running a thread based on a condition, that is, whether the thread is blocked

4.1 NSCondition Condition variable

  1. NSConditionThrough the bottom ofpthread_cond_tThe implementation. The NSCondition object actually acts as a lock and a thread inspector
  • Lock: Protects data sources when detecting conditions and performs tasks triggered by conditions;
  • Thread inspector: Decides whether to continue running a thread based on conditions, that is, whether the thread is blocked
  1. NSConditionTo achieve theNSLocking agreementWhen multiple threads access the same code, thewaitIs the watershed. One thread waits for another threadunlockAnd then you gowaitThe code after that.
@protocol NSLocking

- (void)lock;   
- (void)unlock;

@end

@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end
Copy the code
  • 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
  • unlock: unlock
  • wait: Makes the current thread wait
  • signal: Notifies any thread
  • broadcast: Notifies all waiting threads
  1. Application scenario: Producer-consumer model:
  • In production mode, quantity of goods + 1
  • In consumption mode, quantity of goods is -1
  • How to ensure that the quantity of goods in consumption mode is greater than zero?
- (void)testConditon { self.testCondition = [[NSCondition alloc] init]; // Create production-consumerfor(int i = 0; i < 10; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self producer]; / / production}); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self consumer]; / / consumer}); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self consumer]; / / consumer}); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self producer]; / / production}); } } - (void)producer { [self.testCondition lock]; self.ticketCount = self.ticketCount + 1; NSLog(@"Produce an existing count %zd",self.ticketCount); [self.testCondition signal]; [self.testCondition unlock]; } - (void)consumer {// thread safety [self.testCondition lock];while (self.ticketCount == 0) {
        NSLog(@"Wait for count %zd",self.ticketCount); // Ensure normal flow [self.testConditionwait]; Self.ticketcount -= 1; self.ticketcount -= 1; NSLog(@"Consume one left count %zd",self.ticketCount); [self.testCondition unlock]; } to print: 2020-04-27 17:46:43.232762+0800 LOCK [7444:1140032] produce an existing count 1 2020-04-27 17:46:43.232900+0800 lock[7444:1140032] Create an existing count 2 2020-04-27 17:46:43.233001+0800 lock[7444:1140032] consume a remaining count 1 2020-04-27 17:46:43.233109+0800 Lock [7444:1140066] consume a remaining count 0 2020-04-27 17:46:43.233209+0800 lock[7444:1140070] wait for count 0 2020-04-27 17:46:43.233308+0800 lock[7444:1140030] wait for count 0 2020-04-27 17:46:43.233406+0800 lock[7444:1140057] wait for count 0 2020-04-27 17:46:43.233508+0800 LOCK [7444:1140058] Produce an existing Count 1 2020-04-27 17:46:43.233611+0800 lock[7444:1140070] Lock [7444:1140059] wait for count 0 2020-04-27 17:46:43.234100+0800 Lock [7444:1140061] produces an existing count 1 2020-04-27 17:46:43.234343+0800 lock[7444:1140030] consumes an existing count 0Copy the code

4.2NSconditionlock conditional lock

  1. NSConditionLock is implemented with the help of NSCondition, which is essentially a producer-consumer model. NSConditionLock internally holds an NSCondition object and the _condition_value attribute, which is assigned at initialization.

  2. NSConditionLock implements the NSLocking protocol. A thread waits for another thread to unlock or unlock withcondition: before moving lock or lockWhenCondition:.

  3. Compared with NSCondition, NSConditonLock comes with a conditional detection variable, which is more flexible to use.

@protocol NSLocking

- (void)lock;   
- (void)unlock;

@end

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit; @property (nullable, copy) NSString *name API_AVAILABLE(MacOS (10.5), ios(2.0), Watchos (2.0), TVOs (9.0)); @endCopy the code
  • lock: indicates that XXX 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
  • condition: Internal condition. This property is very important,External conditionInternal conditionThe lock object will be obtained only if the same; Instead, block the current thread until conditions are the same
  • lockWhenCondition:(NSInteger)conditionA: indicates that if no other thread obtains the lock, the internal condition of the lock is not equal to condition A, the lock cannot be obtained, and the lock is still waiting. If condition inside the lock is equal to condition A, it enters the code area and sets it to acquire the lock, and any other thread will wait for its code to complete until it unlocks.
  • unlockWithCondition:(NSInteger)conditionA: releases the lock and sets the internal condition to condition A
  • return = lockWhenCondition:(NSInteger)conditionA beforeDate:(NSDate *)limitA: indicates that the thread is no longer blocked if it is locked (the lock is not acquired) and time A expires. 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
- (void)testConditonLock
{
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
       [conditionLock lockWhenCondition:1];
       NSLog(@Thread 1 "");
       [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       [conditionLock lockWhenCondition:2];
       NSLog(@Thread 2 "");
       [conditionLock unlockWithCondition:1];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       [conditionLock lock];
       NSLog(@"Thread 3"); [conditionLock unlock]; }); } to print: 2020-04-27 18:00:21.876356+0800 lock[7484:1148383] thread 3 2020-04-27 18:00:21.876629+0800 lock[7484:1148384] thread 2 2020-04-27 18:00:21.876751+0800 lock[7484:1148386] thread 1Copy the code
  • Thread 1 call[NSConditionLock lockWhenCondition:1]Because the current condition is not met, the state will enter waiting. When the current state enters waiting, the current mutex will be released.
  • Now thread 3 calls[NSConditionLock lock], essentially calling [NSConditionLock lockBeforeDate:], there is no need to compare conditions, so thread 3 prints
  • Next thread 2 executes[NSConditionLock lockWhenCondition:2], thread 2 prints, and when it’s done, it calls[NSConditionLock unlockWithCondition:1]Set value to 1 and send boradcast
  • Thread 1 receives the current signal, wakes up to execute and prints.
  • The current print is thread 3-> Thread 2 -> thread 1.
  • [NSConditionLock lockWhenCondition:]The condition and Value values passed in are compared, and if they are not equal, the thread pool is blocked, otherwise code execution continues
  • [NSConditionLock unlockWithCondition:]The current value is changed, and then the current thread is broadcast to wake up.

5. Semaphore

Widely used for synchronization and mutual exclusion between processes or threads, a semaphore is essentially a non-negative integer counter that is used to control access to a common resource.

#include <semaphore.h>Int sem_init(sem_t *sem, int pshared, unsigned int value); // semaphore P operation (minus 1) int sem_wait(sem_t *sem); Int sem_trywait(sem_t *sem); sem_trywait(sem_t *sem); // semaphore V operation (+ 1) int sem_post(sem_t *sem); Int sem_getValue (sem_t *sem, int *sval); Int sem_destroy(sem_t *sem);Copy the code

GCD dispatch_semaphore, can refer to iOS Advanced Path (16) multi-threaded – GCD

Six: summarize

In ibireme’s OSSpinLock, which is no longer safe, various locks were tested (unlocked immediately after locking, without counting the time spent in contention).

  • OSSpinLockThe highest performance, but it is no longer safe.
  • @synchronizedWith this article, @synchronized is no longer the first to lock.

The resources

Cooci — Eight locks in iOS

Bestswifter — In-depth understanding of locks in iOS development

There’s more than you need to know about @synchronized