| Elapsed | Time | Activity |
|---|---|---|
| 0:00 | 0:05 | Objectives |
| 0:05 | 0:25 | Initial Exercise |
| 0:30 | 0:20 | Intro to Operations (TT I) |
| 0:50 | 0:10 | BREAK |
| 1:00 | 0:30 | In Class Activity I |
| 1:30 | 0:20 | Operation Lifecyle Events (TT II) |
| TOTAL | 1:50 |
From the start of this course, we've called out that Grand Central Dispatch (GCD) and Operations are the two built-in APIs from Apple that you use in iOS to manage concurrent tasks (as opposed to working with threads directly).
We have also mentioned that...
- Both technologies are designed to encapsulate units of work and dispatch them for execution.
- Operations are build on top of GCD.
- Apple advises developers to use the highest level of abstraction that is available (which is Operations).
- Most developers implement a combination of GCD and Operations, depending on which suits their specific requirements.
But Operations are not without their own challenges and pitfalls.
As a developer, you need to know:
- The benefits Operations offer — as well as their challenges and pitfalls.
- The differences between GCD and Operations.
- Under which circumstances might Operations be a better solution than GCD.
- Identify and describe:
- The benefits of using Swift Operations for concurrency
- The
Operationclass and its two pre-defined subclasses:NSInvocationOperationandBlockOperation - Block Operations
- Operation Lifecycle Events
- Implement:
- basic
BlockOperationexamples
Review solutions to JankyTable app from Lesson 4...
- One or more volunteers present their solutions. Opens a class discussion.
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.
What if — instead of sending simple, individual tasks for execution on a queue — you could send a more complex and reusable "package"?
The Operation Class
Operation (formerly called NSOperation) is an abstract class that allows you to encapsulate (wrap) a unit of work into such a package that you can submit for execution at some time in the future.
Subclasses of Operation can represent the code and data associated with a single task.
class Operation : NSObjectKey Attributes
- An Operation describes a single unit of work
- A higher level of abstraction over GCD
- Object-oriented (vs functions/closures in GCD)
- Execute concurrently — but can be serial by using dependencies
- Offer more developer control (than GCD)
Instead of sending a task as a closure or function (as in GCD), operations allow you to create and submit tasks as pre-defined, reusable objects — objects in which you can implement helper methods, dynamically pass input parameters to set up the task, and much more...
In addition, the Operation class offers a number of compelling benefits over GCD:
Reusability
Instances of concrete Operation subclasses are "once and done" tasks. This means that once an Operation object is added to an OperationQueue, the same object cannot be added to any other OperationQueue; the specific task represented by that particular object cannot be executed twice.
But, because an instance of Operation is an actual Swift object representing a unit of work, you can easily submit that unit of work multiple times by creating and sending new objects of that same Operation subclass, if needed.
Dependencies
Dependencies enables developers to execute tasks in a specific order.
By default, an operation object with dependencies is not considered ready until all of its dependent operation objects have finished executing. Once the last dependent operation finishes, the operation object becomes ready and able to execute.
KVO-Compliant
Operation and OperationQueue classes have a number of properties that can be observed using KVO (Key Value Observing).
This allows you to monitor the state 1 of an operation or operation queue.
Developer Control
Using GCD, once you dispatch a task, you no longer have control or insight into the execution of that task.
The Operation and OperationQueue classes are more flexible in that respect, giving the developer control over the operation's life cycle:
-
Max Number of Operations — For an
OperationQueue, you can specify the maximum number of queued operations that can run simultaneously. This makes it easy to (a) control how many operations run at the same time or (b) to create a serial operation queue. -
Execution Priority Levels — For subclasses of
Operation, you can configure the execution priority level of an operation in an operation queue. 1 -
Pause, Resume, Cancel —
Operationscan also be paused, resumed, and cancelled.
1 Details on operation state, KVO properties, and priority levels coming up later...
Because the Operation class is an abstract class, you do not use it directly. Instead, you subclass Operation or use one of the system-defined subclasses (NSInvocationOperation or BlockOperation) to perform the actual task.
NSBlockOperation— Use this class to initiate operation with one or more blocks. The operation itself can contain more than one block and the operation will be considered finished when all blocks have completed execution.NSInvocationOperation— Use this class to initiate an operation that consists of invoking a selector on a specified object.
There are two ways to execute operations:
- Operation Queues — Typically, you execute operations by submitting them to an operation queue — an instance of the
OperationQueueclass — to be processed based on the priority of each operation submitted.
- An operation queue executes its operations either directly — by running them on secondary threads — or indirectly using the
libdispatchlibrary (aka, GCD).
Diagram showing an OperationQueue with three operations enqueued.
More on OperationQueues in later lessons...
- The start() Method — You can also choose not to use an
OperationQueueand execute an operation yourself by calling itsstart()method directly from your code.
- Because starting an operation that is not in the ready state triggers an exception, executing operations manually puts additional burden on your code to handle state changes if you choose to call an operation's
start()method directly.
Note that later we'll see that the
isReadyproperty reports on the operation’s readiness state.
Some things to note
- An
Operationobject is a "single-shot object" — that is, it executes its task once and cannot be used to execute it again. (Though new objects of the same class can be instantiated and submitted for execution.) - Unlike GCD, operations run synchronously by default — that is, they perform their task in the thread that calls their
start()method. (You can get them to run asynchronously, but this requires much more work.) - Despite being abstract, the base implementation of
Operationincludes significant logic to coordinate the safe execution of your task.
- This allows you to focus on the actual implementation of your task, rather than on the glue code needed to ensure it works correctly with other system objects.
Before implementing your own custom subclasses of the Operation class, let's examine the behavior of one of the two built-in subclasses of Operation provided by Apple: BlockOperation
class BlockOperation : OperationBlockOperation can be thought of as a bridge between GCD DispatchQueues and Operations because:
- it manages the concurrent execution of one or more closures on the default global queue.
- as an actual
Operationsubclass, it lets you take advantage of all the other features of an operation: cancelling a task, reporting task state, specifying dependences between tasks, using KVO notifications, etc.
If you simply need to execute a small bit of code or to call a method — if you find that you have a need for a simpler, GCD-like closure — you can use BlockOperation (or NSInvocationOperation) instead of subclassing Operation.
In addition, a BlockOperation object can be used to execute several blocks at once without having to create separate operation objects for each. When executing more than one block, the operation itself is considered finished only when all blocks have finished executing.
In this way, a BlockOperation can also behave like a GCD DispatchGroup.
Note that Block operations execute concurrently. To run them serially, you must submit them to a private dispatch queue or set up dependencies instead.
Simple Example
Here is an extremely simplified example of how to create and submit an instance of an Operation subclass — in this class, an instance of BlockOperation — to an OperationQueue for execution:
At 1) — an instance of BlockOperation called myBlockOperation is created.
At 2) — myBlockOperation is added to an OperationQueue for execution by the queue.
// An instance of an Operation subclass
let myBlockOperation = BlockOperation { // 1) create BlockOperation
// perform task here
}
queue.addOperation(myBlockOperation) // 2) add myBlockOperation to a queueNot shown here: (1) Creation of the OperationQueue (2) execution details.
Here is a simple example of a BlockOperation that:
- is made up of multiple code blocks
- is started by the OperationQueue
Let's look at what is going on here...
- At 1), we create a
printerOperationas ourBlockOperationobject. - At 2), then we add blocks of code to the
printerOperationthat will be part of the operation. - At 3), after adding all of blocks, we set a
completionBlockon the operation, which will be executed after the operation finishes. - At 4) we create an
OperationQueueobject that will callstart()on our operation object - ...and at 5) we add our
printerOperationobject to the queue
import Foundation
let printerOperation = BlockOperation() // 1) create printerOperation as BlockOperation
// 2) add code blocks to the operation
printerOperation.addExecutionBlock { print("I") }
printerOperation.addExecutionBlock { print("am") }
printerOperation.addExecutionBlock { print("printing") }
printerOperation.addExecutionBlock { print("block") }
printerOperation.addExecutionBlock { print("operation") }
printerOperation.completionBlock = { // 3) set completion block
print("I'm done printing")
}
let operationQueue = OperationQueue() // 4) Create an OperationQueue
operationQueue.addOperation(printerOperation) // 5) add operation to queueSource: https://blog.infullmobile.com/basics-of-operations-and-operation-queues-in-ios-a8e7b02950c3
TODO: Run the code as a playground a few times and observe results...
Q: What did you notice about the order in which the submitted blocks execute?
Q: How about the completionBlock's execution order?
Required Resources
- The BlockOperation_ex2.playground
is required. You must download this playground as it is dependent on a playground Source file contained within it.
Assignment Notes
The following code is incomplete. It is intended to break a phrase into separate words (aka, "tokens"), and send each token as a separate block to a simple BlockOperation object called tokenOperation.
Like Part 1 above, this BlockOperation object will consist of multiple blocks when executed.
Unlike Part 1, the BlockOperation object will not be sent to an OperationQueue — you will need to execute the operation's start() function manually.
import Foundation
let phrase = "Mobile is the greatest!"
let tokenOperation = BlockOperation()
for token in phrase.split(separator: " ") {
tokenOperation.addExecutionBlock {
print(token)
sleep(2)
}
}
// TODO: create completionBlock
duration {
//TODO: start the operation
}TODO: Complete the code so that it executes without error and, when all operations are done, it prints out the following from its completionBlock:
All operations completed!Note: The full output should resemble that which is listed below. Though the order in which the tokens are executed will vary, the completionBlock will always print last.
Mobile
the
greatest!
is
All operations completed!An Operation object has a state machine that represents its lifecycle.
During its lifetime, an Operation object can exist in any of the following states depicted here:
Pending — When being added to a queue it is in Pending state. In this state, it waits for its conditions.
Ready — As soon as all of them are fulfilled it enters the Ready state and in case there is an open slot it will start executing.
Finished — Having done all its work, it will enter the Finished state and will be removed from the OperationQueue.
Cancelled — In each state (except Finished) an Operation can be cancelled.
Source:
https://medium.com/flawless-app-stories/parallel-programming-with-swift-operations-54cbefaf3cb0
Operation objects maintain state information internally to:
- determine when it is safe to execute
- notify external clients of the progression through the operation’s life cycle
The KVO key paths (properties) associated with an operation's state at various stages of its lifecycle are:
- isReady — Lets clients know when an operation is ready to execute. When it has been instantiated and is ready to run, it will transition to the
isReadystate.truewhen the operation is ready to execute now orfalseif there are still unfinished operations on which it is dependent. - isExecuting — Once the
start()method is invoked, your operation moves to theisExecutingstate. This property must reporttrueif the operation is actively working on its assigned task orfalseif it is not. - isCancelled — Informs clients that the cancellation of an operation was requested. If
true, the app calls the cancel method, then it will transition to theisCancelledstate, before moving onto theisFinishedstate. - isFinished — Lets clients know that an operation
finishedits task successfully or wascancelledand is exiting. If it was not canceled, then it will move directly fromisExecutingtoisFinished. Marking operations asfinishedis critical to keeping queues from backing up within-progressorcancelledoperations.
These properties give you the ability to know what state your operation is in at any given point in its lifecycle.
Your custom subclasses of the Operation class inherit these lifecycle (state) properties and can use them to ensure the correct execution of operations in your code.
Some things to note
- Each of the state key paths (properties) listed above are read-only Boolean properties of the
Operationclass.
- You can query them at any point during the execution of the task to see whether or not the task is executing, finished, etc..
- The
Operationclass handles all of these state transitions for you.
- There are only two you can directly manipulate:
-isExecutingstate — Can influence this condition by starting the operation.
-isCancelledstate — By calling thecancel()method on the object.
- Research:
- Concurrent Versus Non-concurrent Operations - Apple docs
NSInvocationOperationobject- Passing Data Between Operations
- KVO-Compliant Properties: (of the
Operationclass) - Apple docs - completionBlock - Apple docs
- Tips for Implementing Operation Objects - Apple docs
- Assignment:
- Resume your solution to the issues with JankyTable app from Lesson 4.
- Resume/start the Assignment 2: Solve the Dining Philosophers Problem (challenge) from previous class: https://github.com/raywenderlich/swift-algorithm-club/tree/master/DiningPhilosophers
- Complete reading
- Complete challenges
