preface

This article mainly records the iOS multithreading in the basic concept and use methods, here to make a record. One is to deepen the impression, after their own use can also be convenient to find and review, two is in their own learning process, there is always daniu’s article as a guide, I hope I can also give some help to the people who need this knowledge.

The Demo of this article can go to my Github MultiThreadDemo to view the source code, if there is any improper, I hope you point out.

GCD knowledge points, the follow-up will continue to update…

1, an overview of the

1.1 Preparation

1.1.1 Synchronous and Asynchronous

  • Synchronization: You must wait for the current statement to complete before executing the next statement.
  • Asynchronous: The next statement can be executed without waiting for the current statement to complete.

1.1.2 Processes and Threads

  • process
    • Concept: a running application on a system.
    • Features: Each process runs in its own dedicated and protected memory space. Different processes are independent of each other.
  • thread
    • A thread is the basic execution unit of a process. All tasks of a process are executed in a thread.
    • Features: A thread executes tasks sequentially. If you want a thread to perform multiple tasks, you can only perform them sequentially, one by one. That is, a thread can only perform one task at a time

1.2 Basic concepts and principles of multi-threading

  • Concept: A process can start multiple threads, and multiple threads can concurrently (simultaneously) perform different tasks.
  • Principle: The CPU can only handle one thread at a time, that is, only one thread is working and multiple threads are executing at the same time. In fact, the CPU quickly switches between multiple threads. If the CPU schedules threads fast enough, it creates the “illusion” that multiple threads are executing concurrently.

1.3 the advantages and disadvantages

  • advantages

    1. Can improve the execution efficiency of the program.
    2. Appropriately improve resource utilization (CPU, memory utilization)
  • disadvantages

    1. Starting threads occupies a certain amount of memory space. If a large number of threads are started, a large amount of memory space will be occupied, thus reducing the performance of the program.
    2. The more threads there are, the more overhead the CPU has on scheduling threads.
    3. The more threads there are, the more complex the program design becomes: inter-thread communication, multi-thread data sharing, etc.

1.4 summarize

  1. In fact, using multiple threads, because threads are opened, inevitably costs performance, but can improve the user experience. Therefore, on the premise of ensuring good user experience, threads can be properly opened.

  2. In iOS, a main thread (UI thread) is created after each process starts. Since the child threads in iOS are independent of Cocoa Touch except for the main thread, only the main thread can update the UI. The use of multithreading in iOS is not complicated, the key is how to control the execution sequence of each thread and deal with the problem of resource competition.

Let’s take a look at several common implementations of multithreading on iOS.

2. Three multithreading schemes

2.1 the Thread

2.1.1 to introduce

  • Lightweight thread development compared to GCD and Operation.
  • It is relatively simple to use, but requires manual management of the life cycle of creating threads, synchronization, asynchrony, locking, and so on.

2.1.2 Basic Usage

This section describes three ways to create a Thread. The Target class in the following three creation modes is:

class Receiver: NSObject {
    @objc func runThread() {
        print(Thread.current)
    }
}
Copy the code
  1. Create an instance and start it manually
// 1. Create threadlet thread_one = Thread(target: Receiver(), selector: #selector(Receiver.runThread), object: nil)

letthread_two = Thread { // TODO } // 2. Thread_one.start () thread_two-.start ()Copy the code
  1. Class method is created and started
/ / create a Thread start automatically after the Thread Thread. DetachNewThread {/ / TODO} Thread. DetachNewThreadSelector (#selector(Receiver.runThread), toTarget: Receiver(), with: nil)
Copy the code
  1. Implicitly create and start
letObj = Receiver() // Implicitly create and start the thread obj. PerformSelector (inBackground: #selector(obj.runThread), with: nil)
Copy the code

2.1.3 Communication between threads

// Execute the specified method performSelector(onMainThread: Selector, with: Any? .waitUntilDone: Bool, modes: [String]?) Perform (aSelector: Selector, on: Thread, with: Any? .waitUntilDone: Bool, modes: [String]?)
Copy the code
  • Any? : Data to be transmitted
  • modes? : Runloop Mode values

2.1.4 Thread Priority

Receives a Double when setting thread priority.

The value ranges from 0.0 to 1.0.

For newly created threads, the value of Priority is typically 0.5. However, because the priority is determined by the system kernel, there is no guarantee what this value will be.

var threadPriority: Double { get set }
Copy the code

2.1.5 Thread Status and Lifecycle

Functions related to thread state and lifecycle:

Class func sleep(until date: date) class func sleep(until date: date)forTimeInterval ti: TimeInterval) // - Cancels the operation of the thread. After the current operation is performed, the task will not be continued. Func cancel() // - Forcibly stops the thread and enters the death state class funcexit(a)Copy the code

Cancel () : Instead of canceling the current thread immediately, the method changes the thread’s state to indicate that it should exit.

Exit () : This method should be avoided because it does not give the thread an opportunity to clean up any resources it has allocated during execution.

  • New (New) : Instantiates the thread object
  • Runnable: A start message is sent to a thread object that is added to the pool of schedulable threads waiting for CPU scheduling.
  • Running: The CPU is responsible for scheduling the execution of threads in the schedulable thread pool. Before the thread completes execution, the state may switch back and forth between ready and run. State changes between ready and run are the responsibility of the CPU, not the programmer.
  • Blocked: When a predetermined condition is met, a thread execution can be Blocked using sleep or locking. SleepForTimeInterval, sleepUntilDate, @synchronized(self) :(mutexes).
  • Dead: Normal death, thread execution complete. Unnatural death, aborting execution inside a thread/aborting a thread object in the main thread when a condition is met

The system also defines several NSNotifications. If you are interested in changes to the current thread state, you can subscribe to these notifications:

Static when the last thread other than the main thread exitsletNSDidBecomeSingleThreaded: NSNotification. Name / / when a thread to receiveexit() Messages are staticletNSThreadWillExit: nsnotification. Name // Published when the first child thread other than the main thread is created, and no further notification is given when the second child thread is created. / / notice notice method in the main thread of the observer's call NSWillBecomeMultiThreaded: NSNotification. NameCopy the code

2.1.6 Other common methods

/ / for the main Thread Thread. The main / / get the current Thread Thread. The current / / get the Thread to the current Thread state. The current. The isCancelled Thread. Current. IsFinished Thread.current.isFinishedCopy the code

2.2 Operation and OperationQueue

2.2.1 introduces

  • Operation

Operation is an abstract class that encapsulates a task that contains code logic and data. Operation is an abstract class, so it cannot be used directly when writing code. To use its subclasses, the system provides NSInvocationOperation (not available in Swift) and BlockOperation by default.

  • OperationQueue

The OperationQueue is used to control the execution of a series of operation objects. After an action object is added to the queue, it persists until the action is canceled or completed. The order in which the operation objects in the queue are executed is determined by the priority of the operation and the dependencies between the operations. Multiple queues can be created in an application to process operations.

advantage
  1. You can add completed blocks of code to execute after the action is complete.
  2. Add dependencies between operations to easily control the order of execution.
  3. Set the priority of the operation.
  4. You can easily cancel an operation.
  5. Use KVO to observe changes in the execution status of operations: isExecuteing, isFinished, isCancelled.

2.2.2 Basic Usage

The instructions for Operation and OperationQueue are as follows:

  1. Create operations: Encapsulate the operations to be performed in an Operation object.
  2. Create a queue: Create an OperationQueue object.
  3. Queue operations: Add Operation objects to the OperationQueue object.

After that, the Operation of the OperationQueue is automatically removed and executed in a new thread.

① Create a vm.
  • NSInvocationOperation (not supported by Swift)

Threads are not started by default, but only performed in the current thread. Multithreading can be implemented through Operation and OperationQueue.

NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil]; // the thread will not be started [op start];Copy the code
  • BlockOperation

Whether BlockOperation starts a new thread depends on the number of operations. If the number of operations added is large, a new thread is automatically started. Of course, the number of open threads is determined by the system.

// 1. Create BlockOperation objects and encapsulate operationslet op = BlockOperation.init {
    print("init + \(Thread.current)"Op.start ();} // 2.Copy the code
  • Custom subclass inherited from Operation

By default, subclasses of Operation execute synchronously, and if we were to create a subclass capable of concurrency, we might need to override some methods.

  • Start: All parallel Operations must override this method, and then call this method manually in the thread you want to execute. Note: The start method of the parent class cannot be called at any time.
  • Main: called in the start method, but be careful to define separate auto-release pools from other threads.
  • IsExecuting: KVO notification needs to be implemented.
  • IsFinished: Indicates whether the KVO notification mechanism is completed.
  • **isAsynchronous: ** This method returns false by default, indicating non-concurrent execution. Concurrent execution requires customization and returns true. This return value is later used to determine whether concurrency is possible.

Copy the code
② Create queue

There are two types of Queues: primary queue and custom queue. The custom queue contains both serial and concurrent functions. The following describes the basic creation methods and features of the main queue and custom queue.

// Get the main queueletMainQueue = operationQueue. main // Custom queue creation methodlet queue = OperationQueue()
Copy the code
  • The home side column
    • Any operations added to the main queue are executed in the main thread.
  • Custom queue
    • Operations added to this queue are automatically placed into child threads for execution.
    • It also includes serial and concurrent functions.
③ Add the operation to the queue

Operation needs to work with OperationQueue to implement multithreading. We need to queue the created operations. There are two ways:

  1. addOperation(_ op: Operation)

Add an instance object of the created Operation or its subclasses directly.

  1. addOperation(_ block: @escaping () -> Void)

Add an operation directly to the queue using a block.

2.2.3 Serial and parallel control

The custom queue created by OperationQueue has both serial and concurrent capabilities. Its serial function is through the attribute maximum number of concurrent operation – maxConcurrentOperationCount is used to control a particular queue can have how many operations involved in concurrent execution at the same time.

Note: maxConcurrentOperationCount control is not the number of concurrent threads, but a queue at the same time the maximum number of operations that can execute concurrently. And an operation doesn’t have to run in just one thread.

  • Maximum number of concurrent operation: maxConcurrentOperationCount
    • MaxConcurrentOperationCount by default to 1, said not to limit, can be carried out concurrently.
    • MaxConcurrentOperationCount to 1, the queue is a serial queue. Only serial execution.
    • MaxConcurrentOperationCount is greater than 1, the queue for the concurrent queue. Of course, this value should not exceed the system limit. Even if you set a large value, the system will automatically adjust to min{self-set value, the default maximum value set by the system}.
let queue = OperationQueue()

queue.maxConcurrentOperationCount = 1

queue.addOperation {
    sleep(1)
    print("1---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")
}
queue.addOperation {
    sleep(1)
    print("2---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")
}
queue.addOperation {
    sleep(1)
    print("3---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")
}
queue.addOperation {
    sleep(1)
    print("4---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")} ----- the maximum concurrent operand is 1, and the output is ------ 1-- <NSThread: 0x600001DDC200 >{number = 5, name = (null)}----576945144.766482 2-- <NSThread: 0x600001DD0280 >{number = 6, name = (null)}----576945145.775298 3-- <NSThread: 0x600001DFBD00 >{Number = 4, name = (null)}----576945146.775842 4-- <NSThread: 0x600001DD0280 >{number = 6, name = (null)}----576945147.779273 ----- The maximum number of concurrent operations is 3. ------ 2-- <NSThread: 0x6000018dc0C0 >{number = 5, name = (null)}----576945253.401897 1-- <NSThread: 0x6000018C5D00 >{number = 7, name = (null)}----576945253.401891 3-- <NSThread: 0x6000018CA540 >{Number = 6, name = (null)}----576945253.401913 4-- <NSThread: 0x6000018DC100 >{Number = 8, Name = (NULL)}----576945254.403032Copy the code

When the maximum concurrent operand is 1, the operations are executed sequentially. When the maximum number of concurrent operations is 3, three operations are executed concurrently, and the other one is executed 1s later. The number of open threads is determined by the system and does not need us to manage.

2.2.4 Operation Dependency

Operation provides three interfaces for managing and viewing dependencies.

// Add a dependency that makes the current operation dependent on the completion of the operation op. Func addDependency(_ op: Operation) // Remove dependency from Operation op. Func removeDependency(_ op: Operation) // An array of action objects that must be completed before the current object begins execution. var dependencies: [Operation] { get }Copy the code

By adding an operation dependency, the result is op2 first and op1 second, no matter how many times the operation is run.

let queue = OperationQueue()

let op1 = BlockOperation {
    print("op1")}let op2 = BlockOperation {
    print("op2")} op1.adddependency (op2) queue.addOperation(op1) queue.addOperation(op2) ---- Result: ---- op2 op1Copy the code

2.2.5 Thread Priority

The OperationQueue provides the queuePriority property, which applies to operations in the same OperationQueue and not to operations in different operation queues. By default, all newly created action objects have a priority of Normal. However, we can assign a value to change the priority of the current operation in the same queue.

Public enum QueuePriority: Int {case veryLow
        case low
        case normal // default value
        case high
        case veryHigh
    }
Copy the code

For operations added to the queue, the ready state is first entered (the ready state depends on the dependencies between the operations), and then the start execution order (non-end execution order) of the ready operations is determined by the relative priority of the operations (priority is an attribute of the operation object itself).

Now that we understand the ready operation, we understand the object of the queuePriority property.

  • The queuePriority attribute determines the order of start execution between operations that enter the ready state. And priorities are no substitute for dependencies.
  • If a queue contains both high-priority and low-priority operations, and both operations are ready, the queue performs the high-priority operation first.
  • If a queue contains both ready and unready operations, the unready operations have a higher priority than the ready operations. Then, the ready operation will be executed first, even though it is of low priority. Priorities are no substitute for dependencies. If you want to control the startup sequence between operations, you must use dependencies.

2.2.6 Communication between threads

let queue = OperationQueue()

let op = BlockOperation {
    print("Asynchronous operation -- \(thread.current)") / / back to the main thread OperationQueue. Main. AddOperation ({print("Back to main Thread -- \(thread.current)")})} queue. AddOperation (op) -- -- -- -- -- the output: asynchronous operations -- -- -- -- -- -- < NSThread: <NSThread: 0x60000100D680 >{number = 1, name = main}Copy the code

2.2.7 Other common methods

  • Operation Common attributes and methods
* func Cancel () Cancels the operation, marking isCancelled status. 2. Method for checking the operation status * isFinished Determine whether the operation is complete. * isCancelled Determines whether the operation has been marked as cancelled. * isExecuting Checks whether the operation is running. * isAsynchronous determines whether an operation performs its task asynchronously. * isReady determines whether an operation isReady. This value depends on the dependency of the operation. 3. Operation synchronization * funcwaitUntilFinished() blocks the current thread until the operation ends. Can be used for sequential synchronization of threads. * completionBlock: (() -> Void)? Executes the completionBlock when the current operation completes.Copy the code
  • Common properties and methods of OperationQueue
1. Cancel/suspend/resume operations * func cancelAllOperations() cancels all operations on the queue. * isSuspended determines and sets whether a queue isSuspended.trueIs the suspended state,falseTo restore the status. 2. Operation synchronization * funcwaitUntilAllOperationsAreFinished () blocks the current thread until the queue operations performed completely. * func addOperations(_ ops: [Operation],waitUntilFinished waitBool) Add operation array to queue,waitFlag whether to block the current thread until all operations are complete * Operations An array of operations currently in the queue (an operation is automatically cleared from this array when it finishes). * operationCount The operands in the current queue. 4. Get queue * current Gets the current queue, or nil if the current thread is not running on OperationQueue. * main gets the main queue.Copy the code

Note:

  1. Pause and cancel (both the cancellation of operations and the cancellation of queues) do not mean that the current operation can be cancelled immediately, but that no new operation can be performed after the current operation has been completed.
  2. The difference between a pause and a cancel is that after a pause, you can resume the operation and continue. After canceling, all operations are cleared and no further operations can be performed.