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 a
Operation
Add-dependentOperation
After, only what it depends onOperation
All done. CurrentOperation
Before it can be executed. Regardless of dependentOperation
Execution 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 taking
defaultMaxConcurrentOperationCount
, that is, -1, where the default maximum operand is determined byOperationQueue
Object is dynamically determined based on current system conditions (system memory and CPU). maxConcurrentOperationCount
If the value is 0, Operation in the queue will not be executed.maxConcurrentOperationCount
When it is 1, the queue executes serially.maxConcurrentOperationCount
When 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:
- When state variables are switched, locks are needed to ensure thread safety.
- Although the official document says
main
Methods do not need to be forcibly overridden, but for logic,start
Method is mainly responsible for task initiation,main
Method for task processing, so rewritemain
Methods. - about
isAsynchronous
Properties, which I thought I could control at firstOperation
Whether to automatically open up threads, but based on experiments and looking at the source code, it should just be an identifier for the currentOperation
A 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 theOperation
And the rest of it was written asOperationQueue
In the form of run, we can also putisAsynchronous
Change 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:
OperationQueue
You can set the maximum number of concurrent operationsmaxConcurrentOperationCount
.
Under certain conditions can be analogous to the GCD semaphore
- Establish dependencies between different tasks
addOperation
;
Public func notify(Queue: DispatchQueue, execute: DispatchWorkItem) can be analogous to GCD’s DispatchWorkItem method under certain conditions
OperationQueue
You 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