What can cause security problems for multithreading?

With multithreading, we can perform multiple tasks concurrently, so it is possible for the same resource to be accessed (read/write) in multiple threads at the same time. This phenomenon is called resource sharing, where multiple threads accessing the same object, variable, or file at the same time can lead to data corruption and data security issues.

Classic problem number one — saving and withdrawing money

The following shows the problem in code

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self moneyTest];
}

// Save and withdraw money test- (void)moneyTest {
    self.money = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0.0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [selfsaveMoney]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self drawMoney];
        }
    });
}

-(void)saveMoney {
    NSInteger oldMoney = self.money;
    sleep(2.);// Simulate the task duration, easy to show the problem
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@" 50 yuan deposited, account balance %ld-------%@", (long)oldMoney, [NSThreadcurrentThread]); } - (void)drawMoney {
    NSInteger oldMoney = self.money;
    sleep(2.);// Simulate the task duration, easy to show the problem
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@" withdraw 20 yuan, account balance %ld-------%@", (long)oldMoney, [NSThread currentThread]);
}
Copy the code

In the moneyTest method, we conducted 10 money-saving and money-withdrawal operations in multi-threaded mode, each time 50 is saved, each time 20 is withdrawn, the initial value of the deposit is 100, the target balance should be 100+ (50*10) – (20*10) = 400, the running results are as follows

You can see that the balance at the end is wrong.


Classic problem number two — selling tickets

The following shows the problem in code

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self sellTicketTest];
}

// Ticket problem- (void)sellTicketTest {
    self.ticketsCount = 30;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0.0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<5; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<5; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<5; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(2.);// Simulate the task duration, easy to show the problem
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@" %ld tickets left -------%@", (long)oldTicketsCount, [NSThread currentThread]);
}
Copy the code

In sellTicketTest, the initial number of tickets is 30, and the tickets are sold on three threads at the same time. Each thread sells 10 tickets at the same time

The printout showed an error in the final remaining votes.

The above two classic problems are caused by multiple threads reading and writing to the same resource. I’ll use a picture that you’re all familiar with

The solution to this problem: useThread synchronizationTechnology (Synchrony is a coordinated pace, carried out in a predetermined order). Common thread synchronization techniques are:lock. Let me summarize it with the following figure


Thread synchronization (locking) scheme

There are several thread synchronization schemes in iOS:

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • @synchronized

Let’s try it one by one.

(a) OSSpinLock

OSSpinLock is called “spin lock” and requires the import of the header file #import . It has the following API

  • OSSpinLock lock = OS_UNFAIR_LOCK_INIT;— Initialize the lock object lock
  • OSSpinLockTry(&lock);— Try locking, continue on success, return on failure, continue to execute the following code, do not block the thread
  • OSSpinLockLock(&lock);Lock. Failure to lock will block the thread and wait
  • OSSpinLockUnlock(&lock);– unlock

Let’s see how it is used in code. The following code to undertake the above ticket case to lock operation

@interface ViewController(a)
@property (nonatomic.assign) NSInteger ticketsCount;
@end* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self sellTicketTest];
// [self moneyTest];
}
// Ticket problem- (void)sellTicketTest {
    self.ticketsCount = 30;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0.0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    // Initialize the lock
    OSSpinLock lock = OS_SPINLOCK_INIT;
    / / lock πŸ”’ πŸ”’ πŸ”’ πŸ”’
    OSSpinLockLock(&lock);
    
    
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(2.);// Simulate the task duration, easy to show the problem
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@" %ld tickets left -------%@", (long)oldTicketsCount, [NSThread currentThread]);
    
    / / unlock πŸ”“ πŸ”“ πŸ”“ πŸ”“
    OSSpinLockUnlock(&lock);
}
@end
Copy the code

Post-run resultsIt didn’t work out. What happened? Just to add something hereLocking principle:One picture

Therefore, according to the above principle, we should use the same lock object to lock an operation (code segment). So write the above lock as a global property, as follows

@interface ViewController(a)
@property (nonatomic.assign) NSInteger ticketsCount;
@property (nonatomic.assign) OSSpinLock lock;
@end* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *@implementation ViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self sellTicketTest];
}

// Ticket problem- (void)sellTicketTest {
    self.ticketsCount = 30;
    // Initialize the lock
    self.lock = OS_SPINLOCK_INIT;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0.0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    
    / / lock πŸ”’ πŸ”’ πŸ”’ πŸ”’
    OSSpinLockLock(&_lock);
    
    
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(2.);// Simulate the task duration, easy to show the problem
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@" %ld tickets left -------%@", (long)oldTicketsCount, [NSThread currentThread]);
    
    / / unlock πŸ”“ πŸ”“ πŸ”“ πŸ”“
    OSSpinLockUnlock(&_lock);
}
@end
Copy the code

And then the final result is fine

The ticket selling problem, we are dealing with the same operation, and the deposit and withdrawal problem, involving two operations (deposit and withdrawal), let’s see how to deal with it. Must first clear problem, locking mechanism is to solve from multiple threads at the same time, access to a Shared resource produced by the data problems, whether these threads perform the same operations (such as selling tickets), or a different action (save money), so you can think it doesn’t matter with operation, the nature of the problem is the need to identify clearly, what operations cannot be conducted at the same time, These operations are then locked using the same lock object.

Therefore, we need to use the same lock object to lock the save and withdraw operations since the save and withdraw operations cannot be performed at the same time (that is, two threads cannot deposit money at the same time, two threads cannot withdraw money at the same time, and two threads cannot deposit and withdraw money separately). The following code

@interface ViewController(a)
@property (nonatomic.assign) NSInteger money;
@property (nonatomic.assign) OSSpinLock lock;
@end* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// [self sellTicketTest];
    [self moneyTest];
}
// Saving money and withdrawing money- (void)moneyTest {
    self.money = 100;
    // Initialize the lock
    self.lock = OS_SPINLOCK_INIT;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0.0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [selfsaveMoney]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self drawMoney];
        }
    });
}

-(void)saveMoney {
    
    / / lock πŸ”’ πŸ”’ πŸ”’ πŸ”’
    OSSpinLockLock(&_lock);
    
    NSInteger oldMoney = self.money;
    sleep(2.);// Simulate the task duration, easy to show the problem
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@" 50 yuan deposited, account balance %ld-------%@", (long)oldMoney, [NSThread currentThread]);
    / / unlock πŸ”“ πŸ”“ πŸ”“ πŸ”“OSSpinLockUnlock(&_lock); } - (void)drawMoney {
    / / lock πŸ”’ πŸ”’ πŸ”’ πŸ”’
    OSSpinLockLock(&_lock);
    
    NSInteger oldMoney = self.money;
    sleep(2.);// Simulate the task duration, easy to show the problem
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@" withdraw 20 yuan, account balance %ld-------%@", (long)oldMoney, [NSThread currentThread]);
    / / unlock πŸ”“ πŸ”“ πŸ”“ πŸ”“
    OSSpinLockUnlock(&_lock);
}

@end
Copy the code

And I got the right answer

The principle of spin locking

The principle of spin locking is to keep the thread in busy-wait state when the lock fails, so that the thread stays outside the critical area (the section of code that needs to be locked). Once the lock is successful, the thread can enter the critical area to operate on the shared resource.

There are two ways to block a thread:

  • One is to actually put the thread to sleep,RunLoopIt’s used inmach_msg()The effect of this implementation is that it borrows system kernel instructions to actually stop the thread, the CPU no longer allocates resources to the thread, and therefore no longer executes any assembly instructions. Which we’ll talk about laterThe mutexIn this case, the underlying assembly calls a system functionsyscallCauses the thread to sleep
  • The other kind is spinlockBusy etc.It’s essentially aThe while loop, constantly to judge the lock conditions, once the current has entered the critical area (lock code block) thread completed the operation, unlock after the lock, waiting for the lock thread can successfully lock, again enter the critical area. The spin lock doesn’t actually stop the thread, it’s just temporarily stuck in the while loop while the CPU allocates resources to process its assembly instructions. Let’s verify that the nature of the spin lock is a while loop by using an assembly trace of the ticket-selling operation. The code will be transformed as follows
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [selfsellTicketTest]; } - (void)sellTicketTest {
    self.ticketsCount = 30;
    // Initialize the lock
    self.lock = OS_SPINLOCK_INIT;
    // Start 10 threads to sell tickets
    for (int i = 0; i<10; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(sellTicket) object:nil] start];
    }
}


-(void)sellTicket {
    
    / / lock πŸ”’ πŸ”’ πŸ”’ πŸ”’
    OSSpinLockLock(&_lock);
    
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(600);//⚠️⚠️⚠️ Task duration simulation is 10 minutes, convenient assembly debugging
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@" %ld tickets left -------%@", (long)oldTicketsCount, [NSThread currentThread]);
    
    / / unlock πŸ”“ πŸ”“ πŸ”“ πŸ”“
    OSSpinLockUnlock(&_lock);
}
Copy the code

We add a breakpoint to the lock code of the ticket method, run the program, and the first thread goes to the breakpoint, and the lock succeedsAfter the breakpoint is skipped, the thread continues to execute the following code, while a second thread comes to the lock code breakpointFrom there, we go to the sink code interface and start tracing the underlying function call stack. We pass the first breakpoint, and immediately a second thread comes to the breakpoint, which is bound to block. Xcode in the breakpoint debugging interface, can passDebug -> Debug Workflow -> Always Show DisassemblySwitch to the assembly interface. We can command from the consolesiExecuting a line of assembly code into a function called by the current assembly instruction will bring you to the assembly interface of that function. Okay, let’s start debuggingRead the function call stack for the assembly trace above:

  • sellTicket
  • OSSpinLockLock
  • _OSSpinLockLockSlow

In _OSSpinLockLockSlow, you can see that CMPL loops between CMPL and JNE. If you’re not familiar with arm64’s assembly instructions, CMPL compares two values. Jne stands for jump if not equal, a program that loops between the two. Yes, this is a while loop, and the few lines of code boxed in the figure are typical assembler implementations of a while loop. So, we’ve proved that the essence of a spin lock is a while loop.

Why were spinlocks abandoned

Apple has advised developers to stop using spinlocks because they are no longer safe due to thread priority inversion, which causes the spinlock to get stuck.

We know that the CPU of a computer can only handle one thread at a time, and for a single CPU, the concurrency of threads is actually an illusion, because the system makes the CPU switch back and forth between threads at very small intervals, so it looks like multiple threads are going on at the same time. In the era of multi-core cpus, true thread concurrency is possible, but the number of CPU cores is limited, and the number of threads in the program is usually much larger than the number of CPUS. Therefore, we are still dealing with multi-threading on a single CPU. Based on this scenario, it is important to understand the concept of thread priority, where the CPU will allocate as much time (resources) as possible to the thread with the highest priority. With this in mind, the following diagram illustrates what is called thread priorityPriority inversion problem

The essence of the while loop of spin lock makes the thread not stop. Generally, a thread does not wait for a lock for a long time, and the CPU resource consumed by using spin to block the thread is less than the CPU resource cost caused by the sleep and wake of the thread. Therefore, spin lock is a highly efficient locking mechanism. But priority inversion made spin-locks unsafe, and the ultimate goal of locking was safety rather than efficiency, so Apple abandoned spin-locks.

And why does RunLoop choose true thread sleep? Because the App may be in idle state for a long time without any user activity, no CPU is required. In this scenario, of course, it is better to let the thread sleep to save performance. Well, this is the life of the spin lock, although it is history, but it is certainly better to know.

(2) os_unfair_lock

Os_unfair_lock should be used to replace OSSpinLock after iOS10.0.

To use OS_UNfair_LOCK, you need to import the header #import < OS /lock.h>, which has the following API

  • os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;

Initialize the lock object lock

  • os_unfair_lock_trylock(&lock);

Try to lock, lock success continue, lock failure return, continue to execute the following code, do not block the thread

  • os_unfair_lock_lock(&lock);

Lock. Failure to lock blocks the thread to wait

  • os_unfair_lock_unlock(&lock);

unlock

In os_UNFAIR_LOCK, apple uses the same method as OSSpinLock to solve the problem of priority inversion. In os_UNfair_lock, Apple uses the same method as OSSpinLock to solve the problem of priority inversion.

(3) the pthread_mutex

Pthread_mutex comes from pthreads and is a cross-platform solution. Mutex stands for mutex, and the thread waiting for the lock goes to sleep. The opposite of this is the first type of spin lock we introduced earlier, which does not sleep. It has the following API to initialize the lock property pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NOMAL); Initialize lock pthread_mutex_t mutex; pthread_mutex_init(&mutex, &attr); Pthread_mutex_trylock (&mutex); Lock pthread_mutex_lock (& mutex); Unlock the pthread_mutex_unlock (& mutex); Pthread_mutexattr_destroy (&attr); pthread_mutex_destroy(&attr);

First, a complete code case for selling tickets

#import <pthread.h>

@interface ViewController(a)
@property (nonatomic.assign) NSInteger ticketsCount;
//pthread_mutex
@property (nonatomic.assign) pthread_mutex_t ticketMutexLock;
@end

@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self sellTicketTest];
}

// Ticket problem- (void)sellTicketTest {
    self.ticketsCount = 30;
  
// Initialize the property
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    // Initialize the lock pthread_mutex
    pthread_mutex_init(&_ticketMutexLock, &attr);
    
    dispatch_queue_t queue = dispatch_get_global_queue(0.0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    
    / / lock πŸ”’ πŸ”’ πŸ”’ πŸ”’
    / / add the pthread_mutex
    pthread_mutex_lock(&_ticketMutexLock);
    
    
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(2.);// Simulate the task duration, easy to show the problem
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@" %ld tickets left -------%@", (long)oldTicketsCount, [NSThread currentThread]);
    
    / / unlock πŸ”“ πŸ”“ πŸ”“ πŸ”“
    / / unlock pthread_mutexpthread_mutex_unlock(&_ticketMutexLock); } - (void)dealloc {
    pthread_mutex_destroy(&_ticketMutexLock);
}
@end
Copy the code

As you can see, the locking and unlocking operations are the same, except that the initialization steps are slightly more difficult than the previous ones. There is also the need for lock release. Here’s how to initialize mutex

int pthread_mutex_init(pthread_mutex_t * __restrict,
		const pthread_mutexattr_t * _Nullable __restrict);
Copy the code

The first argument is the lock object to be initialized, and the second argument is the property of the lock object. To this end, we also need to specially generate the property object, by defining the property object -> initialize the property object -> set the property type of these three steps to complete, the property of the following categories

/* * Mutex type attributes */
#define PTHREAD_MUTEX_NORMAL		0
#define PTHREAD_MUTEX_ERRORCHECK	1
#define PTHREAD_MUTEX_RECURSIVE		2
#define PTHREAD_MUTEX_DEFAULT		PTHREAD_MUTEX_NORMAL
Copy the code

PTHREAD_MUTEX_DEFAULT = PTHREAD_MUTEX_NORMAL, both of which represent common mutex locks. PTHREAD_MUTEX_ERRORCHECK indicates that an error lock is checked. PTHREAD_MUTEX_RECURSIVE stands for recursive mutex, which I’ll cover in a moment.

If we give the lock default properties, we can use one line of codepthread_mutex_init(mutex, NULL);To handle the lock initialization, no need to configure the property information. The parameterNULLInitializing a normal mutex

The underlying implementation of mutex

We verified earlier by looking at the assembly that the essence of a spin lock is actually blocking a thread through a while loop. Now let’s see how mutex works, also through assembly. First modify the method of selling tickets as follows

// Ticket problem- (void)sellTicketTest {
    self.ticketsCount = 30;

    // Initialize the property
    pthread_mutexattr_init(&_attr);
    pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_NORMAL);
    // Initialize the lock
    pthread_mutex_init(&_ticketMutexLock, &_attr);
    // Create 10 threads to sell tickets
    for (int i = 0; i<10; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(sellTicket) object:nil] start];
    }
}

-(void)sellTicket {
    / / add the pthread_mutex
    pthread_mutex_lock(&_ticketMutexLock);
    
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(600);// Simulate the task duration, easy to show the problem
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@" %ld tickets left -------%@", (long)oldTicketsCount, [NSThread currentThread]);
    
    / / unlock pthread_mutexpthread_mutex_unlock(&_ticketMutexLock); } - (void)dealloc {
    pthread_mutex_destroy(&_ticketMutexLock);
    pthread_mutexattr_destroy(&_attr);
}
Copy the code

Set the ticket selling process of the above code to 600 seconds for us to locate the problem. The breakpoint on the first thread is crossed firstThen you come to the breakpoint for the second threadThen view the assembly from that breakpointTo summarize the assembly trace diagrams above, the mutex function call stack looks like this:

  • sellTicket
  • pthread_mutex_lock
  • _pthread_mutex_firstfit_lock_slow
  • _pthread_mutex_firstfit_lock_wait
  • __psynch_mutexwait
  • syscall

This syscall stands for system call, which typically calls a method that compares the kernel at the system level. When we continue from Syscall, the breakpoint disappears because the thread is now sleeping and no longer executing assembly code, so the breakpoint cannot be traced.

Os_unfair_lock is a spinner lock that blocks threads through hibernation. It is also a spinner lock. It is also a spinner lock. You should be able to see for yourself who’s right. Os_unfair_lock os_UNFAIR_lock os_UNFAIR_lock os_UNFAIR_lock os_UNFAIR_lock To put it another way, if it really is a spinlock, what about ** *? I strongly suggest you try assembly debugging, practice out of real knowledge, pretty fun, but also deepen the memory.

A mutex recursive lock

Now look at the following scenario

- (void)otherTest {
    NSLog(@"%s",__func__);
    [selfotherTest2]; } - (void)otherTest2 { 
    NSLog(@"%s",__func__);
}
Copy the code

If you call otherTest normally, the result, as you all know, would look like this

2019- 08- 21 14:23:22.853271+0800Multithreaded security [986:22420] -[ViewController otherTest]
2019- 08- 21 14:23:22.853388+0800Multithreaded security [986:22420] -[ViewController otherTest2]
Copy the code

If both pieces of code need to be thread-safe, let’s see what happens by adding a mutex

- (void)otherTest {
    
    
    // Initialize the property
    pthread_mutexattr_init(&_attr);
    pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_NORMAL);
    // Initialize the lock
    pthread_mutex_init(&_mutex, &_attr);
    
    pthread_mutex_lock(&_mutex);
    NSLog(@"%s",__func__);
    [selfotherTest2]; pthread_mutex_unlock(&_mutex); } - (void)otherTest2 {
    pthread_mutex_lock(&_mutex);
    NSLog(@"%s",__func__); pthread_mutex_unlock(&_mutex); } ****************** The result is **********************8
2019- 08- 21 14:33:15.816512+0800Multithreaded security [1101:29283] -[ViewController otherTest]
Copy the code

If you add the same lock to both methods, you can see the callotherTestMethod causes the thread to get stuck inside the method, after only executing the print code, it can’t proceed further. The reason is simple, as shown belowThis problem can be solved simply by adding different lock objects to the two methods.

If we need to lock a recursive function during development, such as the following one

- (void)otherTest {
    pthread_mutex_lock(&_mutex);
    
    NSLog(@"%s",__func__);
    // Business logic

    [self otherTest];
    
    pthread_mutex_unlock(&_mutex);
}
Copy the code

You can’t use different lock objects to add locks. As long as the same lock object is used, deadlock will definitely occur. Pthread provides recursive locks to solve this problem. To use a recursive lock, simply select the recursive lock property when initializing the property. The other steps are the same as normal mutex.

pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_RECURSIVE);
Copy the code

So how do recursive locks avoid deadlocks? For a recursive function with exits, the number of calls is equal to the number of exits, so the number of locks pthread_mutex_lock and unlock pthread_mutex_unlock are equal, So at the end of the recursive function, all πŸ”’ will be unwound.

** But keep in mind that recursive locking is only for repeated locking and unlocking within the same thread. Recursive locking does not apply to repeated locking/unlocking in any scenario other than single-threaded recursive function calls.


Mutex conditionspthread_cond_t

Let’s start by listing the relevant apis

  • pthread_mutex_t mutex;

Define a lock object

  • pthread_mutex_init(&mutex, NULL);

Initialize the lock object

  • pthread_cond_t condition;

Define a conditional object

  • pthread_cond_init(&condition, NULL);

Initializes the condition object

  • pthread_cond_wait(&condition, &mutex);

— Waiting conditions

  • pthread_cond_signal(&condition);

— Activates a thread waiting for the condition

  • pthread_cond_broadcast(&condition);

Activate all threads waiting for the condition

  • pthread_mutex_destroy(&mutex);

— Destroy the lock object

  • pthread_cond_destroy(&condition);

Destroy the conditional object

To illustrate the use of mutex conditions, let’s design an example scenario:

  • We are inremoveMethod faces an arraydataArrTo delete an element
  • inaddIn the methoddataArrPerform the element addition operation
  • And ask, ifdataArrIf the number of elements is 0, the deletion operation cannot be performed

Here is the example code and the results

@interface ViewController(a)
@property (nonatomic.strong) NSMutableArray *dataArr;
/ / lock object
@property (nonatomic.assign) pthread_mutex_t mutex;
// Conditional object
@property (nonatomic.assign) pthread_cond_t cond;
@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
        // Initialize the property
        pthread_mutexattr_t _attr;
        pthread_mutexattr_init(&_attr);
        pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_NORMAL);
        // Initialize the lock
        pthread_mutex_init(&_mutex, &_attr);
        // Release the attribute when it is used
        pthread_mutexattr_destroy(&_attr);
        // Initialization conditions
        pthread_cond_init(&_cond, NULL);
        // Initialize the array
        self.dataArr = [NSMutableArray array];

    [self dataArrTest];
}

- (void)dataArrTest
{
    // Start thread remove, dataArr is null
   NSThread *removeT =  [[NSThread alloc] initWithTarget:self selector:@selector(remove) object:nil];
    [removeT setName:@"REMOVE operation thread"];
    [removeT start];
    
    sleep(1.);
    
    // Then start the thread for add
    NSThread *addT = [[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil];
    [addT setName:@"ADD operation thread"];
    [addT start];
}

//****** adds elements to the array ********- (void)add {
    
    / / lock
    pthread_mutex_lock(&_mutex);
    
    NSLog(@"[LOCK]% @lock succeeded -->add start \n"[NSThread currentThread].name);
    sleep(2);
    [self.dataArr addObject:@"test"];
    NSLog(@"add successful, dataArr contains %lu elements, send conditional signal -------->\n", (unsigned long)self.dataArr.count);
    
    // Send a conditional signal
    pthread_cond_signal(&_cond);
    
    / / unlock
    pthread_mutex_unlock(&_mutex);
    NSLog([UNLOCK]%@ UNLOCK successfully, thread terminated \n"[NSThread currentThread].name);
}


//******** removes the element from the dictionary *******- (void)remove {
    / / lock
    pthread_mutex_lock(&_mutex);
    
    
    NSLog(@"[LOCK]% @lock succeeded -->remove start \n"[NSThread currentThread].name);
    if (!self.dataArr.count) {
        // Perform conditional wait
        NSLog(@"dataArr has no elements, start waiting ~~~~~~~~\n");
        pthread_cond_wait(&_cond, &_mutex);
        NSLog(@"--------> Received conditional update signal, dataArr already has elements, continue deleting operation \n");
    }
    [self.dataArr removeLastObject];
    NSLog(@" Remove successful, there are still %lu elements in dataArr \n", (unsigned long)self.dataArr.count);
    
    / / unlock
    pthread_mutex_unlock(&_mutex);
    NSLog([UNLOCK]%@ UNLOCK successfully, thread terminated \n"[NSThread currentThread].name);
}


@end
Copy the code

The result is as follows

2019- 08- 21 21:14:07.767881+0800Multithreaded security [3751:204180] [LOCK]REMOVE Operation thread LOCK succeeded --> REMOVE start2019- 08- 21 21:14:07.768075+0800Multithreaded security [3751:204180] dataArr has no elements and starts waiting ~~~~~~~~2019- 08- 21 21:14:07.768216+0800Multithreaded security [3751:204181[LOCK]ADD Operation thread LOCK success --> ADD start2019- 08- 21 21:14:09.771757+0800Multithreaded security [3751:204181] add succeeded in dataArr1Element, send conditional signal -------->2019- 08- 21 21:14:09.772048+0800Multithreaded security [3751:204181[UNLOCK]ADD The operation thread is successfully unlocked. The thread ends2019- 08- 21 21:14:09.772081+0800Multithreaded security [3751:204180] --------> Conditional update signal received, dataArr has elements, continue deletion operation2019- 08- 21 21:14:09.772300+0800Multithreaded security [3751:204180] remove successful, dataArr remaining0An element2019- 08- 21 21:14:09.772496+0800Multithreaded security [3751:204180[UNLOCK]REMOVE The operation thread is unlocked successfully, and the thread ends//
Copy the code

From the case and the results of the analysis, the condition of the mutexpthread_cond_tAfter the thread is locked, if the condition is not up to the standard, the thread can be suspended, until the condition meets the standard, continue to execute the thread. This description is still abstract, see the picture below

To summarize what pthread_cond_t does:

  • It is called when the business logic in thread A cannot be executed furtherpthread_cond_wait(&_cond, &_mutex);, this code will first unlock the current thread, and then sleep the current thread to wait for the condition signal,
  • At this point, the lock has been unlocked, then the thread B waiting for the lock can successfully lock, execute the logic behind it, because the completion of some operations in thread B can trigger the running condition of A, then through thread Bpthread_cond_signal(&_cond);Send out a conditional signal.
  • If thread A receives the condition signal, it will be firedpthread_cond_tWake up, once thread B is unlocked,pthread_cond_tThe lock will be re-locked in thread A, and the subsequent operations of thread A will be continued, and finally unlocked. From front to back, there are three locks, three unlocks.
  • throughpthread_cond_tThis implements a thread-to-thread dependency, and we will use this cross-thread dependency in many scenarios in real development.

NSLock, NSRecursiveLock, NSCondition

Above we know mutex common lock, mutex recursive lock, mutex conditional lock, are all based on THE API of C language. Based on this, Apple carries out a level of object encapsulation, and provides the corresponding OC lock for developers as follows

  • NSLock— – > encapsulatepthread_mutex_t(attr = common)
  • NSRecursiveLock— – > encapsulatepthread_mutex_t(attr = recursion)
  • NSCondition— – > encapsulatepthread_mutex_t + pthread_cond_t

Since the underlying layer is pthread_mutex, I’m not going to use code examples here, because the principle is the same except that it’s easier to write. Here’s how to use the API

/ / ordinary locks
NSLock *lock = [[NSLock alloc] init];
[lock lock];
[lock unlock];
/ / recursive locking
NSRecursiveLock *rec_lock = [[NSRecursiveLockalloc] [rec_lock lock]; [rec_lock unlock]; init];Lock / / conditions
NSCondition *condition = [[NSCondition alloc] init];
[self.condition lock];
[self.condition wait];
[self.condition signal];
[self.condition unlock];

Copy the code

(5) NSConditionLock

Apple always wants developers not to know too much, to become lazier and more dependent on their ecosystem, and to this end, NSConditionLock has been further encapsulated based on NSCondition. The lock allows us to set conditions in the lock specific condition value, with this feature, we can more convenient multi-thread dependencies and sequential execution. Let’s start with the API: some of the same functionality as the previous lock

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
Copy the code

features

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
Copy the code

The following is a case to illustrate its functions

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

- (void)viewDidLoad {
    [super viewDidLoad];
    [self otherTest];
}

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

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

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

- (void)__three
{
    [self.conditionLock lockWhenCondition:3];
    
    NSLog(@"__three");
    
    [self.conditionLock unlock];
}

Copy the code

The effect of the code implementation is that the __one method is executed first, then the __two method, and finally the __three method. Because the three methods are in three different child threads, the order of execution, or dependency, of the three threads is precisely controlled. Use the following illustration again

These NS locks are not open source but belong to the Apple Foundation framework, but we can refer to GNU_Foundation to get a general understanding of the internal implementation of these NS locks. Tell yourself what GNU is.

(6) dispatch_queue (DISPATCH_QUEUE_SERIAL)

The SERIAL queue of the GCD can also achieve multi-threaded synchronization, and it is not done by locking. Thread synchronization essentially requires multiple threads to execute sequentially, linearly, one after the other, and the GCD’s serial queue is designed to do just that. Let’s go straight to code examples to demonstrate this

@interface GCDSerialQueueVC(a)
@property (nonatomic.assign) NSInteger ticketsCount;
@property (nonatomic.assign) NSInteger money;
// A serial queue where ticket operations are placed
@property (strong.nonatomic) dispatch_queue_t ticketQueue;
// This is the serial queue where all the operations are placed
@property (strong.nonatomic) dispatch_queue_t moneyQueue;
@end

@implementation GCDSerialQueueVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
    self.moneyQueue = dispatch_queue_create("moneyQueue", DISPATCH_QUEUE_SERIAL);
    [self moneyTest];
    [self sellTicketTest];
}


// Saving money and withdrawing money- (void)moneyTest {
    self.money = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0.0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [selfsaveMoney]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self drawMoney];
        }
    });
}

-(void)saveMoney {
    
    dispatch_sync(_moneyQueue, ^{// The deposit operation is placed in the deposit or withdrawal queue
        NSInteger oldMoney = self.money;
        sleep(2.);// Simulate the task duration, easy to show the problem
        oldMoney += 50;
        self.money = oldMoney;
        NSLog(@" 50 yuan deposited, account balance %ld-------%@", (long)oldMoney, [NSThreadcurrentThread]); }); } - (void)drawMoney {
    
    dispatch_sync(_moneyQueue, ^{// The withdrawal operation is placed in the withdrawal queue
        NSInteger oldMoney = self.money;
        sleep(2.);// Simulate the task duration, easy to show the problem
        oldMoney -= 20;
        self.money = oldMoney;
        NSLog(@" withdraw 20 yuan, account balance %ld-------%@", (long)oldMoney, [NSThread currentThread]);
    });
}


// Ticket problem- (void)sellTicketTest {
    self.ticketsCount = 30;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0.0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    dispatch_sync(_ticketQueue, ^{// The ticket operation is placed in the ticket queue
        NSInteger oldTicketsCount = self.ticketsCount;
        sleep(2.);// Simulate the task duration, easy to show the problem
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        NSLog(@" %ld tickets left -------%@", (long)oldTicketsCount, [NSThread currentThread]);
    });
}

@end
Copy the code

(7) dispatch_semaphore

In addition to the above solution, GCD also provides a Dispatch_semaphore solution for developers to handle multi-threaded synchronization issues. Semaphore means “semaphore”. The initial value of the semaphore can be used to control the thread and issue the maximum number of accesses. The initial value of the semaphore is 1, which means that one thread is allowed to access the resource at the same time, thus achieving thread synchronization. To familiarize yourself with its API:

  • dispatch_semaphore_create(value)Creates a semaphore from an initial value
  • Dispatch_semaphore_wait (semaphore)

If the semaphore value is <=0, the current thread goes to sleep and waits (until the semaphore value is >0). If the semaphore value is >0, it decreases by 1, and then executes the following code.

  • dispatch_semaphore_signal(semaphore)Increase the semaphore value by 1

If we set the initial semaphore value to 1, multiple threads run as shown below

As you can see, threads are executed sequentially, that is, only one thread can execute business code at a time, which meets thread synchronization requirements. According to the above reasoning, if the initial semaphore value is set to 2, two threads can be running at the same time, equivalent to controlling the number of concurrent threads. So finally, I’ll show you the code example

@interface GCDSemaphoreVC(a)
@property (nonatomic.assign) NSInteger ticketsCount;
@property (nonatomic.assign) NSInteger money;

@property (strong.nonatomic) dispatch_semaphore_t semaphore;// Test the semaphore concurrently
@property (strong.nonatomic) dispatch_semaphore_t ticketSemaphore;// Sell tickets to test semaphores
@property (strong.nonatomic) dispatch_semaphore_t moneySemaphore;// Access money test semaphore
@end

@implementation GCDSemaphoreVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.semaphore = dispatch_semaphore_create(5);
    self.ticketSemaphore = dispatch_semaphore_create(1);
    self.moneySemaphore = dispatch_semaphore_create(1);
    
    
    [self moneyTest];
    [self sellTicketTest];
    [self concurrencytest];
}


// Maximum number of concurrent tests
- (void)concurrencytest
{
    for (int i = 0; i < 20; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(concurrencyOperationUnit) object:nil] start]; }} - (void)concurrencyOperationUnit
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(2);
    NSLog(@"test - %@"[NSThread currentThread]);
    
    // Set the semaphore to +1
    dispatch_semaphore_signal(self.semaphore);
}


// Saving money and withdrawing money- (void)moneyTest {
    self.money = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0.0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {// Execute 10 save operations asynchronously
            [selfsaveMoney]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {// Perform 10 simultaneous withdrawals asynchronously
            [self drawMoney];
        }
    });
}

-(void)saveMoney {
    //πŸ“ΆπŸ“ΆπŸ“ΆπŸ“Ά port port -1
    dispatch_semaphore_wait(_ticketSemaphore, DISPATCH_TIME_FOREVER);
    //πŸ’°πŸ’°πŸ’°πŸ’° service code πŸ’°
    NSInteger oldMoney = self.money;
    sleep(2.);// Simulate the task duration, easy to show the problem
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@" 50 yuan deposited, account balance %ld-------%@", (long)oldMoney, [NSThread currentThread]);
    
    //πŸ“ΆπŸ“ΆπŸ“ΆπŸ“Ά port port +1dispatch_semaphore_signal(_ticketSemaphore); } - (void)drawMoney {
    //πŸ“ΆπŸ“ΆπŸ“ΆπŸ“Ά port port -1
    dispatch_semaphore_wait(_ticketSemaphore, DISPATCH_TIME_FOREVER);
    
    //πŸ’°πŸ’°πŸ’°πŸ’° service code of πŸ’°
    NSInteger oldMoney = self.money;
    sleep(2.);// Simulate the task duration, easy to show the problem
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@" withdraw 20 yuan, account balance %ld-------%@", (long)oldMoney, [NSThread currentThread]);
    
    //πŸ“ΆπŸ“ΆπŸ“ΆπŸ“Ά port port -1
    dispatch_semaphore_signal(_ticketSemaphore);
}


// Ticket problem- (void)sellTicketTest {
    self.ticketsCount = 30;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0.0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [selfsellTicket]; }});dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    //πŸ“ΆπŸ“ΆπŸ“ΆπŸ“Ά port port -1
    dispatch_semaphore_wait(_ticketSemaphore, DISPATCH_TIME_FOREVER);
    
    //🎫🎫🎫🎫 service code for selling tickets
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(2.);// Simulate the task duration, easy to show the problem
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@" %ld tickets left -------%@", (long)oldTicketsCount, [NSThread currentThread]);
    
    //πŸ“ΆπŸ“ΆπŸ“ΆπŸ“Ά port port +1
    dispatch_semaphore_signal(_ticketSemaphore);
    
}

@end
Copy the code

(eight) @ synchronized

Finally, let’s look at a very simple thread synchronization scheme called @synchronized. I’m sure you’ve all used or seen this directive at some point. It’s super simple to use

@synchronized (lockObj) {
        /* Lock code (critical section) */
    }
Copy the code

Although simple to use, it has the worst performance of any thread synchronization scheme. Apple does not recommend using it at all, and you should use it with caution unless you are in a test environment. For mobile devices, what is most valuable is storage space (memory) and CPU resources. Let’s look at the underlying causes of @synchronized’s inefficiency.

First, track it through assembly@synchronizedTo the underlying function call stack, add the breakpoint as shown in the diagramThen display the pool code (Debug` -> `Debug Workflow` -> `Always Show Disassembly)From the compilation,@synchronizedIt’s actually converted toobjc_sync_enterandobjc_cync_exitTwo functions, including the head and tail in the critical region. The source code for these two functions is available in objC4objc-sync.mmFound in the file.


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

From the above analysis, it can be seen that@synchronizedThe interior ends up usingpthread_mutex_tAnd it’s recursive.@synchronizedTo get the parametersobjAnd then we’re going to use delta functionsid2data(obj, ACQUIRE);getSyncData* dataAnd then throughdata->mutexGet the final lock and finally proceedpthread_mutex_tLock unlock operation.

So let’s seeid2data(obj, ACQUIRE);How did you get it?SyncData* data.We see that, in factid2data(obj, ACQUIRE);Is to use obj as the key from the hash table/dictionarysDataListsTake the corresponding from insideSyncDataList, finally processed, and then found the targetSyncData* data.@synchronizedThe core process ofFormal due to@synchronizedInternal encapsulates the array of the dictionary (hash table), c + +, data structure and a series of complex data structures, causing it to the actual performance of the special, is actually the lowest thread synchronization performance, although you probably seen it in some kick-ass framework is used, but if you are not particularly skilled to the underlying, or in accordance with apple’s advice, using less to burn, Because it’s a real waste of performance.

Comparison of synchronization scheme performance

Above, we have experienced each thread synchronization scheme in iOS. In principle, OSSpinLock is very efficient due to its non-sleep nature, but due to security issues, Apple recommended os_UNfair_Lock instead, which is even more efficient. Pthread_mutex is a cross-platform solution with good performance. Of course, apple’s GCD solution is also pretty good. For those OC solutions that start with NS, although the essence is still based on pthread_mutex encapsulation, the efficiency is inevitably reduced due to the additional object-oriented operation overhead. The worst performance was the @synchronized scheme, which was the easiest to use, but resulted in poor performance because of its underlying encapsulation of overly complex data structures. After the actual measurement and summary of various majors, the efficiency of various thread synchronization schemes is ranked from high to low as follows:

  • os_unfair_lock(recommended 🌟 🌟 🌟 🌟 🌟)
  • OSSpinLock(Unsafe ⚠️⚠️)
  • dispatch_semaphore(Recommended 🌟🌟🌟🌟🌟)
  • pthread_mutex(Recommended 🌟🌟🌟🌟)
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)(Recommended 🌟🌟🌟)
  • NSLock(🌟 🌟 🌟)
  • NSCondition(🌟 🌟 🌟)
  • pthread_mutex(recursive)(🌟 🌟)
  • NSRecursiveLock(🌟 🌟)
  • NSConditionLock(🌟 🌟)
  • @synchronized(Least recommended)

Spin-locks versus mutex

When is it better to choose spinlocks? Spin-locks feature high efficiency, low security, and high CPU usage. Therefore, the principles for selecting spin-locks are as follows:

  • Threads are expected to wait a short time for locks
  • Locking code (critical sections) is often invoked, but contention is rare and requires little security
  • CPU resources are not tight
  • Multicore processor

When is it better to use a mutex? Features of a mutex are as follows: High security, small CPU consumption, and CPU resource consumption during hibernation or wake up. Therefore, the principle for selecting a mutex is as follows:

  • The thread is expected to wait a long time for the lock
  • Single-core processor
  • There are IO operations in the critical area
  • Critical section code complex or large loop
  • The competition of critical area is very fierce and the safety requirement is high

Why are atomic devices rarely used in iOS

atomicIs to guaranteesetter,setterThe atomic operation of a method is essentially adding thread-synchronization locks inside getters and setters. We can source in objCobjc-accessors.mmVerify this in the following figure You can see that if the property isatomicAfter retouching,getterMethods andsetterThe inside is actually adding a thread synchronization lock, and as you can see, the lock used is essentiallyos_unfair_lock. The key point to understand here is,atomicOnly guaranteegetterMethods andsetterThread synchronization within a method, such as for a property@property (atomic, strong) NSMutableArray *dataArr;By shared resource, we mean that this attribute has many corresponding_dataArrMember variables.getterMethods andsetterThe method is really just two accesses_dataArrIs just a snippet of code,atomicIt only ensures that the two pieces of code are executed in the same thread, but_dataArrIt is entirely possible to access it directly elsewhere (without attribute access), which isatomicAreas not covered, for example

NSMutableArray *arr = self.dataArr;// Getter methods are safe
for (int i = 0; i<5; i++) {
    dispatch_async(dispatch_get_global_queue(0.0), ^{
        [arr addObject:@ "1"];// There will be multithreading _dataArr, and atomic cannot guarantee synchronization of threads
    });
}
Copy the code

So atomic is not a complete guarantee of multithreading safety.

In addition, in actual operation, we rarely manipulate the same attribute by multiple threads at the same time, so the problem of resource preemption for the attribute is not prominent. In addition, property is called too frequently in iOS code, and using atomic will lead to excessive use of lock, which consumes CPU resources. This is exactly what is scarce on mobile devices, so using atomic is not of much practical significance. We can directly lock specific places where multithreading risks may occur. In other words, we can add locks when necessary, so as to use CPU more effectively. So, in iOS, we almost never use atomic, and we use them mostly for MAC development.

Multithreaded read and write security

When we talk about depositing and withdrawing money, actually both depositing and withdrawing money involve reading and writing shared resources. Suppose we have two operations that contain only read and write operations respectively

- (void)read {
    sleep(1);
    NSLog(@"read");
}

- (void)write
{
    sleep(1);
    NSLog(@"write");
}
Copy the code

In fact, the purpose of the read operation is just to fetch data, not to modify the data, for example, I just take out the data to print, so it is no problem for multiple threads to read at the same time, do not need to consider the thread synchronization problem. Write operation is the root cause of multi-thread security problems. What we do to files in iOS is we typically do read and write operations, we write files and we read files. For read and write security, the solution is to read and write more, which needs to meet the following requirements:

  • Requirement 1: Only one thread can write data at a time
  • Requirement 2: Multiple threads are allowed to read at the same time
  • Requirement 3: Both read and write operations are not allowed at the same time. That is, read and write operations are mutually exclusive

First we review the iOS multithreaded synchronization solutions, can be found, we can through to the write lock, the realization of the above, request 1 】 【 lock, not read operation can be achieved, require 2 】 【 but there isn’t a solution can satisfy the request 2 】 【 under the premise of implementation, require 3 】 【 give it a try.

There are two solutions in iOS to meet the above read/write security requirements

  • pthread_rwlock:Read-write lock
  • dispatch_barrier_async:Asynchronous fence call





pthread_rwlock

With pthread_rwlock, threads waiting for the lock will go to sleep, using the following API

// Initialize the lock
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
// Read operations are locked
pthread_rwlock_rdlock(&lock);
// Read attempts to lock
pthread_rwlock_tryrdlock(&lock);
// Write operations are locked
pthread_rwlock_wrlock(&lock);
// Write attempts to lock
pthread_rwlock_trywrlock(&lock);
/ / unlock
pthread_rwlock_unlock(&lock);
/ / destruction of the lock
pthread_rwlock_destroy(&lock);
Copy the code

Because it is relatively simple to use, I will not cover the code examples here, just understand the effect of it.





dispatch_barrier_async

One important thing about dispatch_barrier_async is that the queue parameters you accept must be manually created (dispatch_queue_create). If you accept a serial queue or a global queue, Then this function has the same effect as dispatch_async. The specific principle of use is very simple

// Create a queue manually
    dispatch_queue_t queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queueThe ^ {/*
         θ―»ζ“δ½œδ»£η 
         */
    });
    dispatch_barrier_async(queueThe ^ {/* Write the operation code */
    })
Copy the code

Here is a code example

for (int i = 0; i < 10; i++) {
        dispatch_async(self.queue, ^{
            [self read];
        });
        
        dispatch_async(self.queue, ^{
            [self read];
        });
        
        dispatch_async(self.queue, ^{
            [self read];
        });
        
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
    }
Copy the code

So to sum up

So much for the solution to the synchronization problem and multi-threaded read and write security problem in iOS.