Pre Modern Concurrency Recap Swift part 2

abdul ahad
7 min readDec 21, 2024

--

Photo by National Cancer Institute on Unsplash

Concurrency in iOS development has evolved over time, offering developers a range of tools to manage parallel tasks. After exploring Grand Central Dispatch (GCD), we now focus on Operation, a robust, object-oriented alternative for handling concurrency with additional control and flexibility.

Table of Contents

1. Introduction to Operations

2. Understanding BlockOperation

3. Synchronous vs. Asynchronous Operations

4. Creating Custom Operations

5. Operation Queues

6. Real-World Example: Image Download and Processing

Introduction to Operations

What is an Operation?

An Operation represents a single unit of work. It’s an abstract class (Operation is actually a subclass of NSOperation in Objective-C) that encapsulates the code and data associated with a task. Operations can be used to perform tasks asynchronously or synchronously, depending on how they are configured.

Key Features:

Encapsulation: Operations encapsulate the logic of a task, making code modular and reusable.

Concurrency: They can be executed concurrently, allowing multiple operations to run simultaneously.

Dependencies: Operations can have dependencies on other operations, enabling you to control the execution order.

State Management: Operations have built-in support for KVO-compliant properties like isExecuting, isFinished, and isCancelled.

When to Use Operations?

Operations are ideal when you need:

Complex Task Management: For tasks that require dependencies or need to be cancelled.

Reusability: Encapsulating code into operations allows for easy reuse across your app.

Fine-Grained Control: Operations give you more control over execution than Grand Central Dispatch (GCD).

Understanding BlockOperation

BlockOperation is a concrete subclass of Operation that allows you to execute one or more blocks concurrently.

Features of BlockOperation:

Multiple Blocks: You can add multiple blocks to a single BlockOperation, and they will execute concurrently.

Completion Handling: The BlockOperation is considered finished when all of its blocks have completed execution.

Ease of Use: Simplifies the creation of operations without the need to subclass.

Example:

let blockOperation = BlockOperation()

blockOperation.addExecutionBlock {
print("Task 1")
sleep(2)
}

blockOperation.addExecutionBlock {
print("Task 2")
sleep(2)
}

blockOperation.addExecutionBlock {
print("Task 3")
sleep(2)
}

blockOperation.completionBlock = {
print("All tasks are complete.")
}

let operationQueue = OperationQueue()
operationQueue.addOperation(blockOperation)

Output:

Task 1
Task 2
Task 3
All tasks are complete.

Synchronous vs. Asynchronous Operations

Synchronous Operations

By default, operations are synchronous. When you execute an operation by calling its start() method directly, it runs synchronously on the current thread.

Characteristics:

Blocking: The current thread waits until the operation completes.

Simple State Management: No need to manage the operation’s execution state manually.

Asynchronous Operations

Asynchronous operations run independently of the current thread, allowing it to continue executing while the operation runs in the background.

Characteristics:

Non-Blocking: The current thread is free to perform other tasks.

State Management: Requires manual management of KVO-compliant properties (isExecuting, isFinished).

Making an Operation Asynchronous

To create an asynchronous operation, you need to:

1. Subclass Operation.

2. Override the necessary properties and methods.

3. Manage the operation’s state transitions.

Example of an Asynchronous Operation:

class AsyncOperation: Operation {
enum State: String {
case isReady, isExecuting, isFinished

fileprivate var keyPath: String {
return rawValue
}
}

private var _state = State.isReady

private let stateQueue = DispatchQueue(label: "com.example.operation.state", attributes: .concurrent)

private func setState(_ newState: State) {
willChangeValue(forKey: _state.keyPath)
willChangeValue(forKey: newState.keyPath)
stateQueue.sync(flags: .barrier) {
_state = newState
}
didChangeValue(forKey: _state.keyPath)
didChangeValue(forKey: newState.keyPath)
}

override var isReady: Bool {
return super.isReady && state == .isReady
}

override var isExecuting: Bool {
return state == .isExecuting
}

override var isFinished: Bool {
return state == .isFinished
}

override var isAsynchronous: Bool {
return true
}

private var state: State {
get {
return stateQueue.sync {
_state
}
}
set {
setState(newValue)
}
}

override func start() {
if isCancelled {
state = .isFinished
return
}
state = .isExecuting
main()
}

override func cancel() {
super.cancel()
state = .isFinished
}
}

Creating Custom Operations

Custom operations provide the flexibility to encapsulate complex tasks with fine-grained control over their execution.

Steps to Create a Custom Operation:

1. Subclass Operation or AsyncOperation: Depending on whether the operation is synchronous or asynchronous.

2. Override main() Method: Place the task’s code inside this method.

3. Manage State: If asynchronous, handle the state transitions (isExecuting, isFinished).

Example: Synchronous Custom Operation

class DataProcessingOperation: Operation {
let inputData: Data
var outputData: Data?

init(inputData: Data) {
self.inputData = inputData
super.init()
}

override func main() {
if isCancelled {
return
}

// Perform data processing
outputData = processData(inputData)
}

private func processData(_ data: Data) -> Data {
// Simulate data processing
sleep(2)
return data // Return processed data
}
}

Example: Asynchronous Custom Operation

class NetworkRequestOperation: AsyncOperation {
let url: URL
var responseData: Data?

init(url: URL) {
self.url = url
super.init()
}

override func main() {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
defer { self.state = .isFinished }
if self.isCancelled {
return
}
self.responseData = data
}
task.resume()
}
}

Operation Queues

OperationQueue manages the execution of Operation objects. It handles the scheduling, execution, and coordination of operations, allowing you to perform tasks concurrently without managing threads yourself.

Key Features:

Concurrent Execution: Runs multiple operations simultaneously, depending on the queue’s configuration.

Dependency Management: Automatically manages operation dependencies.

Priority and Quality of Service: Operations can have priorities and QoS classes assigned to influence execution order.

Thread Management: Abstracts away thread creation and management.

Important Properties:

maxConcurrentOperationCount: Controls the maximum number of concurrent operations.

isSuspended: Pauses the execution of queued operations when set to true.

qualityOfService: Sets the quality of service for the queue.

Example:

let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 2

let operation1 = BlockOperation {
print("Operation 1")
sleep(2)
}

let operation2 = BlockOperation {
print("Operation 2")
sleep(2)
}

let operation3 = BlockOperation {
print("Operation 3")
sleep(2)
}

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

Output:

Operation 1
Operation 2
Operation 3

Real-World Example: Image Download and Processing

Let’s create a real-world example where we need to download images from the internet and process them (e.g., apply filters). We’ll use Operation and OperationQueue to manage these tasks efficiently.

Problem Statement

Task: Download multiple images concurrently and apply a filter to each image.

Requirements:

• Limit the maximum number of concurrent downloads to avoid overwhelming the network.

• Ensure that image processing starts only after the image has been downloaded.

• Update the UI with the processed images on the main thread.

Solution Overview

We’ll create two custom operations:

1. ImageDownloadOperation: An asynchronous operation to download an image.

2. ImageProcessingOperation: A synchronous operation to process the downloaded image.

We’ll then add these operations to an OperationQueue, setting up dependencies so that each ImageProcessingOperation depends on its corresponding ImageDownloadOperation.

Step-by-Step Implementation

1. ImageDownloadOperation

import UIKit

class ImageDownloadOperation: AsyncOperation {
let imageURL: URL
var downloadedImage: UIImage?

init(imageURL: URL) {
self.imageURL = imageURL
super.init()
}

override func main() {
let task = URLSession.shared.dataTask(with: imageURL) { data, response, error in
defer { self.state = .isFinished }

if self.isCancelled {
return
}

if let data = data {
self.downloadedImage = UIImage(data: data)
}
}
task.resume()
}
}

2. ImageProcessingOperation

import UIKit

class ImageProcessingOperation: Operation {
let inputImage: UIImage
var outputImage: UIImage?

init(inputImage: UIImage) {
self.inputImage = inputImage
super.init()
}

override func main() {
if isCancelled {
return
}

// Apply a sepia filter as an example
outputImage = applySepiaFilter(to: inputImage)
}

private func applySepiaFilter(to image: UIImage) -> UIImage? {
guard let ciImage = CIImage(image: image) else { return nil }

let filter = CIFilter(name: "CISepiaTone")!
filter.setValue(ciImage, forKey: kCIInputImageKey)
filter.setValue(0.8, forKey: kCIInputIntensityKey)

guard let outputCIImage = filter.outputImage else { return nil }
let context = CIContext()

if let cgImage = context.createCGImage(outputCIImage, from: outputCIImage.extent) {
return UIImage(cgImage: cgImage)
}

return nil
}
}

3. Setting Up the Operation Queue

let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 2 // Limit concurrent downloads

let imageUrls = [
URL(string: "https://example.com/image1.jpg")!,
URL(string: "https://example.com/image2.jpg")!,
URL(string: "https://example.com/image3.jpg")!
]

var processedImages: [UIImage] = []

for imageUrl in imageUrls {
let downloadOperation = ImageDownloadOperation(imageURL: imageUrl)
let processingOperation = ImageProcessingOperation(inputImage: UIImage())

// Set up dependency
processingOperation.addDependency(downloadOperation)

// Completion Block for Processing Operation
processingOperation.completionBlock = {
if let processedImage = processingOperation.outputImage {
// Append to array in a thread-safe manner
DispatchQueue.main.async {
processedImages.append(processedImage)
// Update UI if needed
}
}
}

// Update the input image for processing operation after download
downloadOperation.completionBlock = {
if let downloadedImage = downloadOperation.downloadedImage {
processingOperation.inputImage = downloadedImage
}
}

// Add operations to the queue
operationQueue.addOperation(downloadOperation)
operationQueue.addOperation(processingOperation)
}

4. Handling UI Updates

Since UI updates must occur on the main thread, ensure that any UI modifications are dispatched accordingly.

processingOperation.completionBlock = {
if let processedImage = processingOperation.outputImage {
DispatchQueue.main.async {
// Update your UIImageView or collection view here
imageView.image = processedImage
}
}
}

Explanation

Concurrency Control: By setting maxConcurrentOperationCount, we control the number of simultaneous downloads.

Dependencies: Each ImageProcessingOperation depends on its corresponding ImageDownloadOperation, ensuring that processing starts only after the download is complete.

Thread Safety: Access to shared resources (like the processedImages array) is managed on the main thread to avoid race conditions.

Conclusion

Operations and operation queues in Swift provide a strong framework for handling concurrency with high levels of control and flexibility. By encapsulating tasks within operations and managing them through operation queues, developers can create scalable and efficient applications that make full use of system resources without sacrificing code readability or maintainability.

In this article, we’ve covered:

• The basics of Operation and BlockOperation.

• How to create synchronous and asynchronous operations.

• Managing operations using OperationQueue.

• Implementing a real-world example involving image downloading and processing.

References

--

--

abdul ahad
abdul ahad

Written by abdul ahad

A software developer dreaming to reach the top and also passionate about sports and language learning

No responses yet