Post

Operation VS DispatchWorkItem

Operation VS DispatchWorkItem

image

In multithreaded programming, we often compare Operation and GCD. The task unit of OperationQueue is Operation, but in fact GCD also has a similar task unit, DispatchWorkItem. Features such as cancellation and adding dependencies in OperationQueue are provided by Operation. That comparison often ignores the existence of DispatchWorkItem, which is very similar. The main purpose of this article is to compare the differences between the two.

start() vs perform()

The basic execution capability is similar: both can run independently and do not necessarily need OperationQueue or DispatchQueue.

1
2
3
4
let operation = BlockOperation {
    print(#function)
}
operation.start()
1
2
3
4
let workItem = DispatchWorkItem {
    print(#function)
}
workItem.perform()

cancel() vs cancel()

Both have cancellation support, and both also provide the corresponding isCancelled property to check whether the task has already been cancelled.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lazy var workItem: DispatchWorkItem = {
     return DispatchWorkItem {
      sleep(2)
      guard self.workItem.isCancelled == false else {
          return
      }
      print(#function)
      }
  }()

  override func viewDidLoad() {
      super.viewDidLoad()
      DispatchQueue.global().async(execute: workItem)
  }

If you try to cancel the task before two seconds pass, nothing will be printed.

1
2
3
@IBAction func buttonAction(_ sender: UIButton) {
		workItem.cancel()
}

waitUntilFinished() vs wait()

First, let’s look at Apple’s official explanation of waitUntilFinished():

Blocks execution of the current thread until the operation object finishes its task.

Note that this refers to the current thread, not the thread that executes the task, except for the main thread. For example:

1
2
3
4
5
6
7
8
9
10
override func viewDidLoad() {
  super.viewDidLoad()
	let operation = BlockOperation {
      Thread.sleep(forTimeInterval: 2)
      print(Thread.current)
  }
  OperationQueue().addOperation(operation)
  operation.waitUntilFinished()
  print(#function)
}

The operation finishes on a background thread after waiting two seconds. Even though the task is dispatched to a background thread, the main thread is still blocked until the operation completes.

1
2
3
4
5
------Main thread---------—————————————blocked————————————--------------->


                    |--------Background thread executes task-------->|

This looks a lot like a synchronous operation, except that execution has been moved to another thread. DispatchWorkItem is very similar, so I will not go into too much detail here.

1
2
3
4
5
6
let workItem = DispatchWorkItem {
    print(#function)
}

DispatchQueue.global().async(execute: workItem)
workItem.wait()

In addition to wait(), DispatchWorkItem also provides two methods for checking whether a task has timed out.

1
2
3
public func wait(timeout: DispatchTime) -> DispatchTimeoutResult

public func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult

Let’s look at a simple example. The task takes at least two seconds to run, but the timeout is set to one second, so it will time out.

1
2
3
4
5
6
7
8
9
10
11
12
13
let workItem = DispatchWorkItem {
    Thread.sleep(forTimeInterval: 2)
    print(#function)
}

DispatchQueue.global().async(execute: workItem)
let result = workItem.wait(timeout: .now() + 1)
switch result {
case .success:
    print("success")
case .timedOut:
    print("timedOut")
}

As for the two timeout parameters: DispatchTime can be understood as relative time or app-relative time, while DispatchWallTime is absolute time or wall-clock time. The test is simple: run the two delayed-execution examples below, then adjust the system time forward by five minutes. The second callback will execute immediately.

1
2
3
4
5
6
DispatchQueue.main.asyncAfter(deadline: .now() + 300) {
    print(#function)
}
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 300) {
    print(#function)
}

addDependency(:) vs notify(::)

Many developers think OperationQueue is more elegant than GCD when it comes to adding task dependencies, and even believe that GCD cannot express dependencies at all. That is not true. With notify(queue: DispatchQueue, execute: DispatchWorkItem), dependencies can still be added quite elegantly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let workItem1 = DispatchWorkItem {
    Thread.sleep(forTimeInterval: 1)
    print(1)
}

let workItem2 = DispatchWorkItem {
    print(2)
}

let workItem3 = DispatchWorkItem {
    print(3)
}


workItem1.notify(queue: .global(), execute: workItem3)
workItem2.notify(queue: .global(), execute: workItem1)
DispatchQueue.global().async(execute: workItem2)

But imagine a requirement where workItem1 must execute last, regardless of the call order of the other tasks. In that case, the Operation implementation is simple and elegant, while DispatchWorkItem cannot achieve it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let operation1 = BlockOperation {
    Thread.sleep(forTimeInterval: 1)
    print(1)
}

let operation2 = BlockOperation {
    print(2)
}

let operation3 = BlockOperation {
    print(3)
}

operation1.addDependency(operation2)
operation1.addDependency(operation3)
OperationQueue().addOperations([operation1, operation2, operation3], waitUntilFinished: false)

Of course, DispatchGroup can also satisfy this requirement easily, but that is outside the scope of this article.

completionBlock: (() -> Void)? vs notify(::: () -> Void)

Both can set a completion callback.

1
2
3
workItem1.notify(queue: .global()) {
    print("\(Thread.current)" + "1")
}
1
2
3
operation1.completionBlock = {
    print(#function)
}

qualityOfService vs qos

After initialization, Operation can set qualityOfService. I will not go into the details of what this property does here.

1
operation.qualityOfService = .userInitiated

Likewise, when initializing DispatchWorkItem, the qos parameter is equivalent to qualityOfService.

1
2
3
DispatchWorkItem(qos: .userInitiated, flags: .barrier) {

}

At this point, all methods of DispatchWorkItem have appeared in this article. Operation still has the following properties and methods that do not have corresponding implementations in DispatchWorkItem:

1
2
3
4
5
6
7
8
9
10
open func main()
open var isExecuting: Bool { get }
open var isFinished: Bool { get }
open var isConcurrent: Bool { get }
open var isAsynchronous: Bool { get }
open var isReady: Bool { get }
open func removeDependency(_ op: Operation)
open var dependencies: [Operation] { get }
open var queuePriority: Operation.QueuePriority
open var name: String?

You can see that Operation additionally provides many state properties, a method for removing dependencies, and task priority. So when comparing DispatchWorkItem and Operation directly, the claim that GCD tasks cannot be cancelled is incorrect. OperationQueue does indeed have an advantage when it comes to adding task dependencies.

This post is licensed under CC BY 4.0 by the author.