I. Overview and implementation scheme

Thread and process

Multithreading plays an important role in iOS, which is a necessary skill for every developer. Of course, it is also a technical point often tested in interviews. This article mainly explores the problems of multithreading in our actual development or interview. What is a thread? What is the relationship between it and process, what is the relationship between queue and thread, how to understand the concepts of synchronization, asynchronous, concurrent (parallel), serial, what are the common multi-threading schemes in iOS, and what are the thread synchronization technologies and so on.

A thread (in English: thread) is the smallest unit on which an operating system can schedule operations. In most cases, it is contained within the process and is the actual operating unit within the process. A thread refers to a single sequential flow of control in a process. A process can have multiple threads concurrently, each performing different tasks in parallel. — Wikipedia

Here is a process, that what is a process, to put it plainly is to refer to the operating system is running an application, such as wechat, Alipay app and so on are a process. A thread is the basic execution unit of a process. All tasks of a process are executed in a thread. This means that a process must have at least one thread, which is the main thread. Of course, in our actual use process can not only have a main thread, in order to improve the execution efficiency of the program, we often need to open up multiple sub threads to perform some time-consuming tasks, here leads to the concept of multithreading.

Multithreading (English: multithreading) is a technique that enables multiple threads of software or hardware to execute concurrently

According to the operating system and hardware, it is divided into two categories: software multithreading and hardware multithreading

  • Software multithreading: Even if the CPU can only run one thread, the operating system can quickly switch between threads at small intervals to give the user the illusion that multiple threads are running at the same time

  • Hardware multithreading: If the CPU has multiple cores, the operating system can allow each core to execute one thread, thus truly allowing multiple threads to execute simultaneously. Of course, since the number of tasks is far greater than the number of cores in the CPU, the operating system automatically schedules many tasks to each core in turn.

So that’s a bunch of stuff that Google came up with, it’s a little bit abstract, but let’s take a look at the actual multithreading techniques that we use in iOS development.

2. Multithreading in iOS

In iOS, there are four main multi-threading schemes: PThread, NSThread, GCD and NSOperation. PThread is a set of PURE C LANGUAGE API, which can be applied to Unix, Linux, Windows and other systems. The thread life cycle needs to be managed by programmers themselves, which is difficult to use. In our actual development is almost not used, here we do not do too much introduction, interested directly to Baidu. We’ll focus on three other options.

Let’s explain the life cycle of a thread, which is the process from creation to death. You typically go through a new – ready – run – block – die process.

  • New: initializes the thread object
  • Ready: A start message is sent to the thread object, which is added to the schedulable thread pool and waits for THE CPU to schedule.
  • Run: The CPU is responsible for scheduling the execution of threads in the pool of schedulable threads, and the state may switch back and forth between ready and run until the thread completes. State changes between ready and running are the responsibility of the CPU and cannot be interfered with by the programmer.
  • Block: When a predetermined condition is met, you can use sleep or lock to block a thread from executing
  • Death: The thread completes execution, exits, and is destroyed.

(1) NSThread

NSThread is apple official provide object-oriented operation thread technology, simple and convenient, you can directly operate the thread object, but you need to control the life cycle of the thread, let’s see apple official method given.

[1] Initialization method
  • Instance initialization method
- (instanceType)init API_AVAILABLE(MacOS (10.5), ios(2.0), Watchos (2.0), TVOs (9.0)) NS_DESIGNATED_INITIALIZER; - (instanceType)initWithTarget (ID) Target Selector (SEL) Selector Object (nullable ID)argument API_AVAILABLE(MacOS (10.5), Ios (2.0), watchos (2.0), tvos (9.0)); - (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));Copy the code

The corresponding initialization method:

NSThread *newThread = [[NSThread alloc]initWithTarget:self selector:@selector(demo:) object:@"Thread"]; NSThread *newThread=[[NSThread alloc]init]; NSThread *newThread= [[NSThread alloc]initWithBlock:^{ NSLog(@"Block"); }];Copy the code

Note that all three methods need to execute [newThread start] to start the thread after creation.

  • Class initialization method
+ (void) detachNewThreadWithBlock (void (^) (void)) block API_AVAILABLE (macosx (10.12), the ios (10.0), watchos (3.0), Tvos (10.0)); + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;Copy the code

Note that these two class methods are created and executable, and do not need to be manually enabled

[2] Cancel the exit

Since there is creation, there must be exit

// The instance method cancles the thread - (void)cancel; // Exit the class method + (void)exit;Copy the code
[3] Thread execution status
@property (readonly, getter=isExecuting) BOOL Executing API_AVAILABLE(MacOS (10.5), ios(2.0), watchos(2.0)) Tvos (9.0)); @Property (readonly, getter=isFinished) BOOL Finished API_AVAILABLE(MacOS (10.5), ios(2.0), watchos(2.0), Tvos (9.0)); @property (readonly, getter=isCancelled) BOOL cancelled API_AVAILABLE(MacOS (10.5), ios(2.0), Watchos (2.0), Tvos (9.0));Copy the code
[4] Communication between threads
@Interface NSObject (NSThreadPerformAdditions) /* * Perform the specified method * aSelector: method * arg: * wait: indicates whether to wait for the main thread to finish the task, and YES indicates to execute the following task. NO said execution with the following things * / - (void) performSelectorOnMainThread: (SEL) aSelector withObject: (nullable id) arg waitUntilDone: (BOOL) wait  modes:(nullable NSArray<NSString *> *)array; - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait; * aSelector: method * arg: * wait: indicates whether to wait for the thread to finish the task, and YES indicates to execute the following task. */ - (void)performSelector:(SEL)aSelector onThread:(NSThread *) THR withObject:(nullable id)arg WaitUntilDone :(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), Tvos (9.0)); - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE (macos (10.5), the ios (2.0), watchos (2.0), tvos (9.0)); /* * To open the child thread to execute the specified method * SEL: method * arg: Parameter * / - (void) performSelectorInBackground aSelector: (SEL) withObject: (nullable id) arg API_AVAILABLE (macos (10.5), Ios (2.0), watchos (2.0), tvos (9.0)); @endCopy the code

The methods we use to communicate between threads are actually these methods, all of which can be called by an inheriting NSObject instantiation object. Of course, there are other methods can also achieve the communication between threads, such as: GCD, NSOperation, NSMachPort port and other forms, we will use later in the introduction. Here’s a simple example: We download an image in a child thread and display it in the main thread:

- (void)touches began :(NSSet< touches *> *)touches withEvent:(UIEvent *) {// the child thread is playing with the view [self] performSelectorInBackground:@selector(download) withObject:nil]; } - (void) download {/ / picture network path NSURL * url = [NSURL URLWithString: @ "https://p3.ssl.qhimg.com/t011e94f0b9ed8e66b0.png"); / / download image data NSData * data = [NSData dataWithContentsOfURL: url]; UIImage *image = [UIImage imageWithData:data]; / / back to the main thread display images [self performSelectorOnMainThread: @ the selector (showImage) withObject: image waitUntilDone: YES]; } - (void)showImage:(UIImage *)image{ self.imageView.image = image; }Copy the code
[5] Other common methods
  • +(void)currentThreadGet current thread
  • +(BOOL)isMultiThreadedDetermines whether the thread is currently running on a child thread
  • -(BOOL)isMainThreadDetermine if it is on the main thread
  • +(void)sleepUntilDate:(NSDate *)date; + (void)sleepForTimeInterval:(NSTimeInterval)ti;Current thread sleep time

(2) GCD

Before introducing GCD, let’s take a look at some of the confusing concepts of multithreading

[1]. Synchronous, asynchronous, concurrent (parallel), serial
  • The main impact of synchronization and asynchrony is whether new threads can be started

Synchronous: Tasks are executed in the current thread without the ability to start a new thread. Asynchronous: Tasks are executed in a new thread with the ability to start a new thread

  • Concurrency and serialization have a major impact: how tasks are executed

Concurrent: Also called parallel, also called parallel queue, when multiple tasks are executed concurrently (simultaneously) serial: Also called serial queue, when one task is completed, then the next task is executed

The simple introduction of the concept is more abstract, let’s use it in practical terms to illustrate:

[2] Synchronous and asynchronous methods in GCD
  • Synchronous execution method:dispatch_sync()
  • Asynchronous execution method:dispatch_async()

Usage:

dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_async(dispatch_queue_t queue.dispatch_block_t block);
Copy the code

As you can see, these two methods take two arguments, the first of which requires passing in a queue of type dispatch_queue_T, and the second of which is the block to execute. Let’s look at the GCD queue

[3] Queue in GCD

There are three kinds of queues in GCD: serial queue, parallel queue, and main queue, which are also very simple to create:

  • Serial queues
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
Copy the code

The first parameter is the queue name, and the second is a macro definition. The two commonly used macros DISPATCH_QUEUE_SERIAL and DISPATCH_QUEUE_CONCURRENT indicate serial and parallel queues, respectively. The macros DISPATCH_QUEUE_SERIAL_INACTIVE and DISPATCH_QUEUE_CONCURRENT_INACTIVE indicate that the initialized serial queue and parallel queue are inactive, respectively. Look at the underlying implementation

dispatch_queue_attr_t
dispatch_queue_attr_make_initially_inactive(
		dispatch_queue_attr_t _Nullable attr);
		
#define DISPATCH_QUEUE_SERIAL_INACTIVE \
		dispatch_queue_attr_make_initially_inactive(DISPATCH_QUEUE_SERIAL)

#define DISPATCH_QUEUE_CONCURRENT_INACTIVE \
		dispatch_queue_attr_make_initially_inactive(DISPATCH_QUEUE_CONCURRENT)
Copy the code

It should be noted that tasks added to an initialised inactive queue must be changed to active by calling the dispatch_activate() function before they can be executed.

  • Parallel lines

There are two kinds of parallel queues: the first one is the global method of creating concurrent queues, which is also the method of creating concurrent queues created by the system for us

/* - QOS_CLASS_USER_INTERACTIVE * - QOS_CLASS_USER_INITIATED * - QOS_CLASS_DEFAULT * - QOS_CLASS_UTILITY * - QOS_CLASS_BACKGROUND */
//dispatch_get_global_queue(intptr_t identifier, uintptr_t flags); 

dispatch_queue_t queue = dispatch_get_global_queue(0.0);
Copy the code

There are two parameters, the first parameter identifies the thread execution priority, the second is the Apple reserved parameter: 0 will do. The second option is to manually create a concurrent queue

The first parameter is the name and the second is the identifier: DISPATCH_QUEUE_CONCURRENT, the concurrent queue identifier
dispatch_queue_t queue = dispatch_queue_create("myQueue",DISPATCH_QUEUE_CONCURRENT);
Copy the code
  • The home side column

The main queue is a special kind of serial queue

dispatch_queue_t queue = dispatch_get_main_queue();
Copy the code

A combination of synchronization, asynchrony, and queues can fulfill the need for multithreaded programming of tasks.

  1. Synchronous serial queue
  dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    for(NSInteger i = 0; i < 10; i++){
        dispatch_sync(queue1, ^{
            NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
        });
    }
//thread == <NSThread: 0x6000011b8880>{number = 1, name = main} i====n
Copy the code

As you can see, no new threads are started; tasks are executed in the main thread, and they are executed sequentially

  1. Synchronous parallel queue
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    for(NSInteger i = 0; i < 10; i++){
        dispatch_sync(queue1, ^{
            NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
        });
    }
// thread == <NSThread: 0x600001db8a00>{number = 1, name = main} i====n
Copy the code

Also execute sequentially in the main thread.

  1. Asynchronous serial queue
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    for(NSInteger i = 0; i < 10; i++){
        dispatch_async(queue1, ^{
            NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
        });
    }
Copy the code

Start child threads and execute tasks sequentially

  1. Asynchronous concurrent queue
 dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    for(NSInteger i = 0; i < 10; i++){
        dispatch_async(queue1, ^{
            NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
        });
    }
/*
thread == <NSThread: 0x6000024f9440>{number = 4, name = (null)} i====0
thread == <NSThread: 0x6000024f5340>{number = 5, name = (null)} i====2
thread == <NSThread: 0x6000024a8780>{number = 3, name = (null)} i====3
thread == <NSThread: 0x6000024ac6c0>{number = 6, name = (null)} i====1
thread == <NSThread: 0x6000024f4a80>{number = 8, name = (null)} i====5
thread == <NSThread: 0x6000024b0b40>{number = 7, name = (null)} i====4
thread == <NSThread: 0x60000249cd00>{number = 9, name = (null)} i====6
thread == <NSThread: 0x6000024b0980>{number = 10, name = (null)} i====7
thread == <NSThread: 0x6000024cb900>{number = 11, name = (null)} i====8
thread == <NSThread: 0x6000024f5340>{number = 5, name = (null)} i====9
*/
Copy the code

Multiple child threads are enabled and tasks are executed concurrently.

Note that dispatch_async() has the ability to create new threads, but that does not mean that using it will necessarily create new threads. For example, if the queue passed in is the main queue, the task will be executed in the main thread, and no new threads will be created.

  dispatch_queue_t queue1 = dispatch_get_main_queue();
    for(NSInteger i = 0; i < 10; i++){
        sleep(2);
        dispatch_async(queue1, ^{
            NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
        });
    }
//thread == <NSThread: 0x600002b24880>{number = 1, name = main} i====n
Copy the code

The main queue is a special kind of serial queue, where, as you can see from the print, execution is serial and no new threads are opened.

See the table below for details on how to perform the tasks

[4] dispatch_ group_ t Queue group

Dispatch_group_t is a practical way to construct a group of synchronous or asynchronous submission tasks into a single group that will be notified when all tasks are completed for further processing. Here’s a simple example:

dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, concurrentQueue, ^{ for (int i = 0; i < 10; i++) { NSLog(@"Task1 %@ %d", [NSThread currentThread], i); }}); dispatch_group_async(group, dispatch_get_main_queue(), ^{ for (int i = 0; i < 10; i++) { NSLog(@"Task2 %@ %d", [NSThread currentThread], i); }}); dispatch_group_async(group, concurrentQueue, ^{ for (int i = 0; i < 10; i++) { NSLog(@"Task3 %@ %d", [NSThread currentThread], i); }}); dispatch_group_notify(group, concurrentQueue, ^{ NSLog(@"All Task Complete"); });Copy the code
[5] Diapatch_barrier_async Barrier asynchronous call function

Diapatch_barrier_sync () is called synchronously and asynchronously. Dispatch_barrier_sync has to wait for the fence to finish before it executes the task behind the fence, and dispatch_barrier_Async doesn’t have to wait for the fence to finish, it just keeps going, what’s the use? In fact, the fence function is used most of the place or to achieve synchronous use of threads, such as we have such a requirement: how to use GCD to achieve multiple read single write file IO operation? How to achieve multiple read single write, look at the code:

@interface UserCenter() {// define a concurrent queue dispatch_queue_t concurrent_queue; // User data center, possibly multiple threads need data access NSMutableDictionary *userCenterDic; } @implementation UserCenter - (id)init {self = [super init]; Concurrent_queue = dispatch_queue_create("read_write_queue", "read_write_queue"); DISPATCH_QUEUE_CONCURRENT); UserCenterDic = [NSMutableDictionary Dictionary]; } return self; } - (id)objectForKey:(NSString *)key { __block id obj; Dispatch_sync (concurrent_queue, ^{obj = [userCenterDic objectForKey:key]; }); return obj; } - (void)setObject:(id)obj forKey:(NSString *)key {dispatch_barrier_async(concurrent_queue, ^{ [userCenterDic setObject:obj forKey:key]; }); }Copy the code

As you can see, by putting a write into a fence function, you can achieve thread synchronization

Note: With dispatch_barrier_async, this function can only be used with a custom concurrent queue dispatch_queue_t. Do not use global concurrent queue: dispatch_get_global_queue; otherwise, dispatch_barrier_async has no effect.

[6] Thread deadlock

Let’s start with two examples:

dispatch_queue_t queue = dispatch_get_main_queue(); Dispatch_sync (queue, ^{NSLog(@" execute task 2"); }); // Dispatch_queue_t myQueue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL); dispatch_async(myQueue, ^{ NSLog(@"1111,thread====%@",[NSThread currentThread]); dispatch_sync(myQueue, ^{ NSLog(@"2222,thread====%@",[NSThread currentThread]); }); }); // 1111,thread====<NSThread: 0x6000022dd880>{number = 5, name = (null)} // crashCopy the code

As you can see from the example above, you cannot add tasks to the current serial queue synchronously, otherwise a deadlock will result in crash. Conditions for thread deadlock: Adding tasks to the current serial queue using sync results in a deadlock.

(3) NSOperation

NSOperation is Apple’s object-oriented encapsulation of GCD, its underlying implementation is based on GCD, compared to GCD it adds more practical functions

  • Task dependencies can be added
  • Control of task execution status
  • Set the maximum number of concurrent requests

It has two core classes: NSOperation and NSOperationQueue. NSOperation is the encapsulation of tasks. The encapsulated tasks can be assigned to different NSOperationQueues for serial queue execution or concurrent queue execution.

[1] NSOperation

NSOperation is an abstract class and cannot be used directly. You must use subclasses of NSOperation. There are three ways to use NSOperation: NSInvocationOperation, NSBlockOperation, custom subclass inheritance NSOperation, the first two are encapsulated for us by Apple, can be used directly, custom subclass, we need to implement the corresponding method.

  • NSBlockOperation & NSInvocationOperation

Use:

// Create an NSBlockOperation object, NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{for (int I = 0; I < 5; I ++) { NSLog(@"Task1 %@ %d", [NSThread currentThread], i); } }]; /* Create an NSInvocationOperation object, This method can take one parameter which is object */ NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task:) object:@"Hello, World!"] ; // Run [operation start]; [invocationOperation start]; // Print: Task1 <NSThread: 0x6000019581C0 >{number = 1, name = main} 0Copy the code

You can see that the two task objects are created to execute the task without starting a new thread. NSBlockOperation has one more addExecutionBlock method than NSInvocationOperation,

 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; i++)
        {
            NSLog(@"task1=====%@ %d", [NSThread currentThread], i);
        }
    }];
    
    [operation addExecutionBlock:^{
       
        NSLog(@"task2=====%@",[NSThread currentThread]);
    }];
    
    [operation addExecutionBlock:^{
       
        NSLog(@"task3=====%@",[NSThread currentThread]);
    }];
    
    [operation addExecutionBlock:^{
       
        NSLog(@"task4=====%@",[NSThread currentThread]);
    }];
    
    [operation start];
/*
task3=====<NSThread: 0x600000509840>{number = 6, name = (null)}
task4=====<NSThread: 0x600000530200>{number = 3, name = (null)}
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 0
task2=====<NSThread: 0x600000511680>{number = 5, name = (null)}
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 1
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 2
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 3
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 4
*/
Copy the code

A task appended using an addExecutionBlock is executed concurrently. If the operation has more than one task, then a child thread will be enabled to execute the task concurrently. The appended task may not be a child thread, but may also be the main thread.

[2] NSOperationQueue

There are two types of NSOperationQueue. One is the mainQueue obtained by [NSOperationQueue mainQueue]. There is also a self-created queue [[NSOperationQueue alloc] init], which has both concurrency and serialization capabilities and can be set to a maximum number of concurrency.

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 5; i++) { NSLog(@"task1=====%@ %d", [NSThread currentThread], i); } }]; NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 5; i++) { NSLog(@"Task2===== %@ %d", [NSThread currentThread], i); } }]; NSOperationQueue *queues = [[NSOperationQueue alloc] init]; [queues setMaxConcurrentOperationCount:2]; // Set the maximum number of concurrent queues. If this is set to 1, [queues addOperation:operation] is executed sequentially. [queues addOperation:operation2]; /* Task2===== <NSThread: 0x600000489940>{number = 4, name = (null)} 0 task1=====<NSThread: 0x6000004e15c0>{number = 5, name = (null)} 0 */Copy the code

This example has two tasks, and if the maximum concurrency is set to 2, two threads will be created to execute both tasks concurrently. If set to 1, it will be executed in a new thread.

[3] Task dependency

An addDependency can establish a dependency between two tasks, for example [operation2 addDependency:operation1]. If task 2 depends on task 1, task 2 will not be executed until task 1 has completed. Here is an example

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 5; i++) { NSLog(@"task1=====%@ %d", [NSThread currentThread], i); } }]; NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 5; i++) { NSLog(@"Task2===== %@ %d", [NSThread currentThread], i); } }]; NSOperationQueue *queues = [[NSOperationQueue alloc] init]; [queues setMaxConcurrentOperationCount:2]; [operation addDependency:operation2]; // Set task dependency [operation addDependency:operation2]; [queues addOperation:operation]; [queues addOperation:operation2]; /* Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 0 Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 1 Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 2 Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 3 Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 4 task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 0 task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 1 task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 2 task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 3 task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 4 */Copy the code

Again, set [operation addDependency:operation2]; You can see that the operation of task 1 is performed only after task 2 is completed.

[4] Customize NSOperation

Task execution state is controlled relative to the custom NSOperation subclass. There are two types of custom NSOperation subclasses:

  1. rewritemainmethods

Only override the main method of operation. The main method contains the task to be executed. The system low-level control changes the completion status of the task and the exit of the task. Look at an example

#import "TestOperation.h" @interface TestOperation () @property (nonatomic, copy) id obj; @end @implementation TestOperation - (instancetype)initWithObject:(id)obj{ if(self = [super init]){ self.obj = obj; } return self; } - (void)main{self.obj,[NSThread currentThread],self.obj,[NSThread currentThread]); }Copy the code

call

TestOperation *operation4 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@" I'm task 4"]]; [operation4 setCompletionBlock:^{NSLog(@" completion thread===%@",[NSThread currentThread]);}]; [operation4 start]; Thread ===<NSThread: 0x6000008D8880 >{number = 1, name = main} 0x60000089fa40>{number = 7, name = (null)}Copy the code

You can see that the main method of operation is executed on the main thread, but the callback setCompletionBlock is asynchronous, so that doesn’t seem to help, don’t worry, let’s put it on the queue and let’s see, in the example above, put it on the queue

NSOperationQueue *queue4 = [[NSOperationQueue alloc] init]; TestOperation *operation4 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@" I'm task 4"]]; TestOperation *operation5 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@" I am task 5"]]; TestOperation *operation6 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@" I am task 6"]]; [queue4 addOperation:operation4]; [queue4 addOperation:operation5]; [queue4 addOperation:operation6]; Thread ===<NSThread: 0x600001fc8200>{number = 5, name = (null)} Thread ===<NSThread: 0x600001fc8200> Thread ===<NSThread: 0x600001FD7c80 >{number = 7, name = (null)}Copy the code

At this point, you can see the concurrent execution of the task. When the main method of operation finishes executing, they call their respective dealloc methods to release the task. The life cycle of the task ends. If we want tasks 4, 5, and 6 to execute in reverse order, we can add task dependencies

[operation4 addDependency:operation5]; [operation5 addDependency:operation6]; Thread ===<NSThread: 0x600003D04680 >{number = 6, name = (null)} Thread ===<NSThread: 0x600003D04680 >{number = 6, name = (null)}Copy the code

This seems fine, but what if we have an asynchronous task (such as a network request) in our operation, and we want task 6 to call task 5 after completing the request, and task 4 to call after task 5 succeeds? We describe this in section 2 in the method summary of moving on to the next request after multiple requests have been completed. 2. Override the start method by overriding the main method so that tasks can be executed sequentially. If you want tasks to execute concurrently, you need to override the start method. There is a big difference: if you just override main, the method executes and the entire operation is removed from the queue. If you are a custom operation and it is a proxy for some class that happens to have asynchronous methods, then the proxy will not be found and the application will go wrong. The start method, however, does not change its finish property even after execution, so you can control the lifecycle of the operation. Then manually cancel the operation after the task is completed.

@interface TestStartOperation : NSOperation - (instancetype)initWithObject:(id)obj; @property (nonatomic, copy) id obj; @property (nonatomic, assign, getter=isExecuting) BOOL executing; @property (nonatomic, assign, getter=isFinished) BOOL finished; @end @implementation TestStartOperation @synthesize executing = _executing; @synthesize finished = _finished; - (instancetype)initWithObject:(id)obj{ if(self = [super init]){ self.obj = obj; } return self; } - (void)start{self.executing is a YES before the task starts. Self. Executing is a YES; NSLog(@thread ===%@",self.obj,[NSThread currentThread]); Subtask = null; subtask = null; subtask = null; subtask = null; subtask = null; subtask = null; subtask = null; subtask = null self.finished = YES; return; } NSString *str = @"https://www.360.cn"; NSURL *url = [NSURL URLWithString:str]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLSession *session = [NSURLSession sharedSession]; __weak typeof(self) weakSelf = self; NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {/ / NSLog (@ "response = = % @", response); NSLog (@ "TASK completion: = = = = % @ Subthread ====%@", weakself. obj,[NSThread currentThread]); subthread ====%@", weakself. obj,[NSThread currentThread]); }]; [task resume]; } - (void)setExecuting:(BOOL)executing KVO [self willChangeValueForKey:@"isExecuting"]; _executing = executing; // Call KVO notifying [self didChangeValueForKey:@"isExecuting"]; } - (BOOL)isExecuting { return _executing; } - (void)setFinished:(BOOL)finished {// Call KVO notification manually [self willChangeValueForKey:@"isFinished"]; _finished = finished; // Call KVO notification [self didChangeValueForKey:@"isFinished"]; } - (BOOL)isFinished { return _finished; } - (BOOL)isAsynchronous { return YES; } - (void)dealloc{ NSLog(@"Dealloc %@",self.obj); }Copy the code

Execution and Results

NSOperationQueue *queue4 = [[NSOperationQueue alloc] init]; TestStartOperation * Operation4 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@" I'm task 4"]]; TestStartOperation * Operation5 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@" I am task 5"]]; TestStartOperation * Operation6 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@" I am task 6"]]; // Set task dependency [operation4 addDependency:operation5]; [operation5 addDependency:operation6]; [queue4 addOperation:operation4]; [queue4 addOperation:operation5]; [queue4 addOperation:operation6]; Thread ===<NSThread: 0x600002bb8480>{number = 6, name = (null)} TASK complete :==== 0x600002BD4d80 >{number = 8, name = (null)} 0x600002bb0300>{number = 5, name = (null)} TASK complete :==== thread====<NSThread: 0x600002bb0300>{number = 5, name = (null)} 0x600002BFB080 >{number = 7, name = (null)} TASK completed :==== thread====<NSThread: 0x600002bfb080>{number = 7, Name = (null)} 2021-06-22 17:57:56.436591+0800 Interview01- Print [15994:9172130] Dealloc I am task 4 2021-06-22 17:57:56.436690+0800 Interview01- Print [15994:9172130] Dealloc I am Task 5 2021-06-22 17:57:56.436784+0800 Interview01- Print [15994:9172130] Dealloc I am Task 6 */Copy the code

In this example, we manually set the task’s self.executing and self.finished states after the request is complete, and manually trigger the KVO. The queue will listen for the task’s execution status. Because we set the task dependency, task 5 will be executed only when the request from Task 6 is complete, and task 4 will be executed only when the request from Task 5 is complete. Finally, each task is removed from the queue and released. This solves a problem that was not solved by overriding main above.

Ii. Practical application

Method summary of moving on to the next request after multiple requests have completed

In our work, we often encounter requests where one request depends on the results of another request, or multiple requests are sent together and then all the results are retrieved and the operation continues. Summary of common methods according to these situations:

1. UseGCDthedispatch_group_timplementation

Requirement: the request is executed sequentially, and the result is called back after the execution is complete

NSString *str = @"https://www.360.cn"; NSURL *url = [NSURL URLWithString:str]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLSession *session = [NSURLSession sharedSession]; dispatch_group_t downloadGroup = dispatch_group_create(); for (int i=0; i<10; i++) { dispatch_group_enter(downloadGroup); NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {NSLog(@" request completed =%d", I); dispatch_group_leave(downloadGroup);}]; [task resume]; } dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ NSLog(@"end"); }); /* 2021-06-22 18:37:56.786878+0800 Interview01- Print [17121:9352056] End of Request: 0 2021-06-22 18:37:56.787770+0800 Interview01- Print [17121:9352057] End of Request: 1 2021-06-22 18:37:56.788492+0800 Interview01- Print [17121:9352057] End of Request: 2 2021-06-22 18:37:56.789148+0800 Interview01- Print [17121:9352057] End of Request: 3 2021-06-22 18:37:56.789837+0800 Interview01- Print [17121:9352057] End of Request: 4 2021-06-22 18:37:56.790433+0800 Interview01- Print [17121:9352059] End of Request: 5 2021-06-22 18:37:56.791117+0800 Interview01- Print [17121:9352059] End of Request: 6 2021-06-22 18:37:56.791860+0800 Interview01- Print [17121:9352059] End of Request: 7 2021-06-22 18:37:56.792614+0800 Interview01- Print [17121:9352059] End of Request: 8 2021-06-22 18:37:56.793201+0800 Interview01- Print [17121:9352059] End of Request: 9 2021-06-22 18:37:56.804529+0800 Interview01- Print [17121:9351753] end*/Copy the code

Main methods:

  • dispatch_group_t downloadGroup = dispatch_group_create();Creating a queue Group
  • dispatch_group_enter(downloadGroup);Called before each request is executed
  • dispatch_group_leave(downloadGroup);The leave method is called when the request is complete
  • dispatch_group_notify()Call back to the block when all requests are completed
  • Enter and leave must be used together. For every enter, there must be several leaves

2. GCDA semaphoredispatch_semaphore_t

(1). Requirements: Execute multiple requests sequentially, and call back to end after completion of execution

NSString *str = @"https://www.360.cn"; NSURL *url = [NSURL URLWithString:str]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLSession *session = [NSURLSession sharedSession]; dispatch_semaphore_t sem = dispatch_semaphore_create(0); for (int i=0; i<10; i++) { NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable Response, NSError * _Nullable error) {NSLog(@" Request end: %d",i); dispatch_semaphore_signal(sem); }]; [task resume]; dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); } dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"end"); });Copy the code

The main method

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_semaphore_signal(sem);
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
Copy the code

Dispatch_semaphore semaphore is a counter based multi-threaded synchronization mechanism,dispatch_semaphore_signal(SEM); For count +1 operation, dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); If the value of “dispatch_semaphore_wait” is less than zero, the thread will be blocked until the semaphore is greater than or equal to zero. When the first for loop is executed dispatch_SEMaphore_WAIT blocks the thread until dispatch_SEMaphore_signal is executed and the next for loop continues, and so on.

(2). Requirements: Multiple requests are executed at the same time, and they are called back to end after completion of execution

NSString *str = @"https://www.360.cn"; NSURL *url = [NSURL URLWithString:str]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLSession *session = [NSURLSession sharedSession]; dispatch_semaphore_t sem = dispatch_semaphore_create(0); __block int count = 0; for (int i=0; i<10; i++) { NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { NSLog(@"%d---%d",i,i); count++; if (count==10) { dispatch_semaphore_signal(sem); count = 0; }}]; [task resume]; } dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"end"); }); /* 2021-06-23 09:47:49.723576+0800 Interview01- Print [21740:9823752] Request Completed: 0 2021-06-23 09:47:49.741118+0800 Interview01- Print [21740:9823751] Request Completed: 1 2021-06-23 09:47:49.756781+0800 Interview01- Print [21740:9823752] Request Completed: 3 2021-06-23 09:47:49.765250+0800 Interview01- Print [21740:9823752] Request Completed: 2 2021-06-23 09:47:49.773008+0800 Interview01- Print [21740:9823756] Request Completed: 4 2021-06-23 09:47:49.797809+0800 Interview01- Print [21740:9823751] Request Completed: 5 2021-06-23 09:47:49.801775+0800 Interview01- Print [21740:9823751] Request Completed: 6 2021-06-23 09:47:49.805542+0800 Interview01- Print [21740:9823751] Request Completed: 7 2021-06-23 09:47:49.814714+0800 Interview01- Print [21740:9823751] Request Completed: 8 2021-06-23 09:47:49.850517+0800 Interview01- Print [21740:9823753] Request Completed: 9 2021-06-23 09:47:49.864394+0800 Interview01- Print [21740:9823591] end */Copy the code

The for loop runs and blocks the current thread (currently the main thread, but you can also put the code in a child thread). When all 10 requests have been completed, it sends a signal to continue the process.

Use 3.NSOperationwithGCDUse a combination of

Requirements: Two network requests, the first relying on the callback result of the second

By customizing the operation implementation, we override its main method

@interface CustomOperation : NSOperation @property (nonatomic, copy) id obj; - (instancetype)initWithObject:(id)obj; @end @implementation CustomOperation - (instancetype)initWithObject:(id)obj{ if(self = [super init]){ self.obj = obj; } return self; } - (void)main{// Create the semaphore and set the count to 0 by default. Dispatch_semaphore_t sema = dispatch_semaphore_create(0); NSLog(@" start task %@",self.obj); NSString *str = @"https://www.360.cn"; NSURL *url = [NSURL URLWithString:str]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {NSLog(@"TASK completed :====% @thread ====%@",self.obj,[NSThread currentThread]) dispatch_semaphore_signal(sema); }]; [task resume]; // If the count is 0, wait for dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); }Copy the code

Calls and results

NSOperationQueue *queue3 = [[NSOperationQueue alloc] init]; [queue3 setMaxConcurrentOperationCount:2]; CustomOperation *operation0 = [[CustomOperation alloc] initWithObject:@" I am task 0"]; CustomOperation *operation1 = [[CustomOperation alloc] initWithObject:@" I am task 1"]; CustomOperation *operation2 = [[CustomOperation alloc] initWithObject:@" I am task 2"]; CustomOperation *operation3 = [[CustomOperation alloc] initWithObject:@" I am task 3"]; [operation0 addDependency:operation1]; [operation1 addDependency:operation2]; [operation2 addDependency:operation3]; [queue3 addOperation:operation0]; [queue3 addOperation:operation1]; [queue3 addOperation:operation2]; [queue3 addOperation:operation3]; Thread ====<NSThread: 0x6000039C3340 >{number = 5, name = (null)} 0x6000039Ece80 >{number = 7, name = (null)} Start TASK I am TASK 1 TASK completed :==== I am TASK 1 thread====<NSThread: 0x6000039C3340 >{number = 5, name = (null)} Start TASK I am TASK 0 TASK completed :==== I am TASK 0 thread====<NSThread: 0x6000039c3d00>{number = 6, name = (null)} */Copy the code
  • Task dependencies are set and added to the queue to meet our needs
  • Since the task is an asynchronous callback inside, you can see whether the execution inside the task depends ondispatch_semaphore_tTo implement the
  • Or you can rewrite itstartMethod implementation, which we have already introduced in the previous section, will not be repeated here.

3. To summarize

This article is a bit long, but there are a few things that we haven’t covered, such as thread locking and suspending and cancellations of NSOperationQueue, which are commonly used in iOS.

Due to the author’s limited level, it is hard to avoid mistakes, if there is a problem, please give advice.

References:

Apple official — Concurrent Programming Guide: Operation Queues

IOS GCD dispatch_semaphore