Operations and Operation Queues
Operations
An Operation
is an abstract class in the Foundation framework that represents a single, self-contained task or unit of work. It provides a structured way to encapsulate code, manage execution state, and handle dependencies and cancellation. Operations can be executed standalone or within an OperationQueue
, making them ideal for complex, interdependent tasks in iOS apps, such as downloading files, processing data, or performing computations.
Key Features:
- Encapsulation: Wraps a task in a reusable object.
- State Tracking: Monitors lifecycle states (e.g., ready, executing, finished).
- Dependencies: Allows tasks to depend on others for ordered execution.
- Cancellation: Supports stopping tasks gracefully via
cancel()
. - Priority and QoS: Uses
queuePriority
andqualityOfService
to influence scheduling. - Synchronous or Asynchronous: Can be configured for either execution style.
Use Case: Downloading an image, processing it, and updating the UI, with each step as a separate operation.
Operations State
An Operation
progresses through a well-defined lifecycle, tracked by key properties:
- isReady:
true
when the operation is ready to execute (all dependencies satisfied). Default:true
unless dependencies exist. - isExecuting:
true
when the operation is actively running. Default:false
. - isFinished:
true
when the operation completes or is cancelled. Default:false
. - isCancelled:
true
whencancel()
is called. Default:false
. - isAsynchronous:
true
if the operation runs asynchronously (custom subclasses only). Default:false
.
These properties are Key-Value Observing (KVO) compliant, enabling OperationQueue
to monitor state changes. In custom operations, you must manually manage isExecuting
and isFinished
for asynchronous tasks.
State Transitions:
- Pending (
isReady = true
,isExecuting = false
,isFinished = false
). - Executing (
isReady = false
,isExecuting = true
,isFinished = false
). - Finished (
isReady = false
,isExecuting = false
,isFinished = true
).
Example: Observing Operation State
let operation = BlockOperation {
print("Operation running")
}
operation.completionBlock = {
print("Operation finished: isFinished=\(operation.isFinished), isCancelled=\(operation.isCancelled)")
}
print("Initial state: isReady=\(operation.isReady), isExecuting=\(operation.isExecuting)")
operation.start()
Output:
Initial state: isReady=true, isExecuting=false
Operation running
Operation finished: isFinished=true, isCancelled=false
BlockOperation
BlockOperation
is a concrete subclass of Operation
that executes one or more closures (blocks) concurrently. It’s a simple way to define tasks without subclassing Operation
, ideal for straightforward operations that don’t require complex state management.
Key Features:
- Multiple Blocks: Supports adding multiple closures via
addExecutionBlock(_:)
. - Automatic State Management: Handles
isExecuting
andisFinished
internally. - Concurrent Execution: Multiple blocks may run concurrently on separate threads.
- Completion Block: Supports a
completionBlock
for post-execution tasks.
Example: Basic BlockOperation
let blockOperation = BlockOperation {
print("Executing block 1 on \(Thread.current)")
Thread.sleep(forTimeInterval: 1)
}
blockOperation.addExecutionBlock {
print("Executing block 2 on \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
blockOperation.completionBlock = {
print("Block operation completed")
}
blockOperation.start()
Output (order may vary due to concurrency):
Executing block 1 on <Thread 0x...>
Executing block 2 on <Thread 0x...>
Block operation completed
Use Case: Performing multiple quick tasks, like parsing JSON and saving data, as separate blocks within one operation.
Run Block Operation Concurrently
To run a BlockOperation
concurrently, you add it to an OperationQueue
with a maxConcurrentOperationCount
greater than 1, or rely on its ability to execute multiple blocks concurrently when started manually. When multiple execution blocks are added, BlockOperation
may run them on separate threads, depending on system resources.
Example: Concurrent BlockOperation in OperationQueue
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
let blockOperation = BlockOperation()
for i in 1...3 {
blockOperation.addExecutionBlock {
print("Block \(i) started on \(Thread.current)")
Thread.sleep(forTimeInterval: Double.random(in: 0.5...1.5))
print("Block \(i) completed")
}
}
blockOperation.completionBlock = {
print("All blocks finished")
}
queue.addOperation(blockOperation)
Output (order varies):
Block 1 started on <Thread 0x...>
Block 2 started on <Thread 0x...>
Block 3 started on <Thread 0x...>
Block 2 completed
Block 1 completed
Block 3 completed
All blocks finished
Explanation:
- Three blocks are added to a single
BlockOperation
. - The
OperationQueue
withmaxConcurrentOperationCount = 3
allows all blocks to run concurrently. - Each block runs on a separate thread, managed by the queue.
Use Case: Processing multiple images in parallel within a single operation, such as resizing or applying filters.
Custom Operations
Custom operations are created by subclassing Operation
to define complex tasks with custom behavior, state management, or asynchronous execution. You override methods like main()
(for synchronous tasks) or start()
(for asynchronous tasks) and manage KVO-compliant properties (isExecuting
, isFinished
).
Example: Synchronous Custom Operation
class DataTransformOperation: Operation {
private let input: [Int]
private var output: [Int]?
private var _executing = false
private var _finished = false
init(input: [Int]) {
self.input = input
super.init()
}
override var isExecuting: Bool {
get { _executing }
set {
willChangeValue(forKey: "isExecuting")
_executing = newValue
didChangeValue(forKey: "isExecuting")
}
}
override var isFinished: Bool {
get { _finished }
set {
willChangeValue(forKey: "isFinished")
_finished = newValue
didChangeValue(forKey: "isFinished")
}
}
override func main() {
guard !isCancelled else {
isFinished = true
return
}
isExecuting = true
print("Transforming \(input.count) items on \(Thread.current)")
output = input.map { $0 * 2 }
Thread.sleep(forTimeInterval: 1) // Simulate work
if isCancelled {
output = nil
print("Operation cancelled")
} else {
print("Transformed: \(output?.prefix(5) ?? [])")
}
isExecuting = false
isFinished = true
}
func getOutput() -> [Int]? { output }
}
Explanation:
- Transforms an array by doubling each element.
- Manages
isExecuting
andisFinished
with KVO compliance. - Checks
isCancelled
to support cancellation. - Runs synchronously in
main()
.
Use Case: Performing CPU-intensive tasks like data encryption or image processing with cancellation support.
Operation Queues
An OperationQueue
is a high-level Foundation framework class that manages the execution of Operation
objects. It schedules operations based on their dependencies, priorities, and system resources, providing a robust mechanism for concurrent or sequential task execution. Unlike GCD queues, OperationQueue
excels at handling complex workflows with dependencies and cancellation.
Key Features:
- Concurrency Control: Configures
maxConcurrentOperationCount
to limit simultaneous operations. - Dependency Management: Enforces execution order via operation dependencies.
- Cancellation: Cancels individual or all operations.
- Suspension: Pauses/resumes execution with
isSuspended
. - Priority Scheduling: Uses
queuePriority
andqualityOfService
to prioritize tasks. - Progress Tracking: Integrates with
Progress
for task monitoring.
Key Properties and Methods:
maxConcurrentOperationCount
: Maximum concurrent operations (1 for serial,defaultMaxConcurrentOperationCount
for system default).isSuspended
:true
to pause,false
to resume.addOperation(_:)
,addOperations(_:waitUntilFinished:)
: Add operations to the queue.cancelAllOperations()
: Cancels all queued and running operations.waitUntilAllOperationsAreFinished()
: Blocks until all operations complete (use sparingly).addBarrierBlock(_:)
(iOS 13+): Executes a block after all current operations finish.
Add Operations
Operations are added to an OperationQueue
using addOperation(_:)
for a single operation or addOperations(_:waitUntilFinished:)
for multiple operations. The queue schedules them based on dependencies, priority, and concurrency limits.
Example: Adding Multiple Operations
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .userInitiated
let operation1 = BlockOperation {
print("Operation 1 on \(Thread.current)")
Thread.sleep(forTimeInterval: 1)
}
operation1.queuePriority = .high
let operation2 = BlockOperation {
print("Operation 2 on \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
operation2.queuePriority = .normal
queue.addOperations([operation1, operation2], waitUntilFinished: false)
queue.addBarrierBlock {
print("All operations complete")
}
Output:
Operation 1 on <Thread 0x...>
Operation 2 on <Thread 0x...>
Operation 2 completed
Operation 1 completed
All operations complete
Operations Dependencies
Dependencies ensure operations execute in a specific order by making one operation wait for others to complete. Use addDependency(_:)
to establish relationships. Dependencies are resolved before queuePriority
or QoS.
Example: Chaining Operations with Dependencies
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
let downloadOp = BlockOperation {
print("Downloading data on \(Thread.current)")
Thread.sleep(forTimeInterval: 2)
}
let processOp = BlockOperation {
print("Processing data on \(Thread.current)")
Thread.sleep(forTimeInterval: 1)
}
let saveOp = BlockOperation {
print("Saving data on \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
// Set dependencies
processOp.addDependency(downloadOp)
saveOp.addDependency(processOp)
queue.addOperations([downloadOp, processOp, saveOp], waitUntilFinished: false)
queue.addBarrierBlock {
print("Workflow complete")
}
Output:
Downloading data on <Thread 0x...>
Processing data on <Thread 0x...>
Saving data on <Thread 0x...>
Workflow complete
Explanation:
processOp
waits fordownloadOp
, andsaveOp
waits forprocessOp
.- The queue ensures dependencies are respected, executing operations sequentially despite
maxConcurrentOperationCount = 2
.
Dispatch Group Implementation Using Operation Queue
A DispatchGroup
can be combined with an OperationQueue
to synchronize the completion of multiple operations, similar to GCD’s group functionality. This is useful when you need to wait for a set of operations to finish before performing a final task, such as updating the UI.
Example: Using DispatchGroup with OperationQueue
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
let group = DispatchGroup()
var results: [Int] = []
let lock = NSLock() // Ensure thread-safe array access
for i in 1...5 {
group.enter()
let operation = BlockOperation {
print("Processing task \(i) on \(Thread.current)")
Thread.sleep(forTimeInterval: Double.random(in: 0.5...1.5))
lock.withLock {
results.append(i * 2)
}
group.leave()
}
queue.addOperation(operation)
}
group.notify(queue: .main) {
print("All tasks complete: \(results.sorted())")
}
Output (order varies):
Processing task 1 on <Thread 0x...>
Processing task 2 on <Thread 0x...>
Processing task 3 on <Thread 0x...>
Processing task 4 on <Thread 0x...>
Processing task 5 on <Thread 0x...>
All tasks complete: [2, 4, 6, 8, 10]
Explanation:
- Each operation enters the
DispatchGroup
before starting and leaves upon completion. NSLock
ensures thread-safe appends toresults
.group.notify
runs on the main queue after all operations finish, printing the results.
Use Case: Fetching data from multiple APIs, processing results, and updating the UI once all tasks are done.
Benefits of Operation Queues Over GCD
While GCD is lightweight and efficient, OperationQueue
offers several advantages for complex workflows:
- Dependency Management: Built-in support for operation dependencies, unlike GCD, which requires manual synchronization (e.g., using
DispatchGroup
). - Cancellation: Operations can be cancelled individually or collectively, with built-in
isCancelled
checks. GCD tasks require manual cancellation logic. - State Management: Operations track lifecycle states (
isExecuting
,isFinished
), simplifying complex task coordination compared to GCD’s stateless blocks. - Priority and QoS: Fine-grained control over scheduling with
queuePriority
andqualityOfService
, complementing GCD’s QoS. - Reusability: Operations encapsulate tasks as objects, enabling reuse and customization, unlike GCD’s one-off closures.
- Progress Tracking: Integrates with
Progress
for user-facing tasks, which GCD lacks natively. - Suspension: Supports pausing/resuming the entire queue via
isSuspended
, not directly available in GCD. - Structured Workflows: Ideal for workflows with interdependent tasks, such as download → process → save, compared to GCD’s queue-based model.
When to Use OperationQueue:
- Complex task chains with dependencies or cancellation requirements.
- Tasks needing progress reporting or state monitoring.
- Workflows requiring pause/resume functionality.
When to Use GCD:
- Simple, independent tasks (e.g., one-off network requests).
- Low-level concurrency with fine-grained control.
- High-performance scenarios where overhead must be minimized.
Create an Asynchronous Operation
Asynchronous operations are custom Operation
subclasses that perform tasks non-blocking, such as network requests or file I/O. You override start()
instead of main()
, manage state properties (isExecuting
, isFinished
), and ensure KVO compliance. Asynchronous operations are critical for tasks that don’t complete immediately.
Example: Asynchronous Image Download Operation
class AsyncImageDownloadOperation: Operation {
private let url: URL
private var imageData: Data?
private var _executing = false
private var _finished = false
private var task: URLSessionDataTask?
init(url: URL) {
self.url = url
super.init()
}
override var isAsynchronous: Bool { true }
override var isExecuting: Bool {
get { _executing }
set {
willChangeValue(forKey: "isExecuting")
_executing = newValue
didChangeValue(forKey: "isExecuting")
}
}
override var isFinished: Bool {
get { _finished }
set {
willChangeValue(forKey: "isFinished")
_finished = newValue
didChangeValue(forKey: "isFinished")
}
}
override func start() {
guard !isCancelled else {
isFinished = true
return
}
isExecuting = true
print("Starting download for \(url) on \(Thread.current)")
task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
defer {
self?.isExecuting = false
self?.isFinished = true
}
guard let self = self, !self.isCancelled else {
print("Download cancelled for \(self?.url.absoluteString ?? "")")
return
}
if let error = error {
print("Download failed: \(error)")
return
}
self.imageData = data
print("Download complete for \(self.url)")
}
task?.resume()
}
override func cancel() {
super.cancel()
task?.cancel()
isExecuting = false
isFinished = true
print("Cancelled download for \(url)")
}
func getImageData() -> Data? { imageData }
}
// Usage with OperationQueue
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
let url = URL(string: "https://example.com/image.jpg")!
let downloadOp = AsyncImageDownloadOperation(url: url)
downloadOp.completionBlock = {
if let data = downloadOp.getImageData() {
print("Downloaded image with size: \(data.count) bytes")
}
}
queue.addOperation(downloadOp)
// Cancel after 1 second
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
downloadOp.cancel()
}
Explanation:
isAsynchronous = true
indicates the operation runs asynchronously.start()
initiates aURLSession
data task and manages state.cancel()
stops the network task and updates state.- KVO-compliant properties (
isExecuting
,isFinished
) ensure queue compatibility. defer
guarantees state cleanup after the task completes or fails.- Output (if cancelled early):
Starting download for https://example.com/image.jpg on <Thread 0x...> Cancelled download for https://example.com/image.jpg
Example: Chaining Asynchronous Operations
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
let downloadOp = AsyncImageDownloadOperation(url: URL(string: "https://example.com/image.jpg")!)
let processOp = BlockOperation {
guard !downloadOp.isCancelled, let data = downloadOp.getImageData() else {
print("Processing cancelled or no data")
return
}
print("Processing image with size \(data.count) on \(Thread.current)")
Thread.sleep(forTimeInterval: 1) // Simulate processing
}
processOp.addDependency(downloadOp)
queue.addOperations([downloadOp, processOp], waitUntilFinished: false)
queue.addBarrierBlock {
print("Image workflow complete")
}
Explanation:
processOp
depends ondownloadOp
, ensuring it only runs after the download completes.- The queue manages concurrency and dependencies automatically.
addBarrierBlock
confirms workflow completion.
Use Case: Asynchronous operations are ideal for network requests, file I/O, or tasks involving external resources where blocking is impractical.
Best Practices
- Check Cancellation: Always check
isCancelled
in operations to support graceful termination. - Manage Dependencies: Avoid circular dependencies to prevent deadlocks.
- Limit Concurrency: Set
maxConcurrentOperationCount
to balance performance and resource usage. - Use Completion Blocks: Leverage
completionBlock
for post-execution tasks. - Thread Safety: Use locks or serial queues for shared resources in concurrent operations.
- Integrate with Progress: Use
Progress
for user-facing workflows. - Combine with GCD: Dispatch UI updates to
DispatchQueue.main
or use GCD for simpler tasks. - Test Cancellation: Ensure operations handle cancellation correctly to avoid resource leaks.
Limitations
- Overhead:
OperationQueue
has more overhead than GCD for simple tasks. - State Management Complexity: Custom asynchronous operations require careful KVO compliance.
- Main Thread Safety: UI updates must be dispatched manually to the main queue.
- Learning Curve: Dependencies and state management can be complex for beginners.