Introduction to iOS Multi-Thread Locks

Keywords: Mac iOS Linux Unix

How does thread security come about?

Common examples are the operation of a non-thread-safe variable outside a thread within a thread. Thread security and synchronization must be considered at this time.

- (void)getIamgeName:(NSMutableArray *)imageNames{//Suppose every entry is a thread.
    /*1.imageNames It is a variable outside the thread. Thread safety should be considered at this time.
    Because if our current number of imageNames is 1, threads A and B come in at the same time and find that the number is greater than 0.
    All of them will execute the remove operation, and a thread will surely crash as a result.
    */
    /*2.NSMutableArray *array = [[NSMutableArray alloc]initWithArray:imageNames];
    If an array is generated here, then the image Names are replaced by arrays instead of thread safety.
    But the array.count judgment is always greater than 0, that is, it is always equal to imageNames.count.
     */
    NSString *imageName;
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
}

Here's the synchronization scheme for locks

The Concept of Lock

Locks are the most commonly used synchronization tools. A segment of code can only be accessed by one thread at the same time. For example, after a thread A enters the lock code, another thread B cannot access it because it has been locked. Only when the lock code is unlocked after the previous thread A executes the lock code, can the B thread access the lock code.
Don't put too many other operation codes in it, otherwise, when one thread executes, another thread is waiting all the time, and it can't play the role of multi-threading.

NSLock

A simple mutex is implemented in NSLock of Cocoa program and NSLocking protocol is implemented.
Lock, lock
Unlock, unlock
tryLock, try to lock. If it fails, it will not block the thread, but return immediately.
NOlockBeforeDate:, temporarily blocks the thread before the specified date (if no lock is acquired), if the lock has not been acquired by the expiration date, the thread is awakened, and the function returns NO immediately.
Using tryLock does not succeed in locking, and if the acquisition of the lock fails, the locking code will not be executed.

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    [lock unlock];
}

@ synchronized code block

The first thread lock that every iOS developer comes into contact with is @synchronized, and the code is simple.

- (void)getIamgeName:(int)index{
    NSString *imageName;
    @synchronized(self) {
        if (imageNames.count>0) {
            imageName = [imageNames lastObject];
            [imageNames removeObject:imageName];
        }
    }
}

Conditional semaphore dispatch_semaphore_t

The semaphore in dispatch_semaphore_tGCD can also solve the problem of resource preemption and support signal notification and signal waiting. Whenever a signal notification is sent, the semaphore + 1; whenever a waiting signal is sent, the semaphore - 1; if the semaphore is zero, the signal will be in a waiting state until the semaphore is greater than 0 to start execution.

#import "MYDispatchSemaphoreViewController.h"

@interface MYDispatchSemaphoreViewController ()
{
    dispatch_semaphore_t semaphore;
}
@end

@implementation MYDispatchSemaphoreViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    semaphore = dispatch_semaphore_create(1);
    /**
     *  Create a signal with a semaphore of 1
     *
     */
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**
     *  semaphore: Waiting signal
     DISPATCH_TIME_FOREVER: waiting time
     wait Then the semaphore - 1, 0
     */
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    /**
     *  Send a signal, and then the semaphore + 1, 1
     */
    dispatch_semaphore_signal(semaphore);
}

@end

Conditional Lock NSCondition

NSCondition also implements the NSLocking protocol, so like NSLock, it also has the lock and unlock methods of NSLocking protocol, which can be used as NSLock to solve thread synchronization problems in exactly the same way.

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    [lock unlock];
}

At the same time, NSCondition provides more advanced usage. wait and signal, similar to conditional semaphores.
For example, we listen for the number of imageNames arrays, and when the number of imageNames is greater than 0, we perform the emptying operation. The idea is that when the number of imageNames is greater than 0, the emptying operation is performed, otherwise, wait waits for the emptying operation to be performed. signal signals occur as the number of imageNames increases, allowing the waiting thread to wake up and continue executing.
NSConditions are different from NSLock and @synchronized. NSConditions can lock each thread separately without affecting other threads entering the critical zone. This is very powerful.
But because of this separate locking approach, NSCondition can't really solve resource competition by using wait and locking. For example, we have a need: not let m < 0. Assuming that the current m=0, thread A should judge m > 0 as false and wait; thread B performs m=1 operation and wakes up thread A to perform m-1 operation while thread C judges m > 0, because in different thread locks, thread A and thread C also judges m-1 as true. At this time, thread A and thread C will execute m-1, but m=1, resulting in m=-1.
When I do deletion experiments with arrays, adding and deleting operations do not occur every time, about 3-4 times later. It is no problem to use lock and unlock only.

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];    //Lock up
    static int m = 0;
    static int n = 0;
    static int p = 0;
    NSLog(@"removeObjectBegin count: %ld\n",imageNames.count);

    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObjectAtIndex:0];
        m++;
        NSLog(@"Executed%d Secondary deletion operation",m);
    } else {
        p++;
        NSLog(@"Executed%d Second wait",p);
        [lock wait];    //wait for
        imageName = [imageNames lastObject];
        [imageNames removeObjectAtIndex:0];
        /**
         *  Sometimes clicking to get the picture crashes.
         */
        n++;
        NSLog(@"Executed%d Second Continue Operation",n);
    }

    NSLog(@"removeObject count: %ld\n",imageNames.count);
    [lock unlock];     //Unlock
}
- (void)createImageName:(NSMutableArray *)imageNames{
    [lock lock];
    static int m = 0;
    [imageNames addObject:@"0"];
    m++;
    NSLog(@"Added%d second",m);
    [lock signal];  //Wake up a random thread to cancel and wait for execution to continue

//        [lock broadcast]; // Wake up all threads to cancel and wait for execution to continue
    NSLog(@"createImageName count: %ld\n",imageNames.count);
    [lock unlock];
}

#pragma mark - delete the picture after multi-threading
- (void)getImageNameWithMultiThread{
    [lock broadcast];
    NSMutableArray *imageNames = [[NSMutableArray alloc]init];
    dispatch_group_t dispatchGroup = dispatch_group_create();
    __block double then, now;
    then = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<10; i++) {
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self getIamgeName:imageNames];
        });
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self createImageName:imageNames];
        });
    }
    dispatch_group_notify(dispatchGroup, self.synchronizationQueue, ^(){
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    });

}

Conditional Lock

Others say it's a mutex lock
NSConditionLock also implements the NSLocking protocol, and it is found that the performance is very low during the experiment.

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    [lock unlock];
}

NSConditionLock can also do multi-threaded task waiting calls like NSCondition, and it is thread-safe.

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lockWhenCondition:1];    //Lock up
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObjectAtIndex:0];
    }
    [lock unlockWithCondition:0];     //Unlock
}
- (void)createImageName:(NSMutableArray *)imageNames{
    [lock lockWhenCondition:0];
    [imageNames addObject:@"0"];
    [lock unlockWithCondition:1];
}

#pragma mark - delete the picture after multi-threading
- (void)getImageNameWithMultiThread{
    NSMutableArray *imageNames = [[NSMutableArray alloc]init];
    dispatch_group_t dispatchGroup = dispatch_group_create();
    __block double then, now;
    then = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<10000; i++) {
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self getIamgeName:imageNames];
        });
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self createImageName:imageNames];
        });
    }
    dispatch_group_notify(dispatchGroup, self.synchronizationQueue, ^(){
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    });
}

Recursive Lock NSRecursiveLock

Sometimes there are recursive calls in "lock code". Lock before recursion starts. This method is repeated after recursive calls start so that repeated execution of lock code eventually results in deadlock. At this time, recursive locks can be used to solve the problem. Using recursive locks, locks can be retrieved repeatedly in a thread without causing deadlocks. In this process, the number of acquisitions and releases of locks is recorded, and only the last balanced locks are finally released.

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
        [self getIamgeName:imageNames];
    }
    [lock unlock];
}
- (void)getImageNameWithMultiThread{
    NSMutableArray *imageNames = [NSMutableArray new];
    int count = 1024*10;
    for (int i=0; i<count; i++) {
        [imageNames addObject:[NSString stringWithFormat:@"%d",i]];
    }
    dispatch_group_t dispatchGroup = dispatch_group_create();
    __block double then, now;
    then = CFAbsoluteTimeGetCurrent();
    dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
        [self getIamgeName:imageNames];
    });
    dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    });

}

NSDistributedLock

NSDistributed Lock is a cross-process distributed lock in the development of MAC. The underlying is a mutex implemented by a file system. NSDistributedLock does not implement the NSLocking protocol, so there is no lock method, instead of the non-blocking tryLock method.

NSDistributedLock *lock = [[NSDistributedLock alloc] initWithPath:@"/Users/mac/Desktop/lock.lock"];
    while (![lock tryLock])
    {
        sleep(1);
    }

    //do something
    [lock unlock];

When the program exits when it executes to do something, tryLock can no longer succeed after the program starts again and falls into a deadlock state. Other applications can't access protected shared resources. In this case, you can use breadLock to break an existing lock so that you can get it. But you should usually avoid breaking the lock unless you are sure that the owner process is dead and that it is impossible to release the lock again.
Because it's a thread lock under MAC, there's no demo, and I haven't paid much attention to it here.

Mutex Lock POSIX

POSIX is similar to dispatch_semaphore_t, but completely different. POSIX is a set of conditional mutex API s provided on Unix/Linux platform.
A new simple POSIX mutex lock is created. The header file # import < pthread. H > is declared and a structure of pthread_mutex_t is initialized. Use pthread_mutex_lock and pthread_mutex_unlock functions. Call pthread_mutex_destroy to release the data structure of the lock.

#import <pthread.h>
@interface MYPOSIXViewController ()
{
    pthread_mutex_t mutex;  //Declare the structure of pthread_mutex_t
}
@end

@implementation MYPOSIXViewController
- (void)dealloc{
    pthread_mutex_destroy(&mutex);  //Data structure for releasing the lock
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    pthread_mutex_init(&mutex, NULL);
    /**
     *  Initialization
     *
     */
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**
     *  Lock up
     */
    pthread_mutex_lock(&mutex);
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }
    /**
     *  Unlock
     */
    pthread_mutex_unlock(&mutex);
}

POSIX can also create conditional locks, providing the same conditional control as NSCondition s, initializing mutexes and using pthread_cond_init to initialize conditional data structures.

    // Initialization
    int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);

    // Waiting (blocking)
    int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);

    // Wait regularly
    int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);

    // awaken
    int pthread_cond_signal (pthread_cond_t *cond);

    // Radio Awakening
    int pthread_cond_broadcast (pthread_cond_t *cond);

    // Destruction
    int pthread_cond_destroy (pthread_cond_t *cond);

POSIX also provides many functions, including a complete set of API s, including the creation control of Pthreads threads and so on. At the very bottom level, it can handle the state transition of threads manually, that is, the management life cycle. It can even implement a set of its own multi-threads, which can be further understood by interested parties. I recommend a detailed article, but it's not iOS-based, but Linux-based, but it's very detailed. Detailed Interpretation of Linux Thread Locks

Spinlock OSSpinLock

First of all, OSSpinLock has already appeared BUG, which makes it impossible to guarantee thread safety completely.

In the new version of iOS, the system maintains five different thread priority / QoS: background, utility, default, user-initiated, user-interactive. High priority threads always execute before low priority threads, and a thread will not be disturbed by lower priority threads. This thread scheduling algorithm can cause potential priority inversion problems, thus destroying spin lock.
Specifically, if a low-priority thread acquires a lock and accesses a shared resource, then a high-priority thread tries to acquire the lock, and it will be in the busy state of spinlock, thus occupying a large number of CPUs. At this time, low priority threads can not compete with high priority threads for CPU time, resulting in late completion of tasks, unable to release lock. It's not just a theoretical problem. libobjc has encountered this problem many times, so Apple engineers stopped OSSpinLock.
Greg Parker, an Apple engineer, mentioned that one solution to this problem is to use the real unbounded backoff algorithm, which can avoid the livelock problem, but it can still block high-priority threads for tens of seconds if the system is under high load; the other is to use the handoff lock algorithm, which is currently used by libobjc. The lock owner will save the thread ID inside the lock, and the lock waiter will temporarily contribute its priority to avoid the problem of priority inversion. In theory, this model will cause problems under more complex multi-lock conditions, but in practice, everything is good at present.
OSSpinLock spin lock, the highest performance lock. The principle is simple, just keep doing while and so on. Its disadvantage is that it consumes a lot of CPU resources while waiting, so it is not suitable for long-term tasks. It is very suitable for memory cache access.
- From ibireme

So it's not recommended to continue using it, but you can play with it and import the header file # import <libkern/OSAtomic.h>.

#import <libkern/OSAtomic.h>
@interface MYOSSpinLockViewController ()
{
    OSSpinLock spinlock;  //Declare the structure of pthread_mutex_t
}
@end

@implementation MYOSSpinLockViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    spinlock = OS_SPINLOCK_INIT;
    /**
     *  Initialization
     *
     */
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**
     *  Lock up
     */
    OSSpinLockLock(&spinlock);
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }
    /**
     *  Unlock
     */
    OSSpinLockUnlock(&spinlock);
}
@end

OSSpinLock's performance is really excellent, unfortunately.

GCD thread blocking dispatch_barrier_async/dispatch_barrier_sync

The dispatch_barrier_async/dispatch_barrier_sync can also synchronize threads on a certain basis, interrupting other threads in the thread queue to perform the current task, that is to say, only used in concurrent thread queues will be effective, because the serial queue is one by one, and you interrupt the execution of one is the same effect as inserting one. The difference between the two is whether to wait for the task to complete.

Note: Deadlock occurs if dispatch_barrier_sync interrupts are called by the current thread.

@interface MYdispatch_barrier_syncViewController ()
{
        __block double then, now;
}
@end

@implementation MYdispatch_barrier_syncViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }else{
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    }
}

- (void)getImageNameWithMultiThread{
    NSMutableArray *imageNames = [NSMutableArray new];
    int count = 1024*11;
    for (int i=0; i<count; i++) {
        [imageNames addObject:[NSString stringWithFormat:@"%d",i]];
    }
    then = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<count+1; i++) {
        //100 to test whether the lock is properly executed
        dispatch_barrier_async(self.synchronizationQueue, ^{
             [self getIamgeName:imageNames];
        });
    }
}

summary

@ synchronized: multi-threaded locking with fewer threads and fewer tasks
NSLock: Actually, NSLock is not as bad as you think. I wonder why you don't recommend it.
dispatch_semaphore_t: Using signals to lock, performance improves significantly
NSCondition: It is not thread-safe to use it to make communication calls between multiple threads
NSConditionLock: Simple locking performance is very low, much lower than NSLock, but it can be used for multithreading communication calls for different tasks
NSRecursiveLock: Recursive locks perform surprisingly well, but can only be used recursively, limiting usage scenarios
NSDistributed Lock: Because it was developed by MAC, it was not discussed.
POSIX(pthread_mutex): The underlying api, complex multithreading is recommended and can encapsulate your own multithreading
OSSpinLock: It's also very high performance, but unfortunately there's a threading problem
dispatch_barrier_async/dispatch_barrier_sync: It's surprising to find that dispatch_barrier_sync performs better than dispatch_barrier_async in tests.


Posted by hearn on Wed, 12 Jun 2019 16:28:37 -0700