iOS multithreading you don't know

Posted by pwicks on Thu, 23 May 2019 18:31:02 +0200


Pictures from the Web

Programmers use their limited lives to pursue unlimited knowledge.

Make clear beforehand

First of all, I do not intentionally want to be a headline party, nor do I want to stir-fry cold rice. I just want to change my position to see multi-threading. Most of the content of this article is about how to create deadlocks. However, it is still too shallow to take the first step.Open your Xcode to verify these deadlocks.

Multithreading tips

The following are three ways to achieve multithreading:

  • NSThread
  • GCD
  • NSOperationQueue

about Specific methods used No more details, let's see what they don't know

1. Behind the lock

NSLock is based on POSIX threads, which use mutex to synchronize threads.

Mutexes, or mutexes, are a fundamental mechanism that the pthread library provides to solve this problem.Mutex is a lock that guarantees three things:

  • Atomicity - Locking a mutex is an atomic operation, indicating that the operating system guarantees that if you have already locked a mutex, no other threads will be able to lock it at the same time.

  • Singularity - If a thread locks a mutex, it is guaranteed that no other thread can lock the mutex until the thread releases the lock;

  • Non-busy wait - If a thread (Thread 1) attempts to lock a lock locked by Thread 2, Thread 1 suspend s and does not consume any CPU resources until Thread 2 releases the lock.Thread 1 wakes up and resumes execution, locking the mutex.

2. About life cycle

By exiting a thread with the [NSThread exit] method, NSThread can terminate the task being executed immediately (which may cause a memory leak, which is not very deep here).Even if you can do this in the main thread, the main thread will exit and the app will no longer be able to respond to events.cancel can accomplish similar things by acting as a flag bit and will continue executing if nothing is done.

GCD and NSOperationQueue can cancel tasks that are not started in the queue, but they can't do anything about tasks that have already started.

Implementation\Function Thread life cycle Cancel Task
NSThread Manual management Stop execution immediately
GCD Automatic management Cancel tasks not performed in the queue
NSOperationQueue Automatic management Cancel tasks not performed in the queue

3. Parallelism and Concurrency

There is a small trap in seeing many "Just watch me" series articles referring to concurrent queues that confuse the concepts of concurrency and parallelism.Let's first look at the differences between them:


Concurrency and Parallelism

As you can see from the diagram, parallelism is really multithreaded, while concurrency is simply switching between tasks.Generally, a multicore CPU can execute multiple threads in parallel, whereas a single-core CPU actually has only one thread, and multiplexing achieves near-simultaneous execution.In iOS, concurrentQueue and globalQueue both achieve concurrent results in terms of thread usage in Xcode, although the names are concurrent.

4. Queues and Threads

Queues are saved and managed tasks. Tasks are added to a queue, and tasks are executed in the order they are added to the queue.If it is a global queue and a parallel queue, the system creates new threads based on system resources to handle the tasks in the queue. Thread creation, maintenance and destruction are managed by the operating system, and the queue itself is thread-safe.

When using NSOperationQueue for multithreading, you can control the total number of threads and their dependencies, whereas GCD can only choose parallel or serial queues.

Resource Competition

Multi-threaded execution of tasks at the same time can improve the execution efficiency and response time of programs, but it is inevitable that multi-threaded operations will encounter the same resource at the same time.An example of a resource competition issue you saw earlier is:

@property (nonatomic, strong) NSString *target; 
dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) { 
    dispatch_async(queue, ^{ 
        self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i]; 
    }); 
}

Solution:

  • @property (nonatomic, strong) NSString *target; changes nonatomic to atomic.
  • Change the parallel queue DISPATCH_QUEUE_CONCURRENT to the serial queue DISPATCH_QUEUE_SERIAL.
  • Execute dispatch_async asynchronously instead of dispatch_sync synchronously.
  • Assignments are either @synchronized or locked.

These methods are designed to solve the problem from the point of avoiding simultaneous visits, and there are better ways to welcome sharing.

Pattern Deadlock

Everything has two sides, just as multithreading can improve efficiency, it can also cause problems of resource competition.While locking ensures multithreaded data security, carelessness can also cause problems: deadlocks.

1. NSOperationQueue

Since NSOperationQueue is highly encapsulated and very simple to use and generally does not produce any moths, the following example demonstrates a poor example of how we can perform tasks in an orderly manner by controlling affiliation between NSOperations, but if we depend on each other or cycle affiliation, all tasks will fail to start.

 NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"lock 1 start");
        [NSThread sleepForTimeInterval:1];
        NSLog(@"lock 1 over");
    }];
    
    NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"lock 2 start");
        [NSThread sleepForTimeInterval:1];
        NSLog(@"lock 2 over");
    }];
    
    NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"lock 3 start");
        [NSThread sleepForTimeInterval:1];
        NSLog(@"lock 3 over");
    }];
    
    // Circular Subordination
    [blockOperation2 addDependency:blockOperation1];
    [blockOperation3 addDependency:blockOperation2];
    [blockOperation1 addDependency:blockOperation3]; // Cyclic culprit

    // Mutual subordination
    //[blockOperation1 addDependency:blockOperation2];
    //[blockOperation2 addDependency:blockOperation1];

    [_operationQueue addOperation:blockOperation1];
    [_operationQueue addOperation:blockOperation2];
    [_operationQueue addOperation:blockOperation3];

Has anyone tried this scenario, so if you're curious, try it!

[blockOperation1 addDependency:blockOperation1];

2. GCD

Most developers know that synchronizing tasks in the main thread can cause deadlocks, so let's see what other situations can cause deadlocks or similar problems.

a. Synchronous execution on the main thread caused the EXC_BAD_INSTRUCEION error:

- (void)deadlock1 {
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"task 1 start");
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"task 1 over");
    });
}

b. Similar to the synchronous execution of the main thread, the synchronous execution of tasks is nested in a serial queue, the synchronous queue task1 cannot execute task2 until it is finished, and the nested task2 in task1 causes task1 to be doomed to fail.

- (void)deadlock2 {
    dispatch_queue_t queue = dispatch_queue_create("com.xietao3.sync", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{ // Asynchronous here also causes waiting for each other
        NSLog(@"task 1 start");
        dispatch_sync(queue, ^{
            NSLog(@"task 2 start");
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"task 2 over");
        });
        NSLog(@"task 1 over");
    });
}

Nested synchronous execution tasks are really bug gy prone, but not absolute. Replacing synchronous queue DISPATCH_QUEUE_SERIAL with parallel queue DISPATCH_QUEUE_CONCURRENT is an easy solution.In the case of the modified parallel queue, task1 still has to execute the nested task2 first, and when task2 starts execution, the queue will execute task2 from a new thread, and task1 will continue execution after task2 is finished.

c. Many people have the impression that asynchronous execution is not easy to wait for each other. Indeed, even in a serial queue, asynchronous tasks wait for the current task to execute before they start unless you add some unhealthy spices.

- (void)deadlock3 {
    dispatch_queue_t queue = dispatch_queue_create("com.xietao3.asyn", DISPATCH_QUEUE_SERIAL);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    dispatch_async(queue, ^{
        __block NSString *str = @"xietao3";                             // Thread 1 Create Data
        dispatch_async(queue, ^{
            str = [NSString stringWithFormat:@"%ld",[str hash]];        // Thread 2 Processing Data
            dispatch_semaphore_signal(semaphore);
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"%@",str);                                               // Thread 1 uses processed data
    });
}

d. Regular deadlocks, which are locked again when they are already locked, create a situation where they wait for each other.

  if (!_lock) _lock = [NSLock new];
  dispatch_queue_t queue = dispatch_queue_create("com.xietao3.sync", DISPATCH_QUEUE_CONCURRENT);
    
    [_lock lock];
    dispatch_sync(queue, ^{
        [_lock lock];
        [NSThread sleepForTimeInterval:1.0];
        [_lock unlock];
    });
    [_lock unlock];

To solve this problem, replace NSLock with recursive lock NSRecursiveLock, which acts like a normal door lock, unlocking in a counterclockwise loop after turning the lock clockwise, and unlocking in two clockwise loops as well.Here's a recursive example:

// The following code can be interpreted as turning 10 locks clockwise and 10 unlocks counterclockwise
- (void)recursivelock:(int)count {
    if (count>10) return;
    count++;
    if (!_recursiveLock) _recursiveLock = [NSRecursiveLock new];

    [_recursiveLock lock];
    NSLog(@"task%d start",count);
    [self recursivelock:count];
    NSLog(@"task%d over",count);
    [_recursiveLock unlock];
}

3. Other

In addition to mutexes and recursive locks mentioned above, there are other locks:

  • OSSpinLock
  • pthread_mutex (bottom implementation of locks in OC)
  • NSConditionLock (conditional locks, more deadlocks for beginners)
  • NSCondition (bottom implementation of conditional lock)
  • @synchronized (object lock)

Most locks trigger deadlocks in the same way as mutexes, NSConditionLock is more flexible to use, and spin locks have bugs, although they explode the table, and you want to know more about locks. Click here And don't forget to verify it yourself while you're watching it, remember it more deeply as you read and write.

summary

The article about multi-threading and lock is rotten. This article tries to look at the problem from a new angle and try not to write those duplicate contents. I hope it will help you. If the contents are wrong, please point out.

For reprinting, please note the original text: http://www.jianshu.com/p/0ed2858e0b51

Topics: xcode iOS