Elapsed | Time | Activity |
---|---|---|
0:00 | 0:05 | Objectives |
0:05 | 0:15 | Initial Exercise |
0:20 | 0:20 | Overview I - How to implement Operation objects |
0:40 | 0:15 | In Class Activity I |
0:55 | 0:10 | BREAK |
1:05 | 0:25 | Overview I - OperationQueues |
1:30 | 0:25 | In Class Activity II |
TOTAL | 1:55 |
By the end of this lesson, you should be able to...
- Identify and describe:
- the difference between Synchronous and Asynchronous Operations
- how to subclass
Operation
to create custom concurrent and non-concurrent operations - how to use
OperationQueues
to handle the scheduling and execution ofOperations
- how to add operations to
OperationQueues
and how to manage their behavior
- Implement basic examples of:
- non-current subclasses of the
Operation
class
- Review solutions to "Assignment 2: Solve the Dining Philosophers Problem" (challenge) from previous class: https://github.com/raywenderlich/swift-algorithm-club/tree/master/DiningPhilosophers
- One or more volunteers present their solutions. Opens a class discussion.
Before we explore subclassing Operation
objects, it will help to understand how Apple has defined the behavior of Synchronous and Asynchronous operations...
Source: https://developer.apple.com/documentation/foundation/operation
Unlike GCD, Operation
objects run synchronously1 by default.
In a synchronous operation:
- The operation object does not create a separate thread on which to run its task.
- When you call the
start()
method of a synchronous operation directly from your code, the operation executes immediately in the current thread. - By the time the
start()
method of such an object returns control to the caller, the task itself is complete.
TIP: If you always plan to use queues to execute your operations, it is simpler to define them as synchronous.
If you execute operations manually, though, you might want to define your operation objects as asynchronous.1
Defining asynchronous operations is useful in cases where you want to ensure that a manually executed operation does not block the calling thread.
An asynchronous operation object:
- Is responsible for scheduling its task on a separate thread. The operation could do that by:
- starting a new thread directly
- calling an asynchronous method
- submitting a block to a dispatch queue for execution - When you call the
start()
method of an asynchronous operation, that method may return before the corresponding task is completed. It does not actually matter if the operation is ongoing when control returns to the caller, only that it could be ongoing.
Defining an asynchronous operation requires more work because you have to monitor the ongoing state of your task and report changes in that state using KVO notifications.
TIP: When you add an operation to an operation queue, the queue ignores the value of the isAsynchronous
property and always calls the start()
method from a separate thread.
- thus, if you always run operations by adding them to an operation queue, there is no reason to make them asynchronous.
1 REMEMBER — Asynchronous and Concurrent do not mean the same thing:
Serial versus Concurrent is about the number of threads available to a queue:
- Serial queues only have a single thread associated with them and thus only allow a single task to be executed at any given time.
- Concurrent queues can utilize as many threads as the system has available resources for. On a concurrent queue, threads will be created and released as needed.
Synchronous or Asynchronous is about waiting — whether or not the queue on which you run your task has to wait for your task to complete before it executes other tasks.
- you can submit asynchronous (or synchronous) tasks to either a serial queue or a concurrent queue.
The BlockOperation
class we explored in the previous lesson is handy for simple tasks.
But for more complex tasks, or to create reusable components, you will need to create your own custom subclasses of the Operation
class where each subclass instance represents a specific task.
And though the Operation
class — and its related pre-defined subclasses (BlockOperation
and NSInvocationOperation
) — provide the basic logic to track the execution state of your operation and other Operations benefits, they were designed to be subclassed before they can do any useful work for you.
How you create your subclass depends on whether your operation is designed to execute concurrently or non-concurrently.1
Non-Concurrent Operations
Non-Concurrent operations perform all of their work on the same thread, and when the main()
method returns the operation is moved into the Finished
state. The queue is then notified of this operation's state and removes the operation from its active pool of operations, freeing resources for the next operation to be executed.
For non-concurrent1 operations, you typically override only one method:
func main()
The main()
method performs the receiver’s non-concurrent task.
The default implementation of the main()
method does nothing; You must override main()
and place in it the code needed to perform the intended task.
Source:
https://developer.apple.com/documentation/foundation/operation/1407732-main
Things to note
- In your implementation, do not invoke
super
. - Of course, you should also define a custom initialization method to make it easier to create instances of your custom class.
- Optionally, if you do define custom getter and setter methods, you must make sure those methods can be called safely from multiple threads.
Example: Non-Concurrent Operation
This simple (nonfunctioning) example illustrates subclassing the Operation
class to create non-concurrent operation objects, including the requirement to override its main()
method:
class FilterOperation: Operation {
let flatigram: Flatigram
let filter: String
init(flatigram: Flatigram, filter: String) {
self.flatigram = flatigram
self.filter = filter
}
override func main() {
if let filteredImage = self.flatigram.image?.filter(with: filter) {
self.flatigram.image = filteredImage
}
}
}
Source:
https://learn.co/lessons/swift-multithreading-lab
Concurrent Operations
Concurrent operations can perform some work on a different thread. Thus, returning from the main()
method can not be used to move the operation into its Finished
state.
Because of this, when you create a concurrent operation, you are responsible for moving the operation between the Ready
, Executing
and Finished
states.
If you are creating a concurrent operation, you need to override the following methods and properties at a minimum:
start()
isAsynchronous
isExecuting
isFinished
The start()
method 2
In a concurrent operation, your start()
method:
- is responsible for starting the operation in an asynchronous manner. Whether you spawn a thread or call an asynchronous function, you do it from this method.
The isAsynchronous
property
The isAsynchronous
property of the Operation
class tells you whether an operation runs synchronously or asynchronously with respect to the thread in which its start()
method was called.
By default, this method returns false
, which means the operation runs synchronously in the calling thread.
Note: If you are implementing a concurrent operation, you are not required to override the main()
method but may do so if you plan to call it from your custom start()
method.
2 The
start()
method has additional responsibilities in a concurrent operation, which we will explore further in upcoming lessons. Same for theisAsynchronous
property. For further details of both, also see the Apple source referenced below:
Source:
https://developer.apple.com/documentation/foundation/operation
Example: Concurrent Operation
The (elided, non-functioning) code below illustrates the most basic steps needed to subclass Operation
to create concurrent operation objects:
class MyConcurrentOperation: Operation {
override var isAsynchronous: Bool { return true }
override var isExecuting: Bool { return state == .executing }
override var isFinished: Bool { return state == .finished }
...
override func start() {
if self.isCancelled {
state = .finished
} else {
state = .ready
main()
}
}
override func main() {
if self.isCancelled {
state = .finished
} else {
state = .executing
}
}
}
Discuss, draw, and brainstorm this topic:
- What scenarios can you think of in which your code might benefit from submitting tasks to queues as objects (instead of closures)?
Volunteers to share with class.
The code below is an incomplete effort to create a Non-Concurrent Operation
subclass.
When successfully working, its output should be:
MyOp Started
MyOp Completed
TODO
- Copy the code into a new playground.
- Implement a
main()
method which simply prints "MyOp Started - In its
completionBlock
, print "MyOp Completed"
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
// Queue
let operationQueue = OperationQueue()
operationQueue.qualityOfService = .userInitiated
class MyOperation: Operation {
//TODO: Create main()
}
let myOp = MyOperation()
myOp.completionBlock = {
//TODO: print "MyOp Completed"
}
operationQueue.addOperation(myOp)
The easiest way to execute operations is to use an operation queue, which is particularly powerful because it lets you control QoS levels, how many operations can execute simultaneously, and more...
Operation queues are instances of the OperationQueue
class, and their tasks are encapsulated in concrete instances of the Operation
class.
class OperationQueue : NSObject
You use instances of the OperationQueue
class to (1) manage the scheduling and execution of an Operation and (2) to set the maximum number of operations that can run simultaneously on a given queue.
Just as you'd submit a closure of work to a DispatchQueue
for GCD, instances of the Operation
class can be submitted to an OperationQueue
for execution.
This means you can execute tasks concurrently, just like with GCD and DispatchQueues
, but in an object-oriented fashion.
Though both OperationQueues
and DispatchQueues
are high-level abstractions of the queue model built on top of GCD (a low-level C API), OperationQueues
behave differently from DispatchQueues
in distinct ways. Most notably:
-
No Serial Queues — By default, all
OperationQueues
operate concurrently; you cannot change their type to serial (thought there is a way to execute tasks in operation queues sequentially: by using dependencies2 between operations). -
Developer Control — As a developer, you can:
- set the
maxConcurrentOperationCount
for an operation queue cancel
an operation, even if the operation is currently executing- pause (suspend) an operation queue
- set the priority of an operation by setting the
queuePriority
property - set the
qualityOfService
property to control how much of the system resources will be given to your operation - specify an existing
DispatchQueue
as theunderlyingQueue
- Determining Execution Order — Unlike GCD and
DispatchQueues
,OperationQueues
do not strictly conform to First-In-First-Out execution order.
An OperationQueue
acts like a prioritized FIFO queue:
- Operations within an operation queue are organized according to their readiness, priority level, and dependencies,2 and are executed based on those criteria.
- You can set priority on individual operations. Those with the highest priority get pushed ahead, but not necessarily to the front of the queue — the iOS system determines when to actually execute an operation.
- Operations with the same priority get executed in the order they were added to the queue — unless an operation has dependencies,2 which allow you to define that some operations will only be executed after the completion of the other operations they are dependent on.
2 We'll cover Operation Dependencies in the next class.
If all of the queued operations have the same queuePriority
and are ready to execute when they are put in the queue — that is, their isReady
property returns true
— they are executed in the order in which they were submitted to the queue. Otherwise, the operation queue always executes the one with the highest priority relative to the other ready operations.
Important Note: Because changes in the readiness of an operation can change the resulting execution order, your code should never rely on these "queue semantics" to ensure a specific execution order; ultimately, the system will decide on execution order. Implementing dependent operations2 is the most reliable way to guarantee execution order.
After being added to an operation queue, an operation remains in its queue until it reports that it is finished with its task. You can’t directly remove an operation from a queue after it has been added.
Operation queues retain operations until they're finished, and queues themselves are retained until all operations are finished.
Note that suspending an operation queue with operations that aren't finished can result in a memory leak.
Operation queues use the Dispatch
framework to initiate the execution of their operations. As a result, operations are always executed on a separate thread, regardless of whether they are designated as synchronous or asynchronous.
This means Operation queues are inherently thread safe: You can safely access a single OperationQueue
object from multiple threads without creating additional locks to synchronize access to it.
Creating an operation queue is simple; you declare it in your application as you would any other variable.
Here are three different examples of syntax used to create custom operation queues:
- Formal, long-form approach:
let operationQueue: OperationQueue = OperationQueue()
- Specifying a name and QoS level:
let myDefaultQueue = OperationQueue()
myDefaultQueue.name = "My Default QoS Queue"
myDefaultQueue.qualityOfService = .default
- Creating a
private
queue:
private let myQueue = OperationQueue()
- Your application is responsible for creating and maintaining any operation queues it intends to use.
- An application can have any number of queues, but there are practical limits to how many operations may be executing at a given point in time. Operation queues work with the system to restrict the number of concurrent operations to a value that is appropriate for the available cores and system load. Therefore, creating additional queues does not mean that you can execute additional operations.
In addition to any custom OperationQueues
you create, you can also access the main queue
as an OperationQueue
.
Declaration
class var main: OperationQueue { get }
Source:
https://developer.apple.com/documentation/foundation/operationqueue/1409193-main
Syntax to access:
let mainQueue = OperationQueue.main
This returns the default operation queue bound to the main thread
.
This does not create a new main queue
nor a new main thread
— but it does allow you similar developer control advantages with the main queue
as you would have with any other OperationQueue
(some limitations do apply).
Operation Queues allows you to add work in three separate ways:
- Pass an Operation
- Pass a closure
- Pass an array of Operations
All three use the addOperation(_:)
function from the OperationQueue
class, which takes two forms:
func addOperation(_ op: Operation)
func addOperation(_ block: @escaping () -> Void)
Each example illustrates one of the three ways to add a task mentioned above:
- Adding an
Operation
to anOperationQueue
:
// An instance of some Operation subclass
let myBlockOperation = BlockOperation {
// perform task here
}
someCustomQueue.addOperation(myBlockOperation)
- Adding a task to an
OperationQueue
as a code block:
myQueue.addOperation {
// some code block/task
}
- Adding multiple
Operations
to anOperationQueue
:
let operationsArray = [Operation]()
// Fill array with Operations
myQueue.addOperation(operationsArray)
An operation queue executes operations that are ready, according to quality of service levels, and with respect to any dependencies2.
After being added to a queue, an operation remains in that queue until it is explicitly canceled or finishes executing its task.
Once you’ve added an Operation
to an OperationQueue
, you can't add that same Operation
to any other OperationQueue
. (But, because they are objects, you can execute multiple new instances of that same Operation
subclass on other queues, as often as needed.)
But there are a number of ways you can influence how an operation queue executes operations.
In addition to those listed below, you can also:
- pause the queue
- choose which
DispatchQueue
to set as theunderlyingQueue
property
waitUntilAllOperationsAreFinished
— Blocks the current thread until all of the receiver’s queued and executing operations finish executing.
If you find yourself needing this method, it is best to set up a private serial DispatchQueue
in which you can safely call this blocking method.
TIP: You must never call this method on the main UI thread.
func addOperations([Operation], waitUntilFinished: Bool)
— Adds the specified operations to the queue.
Use this method if you don't need to wait for all operations to complete, but just a set (an array) of operations.
An OperationQueue
behaves like a DispatchGroup
in that you can add operations with different quality of service values and they'll run according to the corresponding priority.
The *default QoS level of an operation queue is .background
.
If you set the qualityOfService
property on the operation queue, keep in mind that it might be overridden by the QoS that you’ve set on individual operations managed by the queue.
You do this by calling the cancel()
method of the operation
object itself or by calling the cancelAllOperations()
method of the OperationQueue
class.
.cancel()
— Advises the operation object that it should stop executing its task..cancelAllOperations()
— Cancels all queued and executing operations.
See this source for more details:
https://www.hackingwithswift.com/example-code/system/how-to-use-multithreaded-operations-with-operationqueue
By default, the dispatch queue will run as many jobs as your device is capable of handling at once.
But you can limit that number by setting the maxConcurrentOperationCount
property on the dispatch queue.
The maxConcurrentOperationCount
property sets the maximum number of queued operations that can execute at the same time.
var maxConcurrentOperationCount: Int { get set }
After setting this property to 2, you will only have at most two operations running at any given time in the queue.
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
let operation1 = BlockOperation(block: {
...
})
operation1.qualityOfService = .userInitiated
let operation2 = BlockOperation(block: {
...
})
operation1.completionBlock = {
...
}
operation2.completionBlock = {
...
}
operation2.addDependency(operation1)
queue.addOperation(operation1)
queue.addOperation(operation2)
Q:
The snippet below sets the maxConcurrentOperationCount
property to 1.
- What would happen if you set the
maxConcurrentOperationCount
to 1?
let operationQueue: OperationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
Finding the answers to all the questions in here using this repo.
- Research:
start()
- Apple docs- Dependencies - Apple docs
- Cancelling (operations)
- Asynchronous Operations
- When and why would you use
OperationQueue.main
instead ofDispatchQueue.main
? defaultMaxConcurrentOperationCount
current
(queue) propertyunderlyingQueue
property (how to set it; what are implications and risks?)- The "Maintaining Operation Object States" and the "Responding to the Cancel Command" sections in: https://developer.apple.com/documentation/foundation/operation
- The "KVO-Compliant Properties" section in: https://developer.apple.com/documentation/foundation/operationqueue
- Continue working on your Course Project
- Complete reading
- Complete challenges