Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”

preface

Hi Coder, I’m CoderStar!

We have talked about iOS multithreaded Thread and iOS multithreaded GCD before. Today we are going to talk about the last and most common mode of iOS multithreading –Operation.

An overview of

For Operation, the analogy to GCD is much less relevant. Operation itself is an abstract class and cannot be used directly. It defines related methods and attributes, which need to be implemented by subclasses. BlockOperation has been implemented in the system. In OC, there is also NSInvocationOperation, but in Swift, this subclass has been removed from Swift4, and it’s easy to understand why, because Swift doesn’t recommend selector.

Operation is a higher level of abstraction, built on top of GCD, that allows us to do multithreaded programming in an object-oriented (Cocoa object) manner.

In fact, NSOpertion was introduced before GCD, when NSOperationQueue took the NSOperation object, created a thread, ran the main method on that thread, and killed the thread when it was finished. In this way, compared with the thread pool of GCD bottom layer, it is very inefficient. Therefore, from Mac OS 10.5 and iOS 2, NSOpertion bottom layer was completely rewritten on the basis of GCD to improve performance and provide some new functions by taking advantage of the related features of GCD. For a simple example, OperationQueue has an unowned(unsafe) open var underlyingQueue: DispatchQueue? Properties.

If you’re interested in the underlying implementation of Operation, check out Operation. Swift in the open source Foundtion framework.

The basic principle of

Start by listing the main properties and methods of Operation and OperationQueue.

This will be explained in the comments.

Operation

// Operation

// MARK: - attributes

// The following attributes are Operation states, read-only attributes

open var isReady: Bool { get }
open var isExecuting: Bool { get }
open var isCancelled: Bool { get }
open var isFinished: Bool { get }


/// This property has been replaced by the following isAsynchronous property
open var isConcurrent: Bool { get }

/// Indicates whether the operation is asynchronous
/// Note that this property takes effect only when the start method is called directly, but adding Operation to OperationQueue ignores the value of this property
/// The default is false
@available(iOS 7.0.*)
open var isAsynchronous: Bool { get }


/// Operation priority
// If there are many operations in the queue, we can set this property to adjust the priority of the operations in the same queue, provided that the operations are in the readey state ATI
open var queuePriority: Operation.QueuePriority

/// This attribute is consistent with the quality of service attribute owned by Thread
/// Is used to describe the overall priority of a task in the process
@available(iOS 8.0.*)
open var qualityOfService: QualityOfService

/// The callback method after the task is completed
// This callback is executed only when the isFinished property is set to YES
@available(iOS 4.0.*)
open var completionBlock: (() -> Void)?

// MARK: - Method

/ / / start
/// this method needs to be overridden for concurrent Operation
Operation can be queued manually, just like normal methods
open func start(a)

/// this method needs to be overridden for non-concurrent operations
open func main(a)

/// cancel the operation
open func cancel(a)

/// Add dependencies
open func addDependency(_ op: Operation)

/// Remove dependencies
open func removeDependency(_ op: Operation)
Copy the code

Operation attributes and methods are described in detail:

Calling Cancel () only sets the status isCanceled to true if the operation is being performed, but does not affect the operation’s continuation. Calling Cancel () sets the states isCanceled and isReady to true if the operation has not yet been performed. If the canceled operation is performed, the state isFinished is set to true and the operation is not performed. It also fires the completionBlock method. Therefore, when subclassing Operation, we should check the isCanceled state before handling time-consuming and startup operations.

AddDependency method

  • Do not set cyclic dependencies. For example, A depends on B, and B depends on A. This will cause deadlock and no one will execute the dependency.
  • Dependencies can be set across operation queues.
  • When given aOperationAdd-dependentOperationAfter, only what it depends onOperationAll done. CurrentOperationBefore it can be executed. Regardless of dependentOperationExecution is considered complete if it succeeds, fails, or is cancelled.

OperationQueue

// Operation

// MARK: - attributes

/// The maximum number of concurrent operations allowed in the queue at the same time
open var maxConcurrentOperationCount: Int

// MARK: - Method

/// Cancel all operations
open func cancelAllOperations(a)

/// Calling this method blocks the current thread, waiting for all tasks to complete before executing further logic
/// is similar to DispatchGroup under certain conditions
open func waitUntilAllOperationsAreFinished(a)

/ / / add Operation
open func addOperation(_ op: Operation)

/// add Operation to closure
@available(iOS 4.0.*)
open func addOperation(_ block: @escaping() - >Void)

/// GCd-like fence function
@available(iOS 13.0.*)
open func addBarrierBlock(_ barrier: @escaping() - >Void)
Copy the code

The OperationQueue attributes and methods are described in more detail:

MaxConcurrentOperationCount properties

  • MaxConcurrentOperationCount if not set, the default value will be takingdefaultMaxConcurrentOperationCount, that is, -1, where the default maximum operand is determined byOperationQueueObject is dynamically determined based on current system conditions (system memory and CPU).
  • maxConcurrentOperationCountIf the value is 0, Operation in the queue will not be executed.
  • maxConcurrentOperationCountWhen it is 1, the queue executes serially.
  • maxConcurrentOperationCountWhen the value is greater than 1, the queue is executed concurrently. Of course, this value should not exceed the system limit. Even if you set a large value, the system will automatically adjust to min(the default value set by the system)64.

Operations that enter the Queue first may not run first, since there is a queuePriority. So when maxConcurrentOperationCount is set to 1 is not a real serial queue, after the higher priority to join the Operation could be performed first.

The value of 64 should also be the default maximum number of threads under GCD, but can be adjusted by adjusting the priority of the target queue. There is a thread explosion concept involved here, and there will probably be an article about it later.

From the above several state attributes of Operation, we can know that the state of Operation will undergo corresponding flow during the running process of the program, as shown in the state diagram below.

use

For general tasks, we can use BlockOperation directly. The following is an example:

let operation = BlockOperation {
  // do something
}

// BlockOperation does not only execute one Block, but can be added to multiple blocks with an internal array for storage.
operation.addExecutionBlock {
   // do something
}

let queue = OperationQueue()
queue.addOperation(operation)
Copy the code

However, most of the time, we need to inherit Operation for some custom operations, such as network request dependencies. In this case, we need to inherit Operation to override the corresponding properties and methods.

Network request dependencies need to be subclassed Operation: Normal Operation waits for the main method to complete and then automatically sets isFinished to True before executing the next one, but for network requests, you need to manually set isFinished to true after the network request callback.

This part is described in detail in Apple’s documentation, Operation documentation link

Instead of putting Operation into the OperationQueue, you can run it directly by calling the start method.

In the first case, the OperationQueue automatically opens up threads for the Operation without additional processing. In the second case, we need to manually control the Operation. We can design the Operation to be synchronous or asynchronous, so called non-concurrent Operation and concurrent Operation

Of course, the direct call to the start method is actually used less frequently in the daily development process than the OperationQueue method. The following sections will give you an overview of how to use Operation and what to be aware of when subclassing Operation.

Operation itself is thread-safe. When we subclass Operation, whether it’s a non-concurrent Operation or a concurrent Operation, we also need to keep it thread-safe, so we need to add mutex in some places, such as during state transitions in subsequent operations.

The concurrentOperation

For non-concurrent operations, since Operation is a synchronous Operation by default calling the start method directly, when we inherit Operation to implement a non-concurrent Operation, we just need to override the main method.

class SyncOperation: Operation {
    override func main(a) {
        // do something}}Copy the code

concurrentOperation

If it is a concurrent Operation, at least the following properties and methods need to be overridden, and KVO notifications need to be generated when running status updates.

  • isAsynchronous
  • isExecuting
  • isFinished
  • start()

The specific code is as follows, please pay attention to read the comments:

public class AsyncOperation: Operation {
    private var block: ((_ operation: AsyncOperation) - >Void)?

    private let queue = DispatchQueue(label: "async.operation.queue")
    private let lock = NSLock(a)private var _executing = false
    private var _finished = false

    / / / data
    ///
    // Bind some data to the Operation so that the dependent Operation can get some data from the Operation
    public var data: Any?

    /// Whether to execute
    ///
    // internal lock to ensure thread safety
    public override var isExecuting: Bool {
        get {
            lock.lock()
            let wasExecuting = _executing
            lock.unlock()
            return wasExecuting
        }
        set {
            if isExecuting ! = newValue {
                willChangeValue(forKey: "isExecuting")
                lock.lock()
                _executing = newValue
                lock.unlock()
                didChangeValue(forKey: "isExecuting")}}}/// Whether to end
    ///
    // internal lock to ensure thread safety
    /// KVO needs to be done manually, otherwise the completionBlock will not be triggered and the dependent Operation will not start
    public override var isFinished: Bool {
        get {
            lock.lock()
            let wasFinished = _finished
            lock.unlock()
            return wasFinished
        }
        set {
            if isFinished ! = newValue {
                willChangeValue(forKey: "isFinished")
                lock.lock()
                _finished = newValue
                lock.unlock()
                didChangeValue(forKey: "isFinished")}}}/// Indicates whether the Operation is running asynchronously
    public override var isAsynchronous: Bool {
        return true
    }

    public override func start(a) {
        // Check whether it has been cancelled before starting to prevent wasteful operation
        if isCancelled {
            isFinished = true
            return
        }

        isExecuting = true

        queue.async { [weak self] in
            self?.main()
        }
    }

    public override func main(a) {
        if let block = block {
            block(self)}else {
            finish()
        }
    }
}

// MARK: - Expose methods

extension AsyncOperation {
    public convenience init(block: ((_ operation: AsyncOperation) - >Void)?) {
        self.init(a)self.block = block
    }

    /// Complete the task
    ///
    // complete the Operation manually after completing some asynchronous operations
    public func finish(a) {
        isExecuting = false
        isFinished = true}}Copy the code

There are a few points in the code that need to be noted:

  1. When state variables are switched, locks are needed to ensure thread safety.
  2. Although the official document saysmainMethods do not need to be forcibly overridden, but for logic,startMethod is mainly responsible for task initiation,mainMethod for task processing, so rewritemainMethods.
  3. aboutisAsynchronousProperties, which I thought I could control at firstOperationWhether to automatically open up threads, but based on experiments and looking at the source code, it should just be an identifier for the currentOperationA flag indicating whether an asynchronous operation is performed when set totrue, we need to open up our own threads for task distribution. When we determine theOperationAnd the rest of it was written asOperationQueueIn the form of run, we can also putisAsynchronousChange the return value to false to remove the internal queue.

For the specific application of Operation, you can read the source code of Alamofire or SDWebImage and other open source libraries. There are internal applications.

GCD VS Operation

The issue of whether to use GCD or Operation has been debated for a long time in the community. From the recommendation of GCD in CS193p at Stanford University to the recommendation of Operation by the speaker at WWDC 2012, it can be seen that developers have different views on this issue. In this section, we will talk about the advantages and differences between the two.

Many articles on the network are based on the comparison between GCD and Operation without the DispatchWorkItem object.

1. From the point of view of the two levels: The bottom layer of GCD is THE API of C language, while Operation is a higher level of abstraction based on GCD, so GCD must be faster and lighter than Operation. (Operation uses the GCD API with some locks to keep the thread safe)

But the flip side is that because Operation is a higher level abstraction, a general rule of thumb is to use the highest-level API first and then degrade as needed. From this point of view, Operation is more abstract, more object-oriented, and more conducive to the underlying traceless change.

2. From the API provided by both: GCD and Operation are similar, especially when DispatchWorkItem (@available(macOS 10.10, iOS 8.0, *)) comes out. DispatchWorkItem can be analogous to the Operation object and DispatchQueue to the OperationQueue object. For example, both DispatchWorkItem and Operation objects can perform operations such as cancel, DispatchQueue and OperationQueue objects can add tasks or operations (objects and closures), fence functions, Suspend, restore (two corresponding methods, one property), and so on. But there are some differences, such as:

  • OperationQueueYou can set the maximum number of concurrent operationsmaxConcurrentOperationCount.

    Under certain conditions can be analogous to the GCD semaphore

  • Establish dependencies between different tasksaddOperation;

    Public func notify(Queue: DispatchQueue, execute: DispatchWorkItem) can be analogous to GCD’s DispatchWorkItem method under certain conditions

  • OperationQueueYou can cancel all operations in the queue.
  • .

3. Compared with GCD, Operation can consolidate some operations by subclassing, which is more convenient to manage.

The last

Try harder!

Let’s be CoderStar!

Recommended learning materials

  • NSOperation vs Grand Central Dispatch
  • guide-to-blocks-grand-central-dispatch
  • When to use NSOperation vs. GCD
  • Operation and OperationQueue Tutorial in Swift
  • Advanced NSOperations

It is very important to have a technical circle and a group of like-minded people, come to my technical public account, here only talk about technical dry goods.

Wechat official account: CoderStar