Parallel programming with Swift: Operation
The original “# Parallel programming with Swift: Operations,” the author | Jan Olbrich translation | JACK edit | JACK
OperationQueue
Let’s review:OperationQueueIs a high-level abstraction of Cocoa on the GCD. More precisely, it is an abstraction of dispatch_queue_t. Like a queue, you can add tasks. inOperationQueueThese tasks are Operation objects. When performing an operation, we need to know which thread it is in. For example, if we want to update the UI, we need to update the UI inMainOperationQueue. In addition, we use our own action queues.
let operationQueue: OperationQueue = OperationQueue()
Copy the code
Unlike dispatch_queue_t, OperationQueue can specify the maximum limit of Operations that can be performed simultaneously in the queue.
let operationQueue: OperationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
Copy the code
Operation
You can think of Operation as a high-level abstraction of the Dispatch block. But there are some differences. For example, when a Dispatch block executes for milliseconds, an Operation may execute for minutes or longer. Since Operation is a class, we can subclass Operation objects to encapsulate our business logic. This way, as long as the package is good, we can minimize changes when the logic changes (for example, the database layer).
Operation life cycle
In the life cycle of an Operation, there are several different phases. When it is added to the queue, it is in a Pending state, waiting for a trigger condition. Once the conditions are met, it goes to Ready. If there is an empty slot, it starts Executing and goes to Executing. After all tasks are completed, it enters the Finished state and is deleted from the OperationQueue. In every state except Finished, the operation can be cancelled.
Example Cancel the execution of Operation
Canceling is very simple. Cancellation may produce different results depending on the Operation. For example, when making a network request, cancellation may cause the request to be interrupted. When importing data, it may mean discarding transferred data. So how do you cancel an operation? You just call Cancel (). This will change the isCancelled property.
let op = DemoOperation()
OperationQueue.addOperations([op], waitUntilFinished: false)
op.cancel()
Copy the code
Note that canceling Operation causes it to discard all conditions and complete the state change from Executing to Finished as soon as possible. The only way for Operation to be removed from the queue is to enter the finished state.
If you forget to check for isCancelled, you may see that Operation is still executing, even after you cancelled it. Also note that Operation is vulnerable to race conditions. For example, pressing a button and setting up a flag may take a few microseconds. During this time, Operation may have completed and been removed from the queue, and the call cancel() is no longer valid.
ready
Readiness is also described by a Boolean property. This means that Operation is ready and waiting for the queue to start it. In a serial queue, the Operation that gets into the ready state first executes, even though it may be at the end of the queue. If multiple operations are ready at the same time, their execution order is determined by their priorities. An Operation is not ready until all of its dependencies have been executed.
Rely on
Dependency is one of the biggest characteristics of Operation. We can create tasks and specify that other tasks need to be executed before the created tasks can be executed by specifying dependencies. Also, for tasks that can be executed in parallel, we can specify dependencies to determine the order of execution. This can be done by calling addDependency().
operation2.addDependency(operation1) //execute operation1 before operation2
Copy the code
Any Operation with dependencies is not ready by default until all of its dependencies have been executed. However, it is up to you to decide how the task continues after the dependency is removed.
For ease of understanding, we can add dependencies by creating our own operator (==>). As follows, that is, the operations are performed in left-to-right order.
precedencegroup OperationChaining {
associativity: left
}
infix operator ==> : OperationChaining
@discardableResult
func ==><T: Operation>(lhs: T, rhs: T) -> T {
rhs.addDependency(lhs)
return rhs
}
operation1 ==> operation2 ==> operation3 // Execute in order 1 to 3
Copy the code
One advantage of dependencies is that they can be used across queues. At the same time, this can lead to unexpected locking behavior. For example, when a UI update relies on a background operation that blocks the execution of other operations, it can cause the UI thread to stall. Also, be aware of circular dependencies. This is the case if operation A depends on operation B and B depends on A. So they’re all waiting for another operation to execute, so a deadlock can occur.
Completion status
When an Operation completes, it enters the finished state and calls the completionBlock:
let op1 = Operation()
op.completionBlock = {
print("done")
}
Copy the code
The sample
With that in mind, let’s create a simple Operation structure. Demonstrate asynchronous execution, operation dependencies, and operation merging by printing “Hello World “. Let’s get started!
AsyncOperation
First, we create an Operation object to create asynchronous tasks. Create a subclass and make it perform tasks asynchronously:
import Foundation
class AsyncOperation: Operation {
override var isAsynchronous: Bool {
return true
}
var _isFinished: Bool = false
override var isFinished: Bool {
set {
willChangeValue(forKey: "isFinished")
_isFinished = newValue
didChangeValue(forKey: "isFinished")
}
get {
return _isFinished
}
}
var _isExecuting: Bool = false
override var isExecuting: Bool {
set {
willChangeValue(forKey: "isExecuting")
_isExecuting = newValue
didChangeValue(forKey: "isExecuting")
}
get {
return _isExecuting
}
}
func execute() {
}
override func start() {
isExecuting = true
execute()
isExecuting = false
isFinished = true
}
}
Copy the code
As you can see, we have to override the properties isFinished and isExecuting. Refer to the official documentation and follow the KVO (… In your custom implementation, you must generate KVO notifications for the key path…) Otherwise the OperationQueue will not be able to observe a change in the operation state. In the start() method, we manage the state of the operation from execution to completion. We create a execute() method, which will be implemented by subclasses.
Supplement: Actually, if you look at the official documentation, at the beginning and end of these two attributes, Both synchronous and asynchronous conditions are officially described (When implementing a concurrent operation object, you must override the implementation of this property… You do not need to reimplement this property for nonconcurrent operations.)
For those unfamiliar with KVO, it is necessary to understand the underlying implementation principles of KVO.
TextOperation
import Foundation
class TextOperation: AsyncOperation {
let text: String
init(text: String) {
self.text = text
}
override func execute() {
print(text)
}
}
Copy the code
Here, we pass text in the initialization method and print it in the execute() method.
GroupOperation
To merge operations, we create a GroupOperation here.
import Foundation
class GroupOperation: AsyncOperation {
let queue = OperationQueue()
var operations: [AsyncOperation] = []
override func execute() {
print("group started")
queue.addOperations(operations, waitUntilFinished: true)
print("group done")
}
}
Copy the code
As you can see, we create an array to which our subclasses will add their operations later. To execute, we simply add the Operation object to the queue. In this way, we ensure that these operations are performed in order. Calling addOperations([Operation], waitUntilFinished: True) causes the queue to block until the Operation added to the queue completes. GroupOperation then becomes completed.
HelloWorldOperation
Now, at last, we are at the last step. Create TextOperation objects, set the dependencies, and add them to the Operations array. At this point, we simply instantiate HelloWorldOperation and execute the start() method to see the result.
import Foundation
class HelloWorldOperation: GroupOperation {
override init() {
super.init()
let op = TextOperation(text: "Hello")
let op2 = TextOperation(text: "World")
op2.addDependency(op)
operations = [op2, op]
}
}
Copy the code
Operation Observer
So, how do we know if the operation is complete? One way is to implement completion Block, the other way is to register an observer for the operation. Next, we create a listener class that listens for the state of the operation through KVO.
import Foundation class OperationObserver: NSObject { init(operation: AsyncOperation) { super.init() operation.addObserver(self, forKeyPath: "finished", options: .new, context: nil) } override func observeValue(forKeyPath keyPath: String? , of object: Any? , change: [NSKeyValueChangeKey : Any]? , context: UnsafeMutableRawPointer?) { guard let key = keyPath else { return } switch key { case "finished": print("done") default: print("doing") } } }Copy the code
The data transfer
The Demo above simply prints “Hello World” and does not necessarily pass data, but let’s take a quick look at how data is passed between operations. The easiest way to do this is to use BlockOperation. It allows us to set the properties for the next operation and do the data transfer. Do not forget to set dependencies, or you may not perform the operation in a timely manner, resulting in incorrect data transfer.
let op1 = Operation1()
let op2 = Operation2()
let adapter = BlockOperation() { [unowned op1, unowned op2] in
op2.data = op1.data
}
adapter.addDependency(op1)
op2.addDependency(adapter)
queue.addOperations([op1, op2, adapter], waitUntilFinished: true)
Copy the code
Error handling
The other thing is that we haven’t added error handling yet. To be honest, I haven’t found a good way to do that yet. One option is to add a finished(withErrors:) method and have each object that inherits AsyncOperation call it instead of processing it in start(). This way, we can check for errors and add them to a list of errors. Suppose you have an operation A that depends on operation B. Suddenly, operation B ends up with some error. In this case, operation A can check the array and abort.
class GroupOperation: AsyncOperation {
let queue = OperationQueue()
var operations: [AsyncOperation] = []
var errors: [Error] = []
override func execute() {
print("group started")
queue.addOperations(operations, waitUntilFinished: true)
print("group done")
}
func finish(withError errors: [Error]) {
self.errors += errors
}
}
Copy the code
Note that the child operations need to handle their state, and we need to make some changes in the AsyncOperation class to make it work.
But as always, there are many ways, and this is just one of them. You can also listen for error values by observer.
It doesn’t matter how you do it. You just have to make sure the operation cleans up after itself. For example, if there is a problem with writing to a CoreData context object, you want to clean up the context. Otherwise, you may end up with inconsistent states and results.
UI Operations
The use of Operation is not limited to elements you can’t see (such as data requests), but everything you do in your program can be an Operation (although I don’t recommend it). Some things are easier to think of as operations. Let’s look at an example of using Operation to perform a dialog display:
import Foundation
class UIOperation: AsyncOperation {
let viewController: UIViewcontroller!
override func execute() {
let alert = UIAlertController(title: "My Alert", message: @"This is an alert.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .`default`, handler: { _ in
self.handleInput()
}))
viewController.present(alert, animated: true, completion: nil)
}
func handleInput() {
//do something and continue operation
}
}
Copy the code
UIOperation will stop execution when OK is pressed. After that, it enters the finished state, and all other operations that depend on UIOperation continue.
The mutex
Given that we can use Operation on the UI, this presents a new problem. When the network is not available, a box is usually displayed, and you may create a series of actions, which can easily result in all of these actions creating a pop-up box showing the network connection problem. As a result, multiple dialogs pop up simultaneously and we don’t know which is the first and which is the second. So we have to make these operations mutually exclusive.
Although this idea is complex, it is easy to implement through dependency. You just need to create a dependency between these operations. The only problem is how to access these action objects in a timely manner whenever needed. We can solve this by naming the operation and then accessing the OperationQueue and searching for the name. So you don’t have to have a variable that keeps referring to the Operation object.
let op1 = Operation()
op1.name = "Operation1"
OperationQueue.main.addOperations([op1], waitUntilFinished:false)
let operations = OperationQueue.main.operations
operations.map { op in
if op.name == "Operation1" {
op.cancel()
}
}
Copy the code
conclusion
In concurrent programming, Operation is a great tool for controlling concurrency. Use it wisely, however, and it may not be necessary to use Operation for small people who switch threads or tasks so that they can be executed quickly. The COMMUNIST Party is a better solution. Moreover, it is not easy to use Operation well in a project, which may cause some troublesome problems.
Add: there is still a Bug in the GroupOperations section above, which I will fix in a later post update.