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
queuePriorityandqualityOfServiceto 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:
truewhen the operation is ready to execute (all dependencies satisfied). Default:trueunless dependencies exist. - isExecuting:
truewhen the operation is actively running. Default:false. - isFinished:
truewhen the operation completes or is cancelled. Default:false. - isCancelled:
truewhencancel()is called. Default:false. - isAsynchronous:
trueif 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=falseBlockOperation
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
isExecutingandisFinishedinternally. - Concurrent Execution: Multiple blocks may run concurrently on separate threads.
- Completion Block: Supports a
completionBlockfor 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 completedUse 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 finishedExplanation:
- Three blocks are added to a single
BlockOperation. - The
OperationQueuewithmaxConcurrentOperationCount = 3allows 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
isExecutingandisFinishedwith KVO compliance. - Checks
isCancelledto 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
maxConcurrentOperationCountto 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
queuePriorityandqualityOfServiceto prioritize tasks. - Progress Tracking: Integrates with
Progressfor task monitoring.
Key Properties and Methods:
maxConcurrentOperationCount: Maximum concurrent operations (1 for serial,defaultMaxConcurrentOperationCountfor system default).isSuspended:trueto pause,falseto 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 completeOperations 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 completeExplanation:
processOpwaits fordownloadOp, andsaveOpwaits 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
DispatchGroupbefore starting and leaves upon completion. NSLockensures thread-safe appends toresults.group.notifyruns 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
isCancelledchecks. 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
queuePriorityandqualityOfService, complementing GCD’s QoS. - Reusability: Operations encapsulate tasks as objects, enabling reuse and customization, unlike GCD’s one-off closures.
- Progress Tracking: Integrates with
Progressfor 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 = trueindicates the operation runs asynchronously.start()initiates aURLSessiondata task and manages state.cancel()stops the network task and updates state.- KVO-compliant properties (
isExecuting,isFinished) ensure queue compatibility. deferguarantees 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:
processOpdepends ondownloadOp, ensuring it only runs after the download completes.- The queue manages concurrency and dependencies automatically.
addBarrierBlockconfirms 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
isCancelledin operations to support graceful termination. - Manage Dependencies: Avoid circular dependencies to prevent deadlocks.
- Limit Concurrency: Set
maxConcurrentOperationCountto balance performance and resource usage. - Use Completion Blocks: Leverage
completionBlockfor post-execution tasks. - Thread Safety: Use locks or serial queues for shared resources in concurrent operations.
- Integrate with Progress: Use
Progressfor user-facing workflows. - Combine with GCD: Dispatch UI updates to
DispatchQueue.mainor use GCD for simpler tasks. - Test Cancellation: Ensure operations handle cancellation correctly to avoid resource leaks.
Limitations
- Overhead:
OperationQueuehas 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.