Thread safety issues – locks

Common types of locks are:

  • 1, OSSpinLock
  • 2, os_unfair_lock
  • 3, the pthread_mutex
  • 4, dispatch_semaphore
  • 5, dispatch_queue (DISPATCH_QUEUE_SERIAL)
  • 6, NSLock
  • 7, NSRecursiveLock
  • 8 NSCondition.
  • 9 NSConditionLock.
  • 10, @ synchronized
  • 11, pthread_rwlock
  • 12, dispatch_barrier_async
  • 13, atomic

The types of lock

spinlocks

OSSpin Lock: busy, etc

Spinlocks are similar to mutex, except that they do not cause the caller to sleep. If the spinlock has been held by another execution unit, the caller loops around to see if the holder of the spinlock has released the lock, hence the name “spin”.

It is used to solve the mutually exclusive use of a resource. Because spinlocks do not cause the caller to sleep, they are far more efficient than mutex.

While it is more efficient than mutex, it has some drawbacks:

1. Spin lock occupies CPU all the time. It keeps running without a lock, so it occupies CPU.

2. It is possible to cause a deadlock when using a spin lock, it is possible to cause a deadlock when using a recursive call, and it is possible to cause a deadlock when calling some other functions, such as copy_to_user(), copy_from_user(), kmalloc(), and so on.

Therefore, we should use spin locks with caution. Spin locks are only really needed if the kernel is preemptible or SMP. In a single-CPU, non-preemptible kernel, the operation of spin locks is empty. Spin locks are suitable for situations where the lock user holds the lock for a short time.

The mutex

The mutex is a lock of the sleep-waiting type.

Ensure that only one thread accesses the object at any time. When the lock operation fails, the thread goes to sleep and wakes up waiting for the lock to be released.

For example, on A dual-core machine you have two threads (thread A and thread B) running on Core0 and Core1, respectively. Suppose thread A wants to use the pthread_mutex_lock operation to obtain A critical block lock, and the lock is held by thread B, so thread A is blocking. Core0 performs A Context Switch at this point to place thread A on the wait queue, at which point Core0 can run other tasks (such as another thread C) without busy waiting.

If thread A uses the pthread_spin_lock operation to request the lock, thread A will keep busy waiting on Core0 until it gets the lock.

Two kinds of locking principle

Spin lock: the thread is always running(lock — > unlock), an infinite loop to detect the lock flag, the mechanism is not complex.

Mutex: Threads go from sleep — >running, with overhead of context switching, CPU preemption, signal sending, etc.

Contrast: the initiation of the mutex original cost is higher than the spin lock, but the basic is once and for all, critical region the size of the lock time will not affect the cost of the mutex, and spin lock is infinite loop testing, lock the entire consume CPU, although initial cost is lower than the mutex, but with the lock time, lock overhead is linear growth.

Two kinds of lock application

Mutex is used for operations where critical sections are held for a long time, such as the following

  • 1 I/O operations are performed on critical sections
  • 2. The code of critical section is complex or the amount of loop is large
  • The competition in critical area is very fierce
  • 4 Single-core PROCESSOR

As for spin lock, it is mainly used in the case that the critical region holding lock time is very short and CPU resources are not tight, and spin lock is generally used in multi-core servers.

The use of the lock

Here is an example to illustrate the use of locks:

(void)ticketTest{self.ticketsCount = 50; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); for (NSInteger i = 0; i < 5; i++) { dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self sellingTickets]; }}); }}} - (void)sellingTickets{int oldMoney = self.ticketscount; sleep(.2); oldMoney -= 1; self.ticketsCount = oldMoney; NSLog(@" current remaining votes -> %d", oldMoney); }Copy the code
1, OSSpinLock

OSSpinLock is called “spin lock” and is used by importing the header #import

OSSpinLock lock = OS_SPINLOCK_INIT; / / lock OSSpinLockLock (& lock); / / unlock OSSpinLockUnlock (& lock);Copy the code
#import "OSSpinLockDemo.h" #import <libkern/OSAtomic.h> @interface OSSpinLockDemo() @property (assign, nonatomic) OSSpinLock ticketLock; @end @implementation OSSpinLockDemo - (instancetype)init { self = [super init]; if (self) { self.ticketLock = OS_SPINLOCK_INIT; } return self; } // sellingTickets - (void)sellingTickets{OSSpinLockLock(&_ticketLock); [super sellingTickets]; OSSpinLockUnlock(&_ticketLock); } @endCopy the code

OSSpinLock has been deprecated since iOS10.0 and can be replaced with os_unfair_lock.

(OSSpinLock that is no longer secure) priority inversion issues may occur

2, os_unfair_lock

Os_unfair_lock is used to replace unsafe osspinlocks, and is not supported until iOS10. The threads waiting for the os_unfair_lock lock are in sleep state.

Os_unfair_lock LOCK = OS_UNFAIR_LOCK_INIT; / / lock os_unfair_lock_lock (& lock); / / unlock os_unfair_lock_unlock (& lock);Copy the code
#import "os_unfair_lockDemo.h" #import <os/lock.h> @interface os_unfair_lockDemo() @property (assign, nonatomic) os_unfair_lock ticketLock; @end @implementation os_unfair_lockDemo - (instancetype)init { self = [super init]; if (self) { self.ticketLock = OS_UNFAIR_LOCK_INIT; } return self; } - (void)sellingTickets{os_unFAIR_lock_lock (&_ticketlock); [super sellingTickets]; os_unfair_lock_unlock(&_ticketLock); } @endCopy the code
3, the pthread_mutex

Mutex is called “mutex,” and the thread waiting for the lock goes to sleep. #import

1. Initialize the lock attribute pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); /* * Mutex type attributes */ #define PTHREAD_MUTEX_NORMAL 0 #define PTHREAD_MUTEX_ERRORCHECK 1 #define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL pthread_mutex_init(mutex, &attr); Pthread_mutexattr_destroy (&attr); Pthread_mutex_lock (&_mutex); pthread_mutex_unlock(&_mutex); Pthread_mutex_destroy (&_mutex); Note: We can pass NULL to use the default PTHREAD_MUTEX_NORMAL attribute without initializing the attribute. pthread_mutex_init(mutex, NULL);Copy the code
#import "pthread_mutexDemo.h" #import <pthread.h> @interface pthread_mutexDemo() @property (assign, nonatomic) pthread_mutex_t ticketMutex; @end @implementation pthread_mutexDemo - (instancetype)init { self = [super init]; If (self) {// Initializes the attribute pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT); // Initialize lock pthread_mutex_init(&(_ticketMutex), &attr); Pthread_mutexattr_destroy (&attr); } return self; } // sellingTickets - (void)sellingTickets{pthread_mutex_lock(&_ticketmutex); [super sellingTickets]; pthread_mutex_unlock(&_ticketMutex); } @endCopy the code

Deadlock may be caused, modify the code as follows

- (void)sellingTickets{pthread_mutex_lock(&_ticketmutex); [super sellingTickets]; [self sellingTickets2]; pthread_mutex_unlock(&_ticketMutex); } - (void)sellingTickets2{ pthread_mutex_lock(&_ticketMutex); NSLog(@"%s",__func__); pthread_mutex_unlock(&_ticketMutex); } the code above causes a thread deadlock, because the end of the method sellingTickets2 needs to be unlocked, and the end of the method sellingTickets2 needs to be unlocked. Cross-referencing causes a deadlockCopy the code

But there is an attribute in pthread_mutex_t that solves this problem: PTHREAD_MUTEX_RECURSIVE

Another solution is to create a new lock in the sellingTickets2 method. The two methods have different lock objects so that the thread does not deadlock.

PTHREAD_MUTEX_RECURSIVE lock: Allows the same thread to repeatedly lock the same lock. Focus on the same thread and the same lock

- (instancetype)init { self = [super init]; If (self) {// Initializes the attribute pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // Initialize lock pthread_mutex_init(&(_ticketMutex), &attr); Pthread_mutexattr_destroy (&attr); } return self; }Copy the code

Pthread_mutexes conditions

// Initialize the attribute pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // Initialize lock pthread_mutex_init(&_mutex, &attr); Pthread_mutexattr_destroy (&attr); Pthread_cond_t condition pthread_cond_init(&_cond, NULL); Pthread_cond_wait (&_cond, &_mutex); // Activate a thread waiting for the condition pthread_cond_signal(&_cond); // Activate all threads waiting for the condition pthread_cond_broadcast(&_cond); Pthread_mutex_destroy (&_mutex); pthread_cond_destroy(&_cond);Copy the code

Use case: Suppose we have an array, there are two threads, one is to add the array, one is to delete the array, we call delete the array, in the call to add the array, but when the array is empty, do not call delete the array.

#import "pthread_mutexDemo1.h" #import <pthread.h> @interface pthread_mutexDemo1() @property (assign, nonatomic) pthread_mutex_t mutex; @property (assign, nonatomic) pthread_cond_t cond; @property (strong, nonatomic) NSMutableArray *data; @end@implementation pthread_mutexDemo1 - (instanceType)init {if (self = [super init]) {// initialize attribute pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // Initialize lock pthread_mutex_init(&_mutex, &attr); Pthread_mutexattr_destroy (&attr); Pthread_cond_init (&_cond, NULL); self.data = [NSMutableArray array]; } return self; } - (void)otherTest { [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start]; } // thread 1 // remove elements from array - (void)__remove {pthread_mutex_lock(&_mutex); NSLog(@"__remove - begin"); Pthread_cond_wait (&_cond, &_mutex); pthread_cond_wait(&_cond, &_mutex); } [self.data removeLastObject]; NSLog(@" delete element "); pthread_mutex_unlock(&_mutex); } // thread 2 // add elements to array - (void)__add {pthread_mutex_lock(&_mutex); sleep(1); [self.data addObject:@"Test"]; NSLog(@" added element "); // Activate a thread waiting for the condition pthread_cond_signal(&_cond); Pthread_cond_broadcast (&_condition); pthread_cond_broadcast(&_condition); pthread_mutex_unlock(&_mutex); } - (void)dealloc { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); }Copy the code
4, NSLock

NSLock is a wrapper around a mutex normal lock. pthread_mutex_init(mutex, NULL);

NSLock complies with the NSLocking protocol. Lock means to Lock, unlock means to unlock, tryLock means to try to Lock, and return NO if it fails, lockBeforeDate: attempts to Lock before the specified Date, and returns NO if the Lock cannot be held before the specified time

@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
@end
Copy the code
#import "lockdemo.h" @interface LockDemo() @property (strong, nonatomic) NSLock *ticketLock; @end@implementation LockDemo - (void)sellingTickets{[self.ticketLock lock]; [super sellingTickets]; [self.ticketLock unlock]; } @endCopy the code
5, NSRecursiveLock

NSRecursiveLock encapsulates a mutex recursive lock, and has the same API as NSLock

#import "RecursiveLockDemo.h" @interface RecursiveLockDemo() @property (nonatomic,strong) NSRecursiveLock *ticketLock; @end@implementation RecursiveLockDemo - (void)sellingTickets{[self.ticketLock lock]; [super sellingTickets]; [self.ticketLock unlock]; } @endCopy the code
6, NSCondition

NSCondition is the encapsulation of MUtex and COND. It is more object-oriented and more convenient and concise to use

@interface NSCondition : NSObject <NSLocking> {
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@property (nullable, copy) NSString *name 
@end
Copy the code

So in the case of the array operation above we can look like this

- (void)__remove {[self.condition lock]; If (self.data.count == 0) {// wait [self.condition wait]; } [self.data removeLastObject]; NSLog(@" delete element "); [self.condition unlock]; } // thread 2 // Add elements to array - (void)__add {[self.condition lock]; sleep(1); [self.data addObject:@"Test"]; NSLog(@" added element "); [self. Condition signal]; [self.condition unlock]; }Copy the code
7, NSConditionLock

NSConditionLock is a further encapsulation of NSCondition, and you can set specific conditional values

@interface NSConditionLock : NSObject <NSLocking> {

- (instancetype)initWithCondition:(NSInteger)condition;

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

There are three common approaches

  • 1.InitWithCondition:Initialize theConditionAnd sets the status value
  • 2,lockWhenCondition:(NSInteger)condition:The lock is added when the status is condition
  • 3,unlockWithCondition:(NSInteger)conditionUnlock when the status value is condition
@interface NSConditionLockDemo()
@property (strong, nonatomic) NSConditionLock *conditionLock;
@end
@implementation NSConditionLockDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    }
    return self;
}

- (void)otherTest
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
}

- (void)__one
{
    [self.conditionLock lock];
    NSLog(@"__one");
    sleep(1);
    [self.conditionLock unlockWithCondition:2];
}

- (void)__two
{
    [self.conditionLock lockWhenCondition:2];
    NSLog(@"__two");
    [self.conditionLock unlockWithCondition:3];
}
@end
Copy the code
8 dispatch_semaphore.
  • A semaphore is called a semaphore.
  • The initial value of a semaphore that can be used to control the maximum number of concurrent accesses by a thread
  • The initial value of the semaphore is 1, which means that only one thread is allowed to access the resource at the same time to ensure thread synchronization
dispatch_semaphore_create(5); // If the semaphore value is <= 0, it will wait until the semaphore value is >0, then it will decrease the semaphore value by 1. Then proceed to dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER); // dispatch_semaphore_signal(self.semaphore);Copy the code
@interface dispatch_semaphoreDemo() @property (strong, nonatomic) dispatch_semaphore_t semaphore; @end @implementation dispatch_semaphoreDemo - (instancetype)init { if (self = [super init]) { self.semaphore = dispatch_semaphore_create(1); } return self; } - (void)otherTest { for (int i = 0; i < 20; i++) { [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start]; }} - (void)test {// If the semaphore is <= 0, then the semaphore is set to <= 0, then the semaphore is set to <= 0. Then proceed to dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER); sleep(2); NSLog(@"test - %@", [NSThread currentThread]); // dispatch_semaphore_signal(self.semaphore); } @end print finds that print occurs every second. Although we have 20 threads open at the same time, only one thread's resources can be accessed at a timeCopy the code
9 dispatch_queue.

It is also possible to achieve thread synchronization using serial queues directly from GCD

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); Dispatch_sync (queue, ^{// add task 1 for (int I = 0; i < 2; ++i) { NSLog(@"1---%@",[NSThread currentThread]); }}); Dispatch_sync (queue, ^{// add task 2 for (int I = 0; i < 2; ++i) { NSLog(@"2---%@",[NSThread currentThread]); }});Copy the code
10, @ synchronized

@synchronized is the encapsulation of a mutex recursive lock,

@synchronized(obj) internally generates a recursive lock corresponding to OBj, and then locks and unlocks the lock

// sellingTickets - (void)sellingTickets{@synchronized ([self class]) {[super sellingTickets]; }}Copy the code

Synchronized, which can be found in objC4’s objc-sync.mm file, starts and ends by calling objc_sync_enter and objc_sync_exit methods.

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

Find a data object using the id2data method, and then perform the mutex.lock() operation on the data object. Enter the id2data method to continue the search

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

Discover that retrieving data objects is based on the sDataLists[obj].data method, which is a hash table.

There’s more to @synchronized than you ever wanted to know

@synchronized(ID) parameter problem

What happens when the following code is executed?

- (void)lg_crash{ for (int i = 0; i < 200000; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ _testArray = [NSMutableArray array]; }); }}Copy the code
  • Initialize _testArray multiple times in multiple threads.
  • _testArray = [NSMutableArray array];The essence is to callsetterMethods; whilesetterThe method requires the values ofreleae, for the new valueretain; In multi-threaded operation_testArraythesetterMethod, which can occur multiple times on the same old value without ensuring thread-safetyrelease, which causes the appearance of wild Pointers, which causes the program to crash.

First modification:

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

As analyzed above, _testArray may become nil at some point, so through the previous analysis of @synchronized principle and multi-threading processing, when we use @synchronized, we need to lock _testArray first and then unlock it. When _testArray becomes nil, the operation in multi-thread will cause @synchronized to pass in a parameter of nil and call objc_sync_nil, thus losing the effect of locking and resulting in a crash.

So when we use @synchronized to secure multithreaded data, we need to ensure that nothing passed in is nil like the example above. There will be no problem if you change it to the following:

- (void)lg_crash{ for (int i = 0; i < 200000; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ @synchronized (self) { _testArray = [NSMutableArray array]; }}); }}Copy the code
11, atomic
  • Atomic is used to guarantee attributesAtomic operations on setters and getters, which is equivalent toLock thread synchronization inside getter and setter
  • See objC4’s objc-accessors.mm
  • It does not guarantee that the process of using attributes is thread-safe
//objc-accessors.mm id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { if (offset == 0) { return object_getClass(self); } // Retain Release world // If not atomic, return id* slot = (id*) ((char*)self + offset); if (! atomic) return *slot; // Atomic retain Release world // If Atomic, lock 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); } 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 it is not atomic, set newValue if (! atomic) { oldValue = *slot; *slot = newValue; } else {// If atomic, use spinlock_t spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } objc_release(oldValue); }Copy the code

Atomic will have one in the getter/setter of the objectspinlock_tControl. If thread A and thread B want to execute A settet while thread A is performing A getter, thread B must wait for thread A to complete the getter before executing it

Q: Are attributes modified by atomic completely safe?

  • Atomic only guarantees that the set/ GET method is safe, but it is no longer safe when multiple threads do not use the set/ GET method.

  • So there is no direct connection between atomic properties and multithreaded property safety

Atomic’s performance is said to be 20 times slower than nonatomic’s due to its use of spin locks, but this is not reflected in the source code

Pthread_rwlock: read/write lock

Pthread_rwlock is often used for reading and writing files, such as files.

Note the following scenarios in the iOS read and write security scheme :(multiple read and single write)

  • 1. Only one thread can write data at a time
  • 2. Multiple threads are allowed to read data at the same time
  • 3. Both write and read operations are not allowed at the same time
// Initialize lock pthread_rwlock_t lock; pthread_rwlock_init(&_lock, NULL); Pthread_rwlock_rdlock (&_lock); Pthread_rwlock_trywrlock (&_lock); pthread_rwlock_trywrlock(&_lock); // Write attempt to lock pthread_rwlock_trywrlock(&_lock) // unlock pthread_rwlock_unlock(&_lock); / / destroyed pthread_rwlock_destroy (& _lock);Copy the code
#import <pthread.h> @interface pthread_rwlockDemo () @property (assign, nonatomic) pthread_rwlock_t lock; @end @implementation pthread_rwlockDemo - (instancetype)init { self = [super init]; If (self) {// Initialize the lock pthread_rwlock_init(&_lock, NULL); } return self; } - (void)otherTest{ dispatch_queue_t queue = dispatch_get_global_queue(0, 0); for (int i = 0; i < 10; i++) { dispatch_async(queue, ^{ [self read]; }); dispatch_async(queue, ^{ [self write]; }); } } - (void)read { pthread_rwlock_rdlock(&_lock); sleep(1); NSLog(@"%s", __func__); pthread_rwlock_unlock(&_lock); } - (void)write { pthread_rwlock_wrlock(&_lock); sleep(1); NSLog(@"%s", __func__); pthread_rwlock_unlock(&_lock); } - (void)dealloc { pthread_rwlock_destroy(&_lock); } @end can see that read 1s May occur more than once, but write does notCopy the code
dispatch_barrier_async

The incoming queue must be created by dispatch_queue_cretate itself. If the incoming queue is a serial queue or a global queue, this function is equivalent to dispatch_async

Queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT); Dispatch_async (self.queue, ^{}); Dispatch_barrier_async (self.queue, ^{});Copy the code

Lock performance comparison

Sort performance from highest to lowest

  • 1, os_unfair_lock
  • 2, OSSpinLock
  • 3, dispatch_semaphore
  • 4, the pthread_mutex
  • 5, dispatch_queue (DISPATCH_QUEUE_SERIAL)
  • 6, NSLock
  • 7, NSCondition
  • 8, the pthread_mutex (recursive)
  • 9 NSRecursiveLock.
  • 10, NSConditionLock
  • 11, @ synchronized

If there is any mistake above, welcome to correct. Please indicate the source of reprint.