Mastering Concurrency: Task

VINODH KUMAR
6 min readFeb 20, 2024

--

Photo by airfocus on Unsplash

Swift 5.5 brought in powerful concurrency tools that simplify and strengthen our approach to various issues. These tools offer a comprehensive solution to data race conditions, making coding more streamlined. Eventually, they’ll enable writing code that resembles synchronous operations, reducing the need to consider threads or reactive streams.

Let’s revisit our program, this time emphasizing Swift’s modern concurrency tools. Unlike the threads and queues we’ve covered before, these tools are integrated deeply within the language, not just as an external library.

Basics

Creation and Execution:
Tasks in Swift are similar to threads or dispatch queues but offer a higher level of abstraction. For instance:

Task {
print(Thread.current)
}

This snippet creates a Task, akin to threads or dispatch queues, and prints the current thread. However, Tasks differ as they’re deeply ingrained within Swift, managing both how and what work is performed.

Task Generics and Asynchronous Context:
Tasks come with two generics: one for the result type and another for potential thrown errors:

let task: Task<Type, Error> = Task {
// Throw an error or return a result
}

They facilitate asynchronous work in a structured and error-handled manner. To invoke asynchronous functions within a Task, ‘await’ is utilized, indicating suspension points during asynchronous operations:

Task {
await doSomethingAsync()
}

This allows the Task to yield control while waiting for asynchronous operations to finish, enabling smooth asynchronous execution.

Compiler Distinctions and Thread Management:
Swift’s compiler discerns between synchronous and asynchronous code at compile time. This is akin to its error handling distinctions, ensuring proper contexts for potentially failing or asynchronous tasks. Unlike a direct correspondence with threads, Tasks handle a pool of threads efficiently, avoiding excessive thread creation.

Task Suspension:

try await Task.sleep(nanoseconds: NSEC_PER_SEC * 1000)

Task.sleep suspends a task without blocking the associated thread, enabling other tasks to utilize the thread, demonstrating cooperative concurrency. This behavior prevents thread blocking, efficiently leveraging the available pool of threads. Swift provides URLSession, FileManager, and similar non-blocking work threads, akin to the behavior of “sleep”.

Task Resumption and Thread Migration:
Tasks might be resumed on different threads from their initial starting points, indicating that assumptions about code execution on specific threads might not hold true. This feature is integral to the abstraction Swift provides, steering away from thinking about concurrency solely in terms of threads.

Task Priorities and Cancellation

Task Priorities:
Tasks in Swift support prioritization, allowing for specifying priority levels similar to Dispatch Queue:

Task(priority: .low) {
print("Low priority task")
}
Task(priority: .high) {
print("High priority task")
}

These priority levels convey the task’s importance, enabling the runtime to allocate resources accordingly.

Task Cancellation:
Cancelling tasks follows a similar pattern to other concurrency forms. For instance:

let task = Task {
print(Thread.current)
}
task.cancel()

However, immediately cancelling a task might not prevent its execution, unlike threads. Cooperative cancellation necessitates periodic checks within the task’s code.

Cooperative Cancellation Checks:

let task = Task {
guard !Task.isCancelled else {
print("Cancelled!")
return
}
print(Thread.current)
}
task.cancel()

Here, the code inside the Task block checks for cancellation, enabling early termination if the task has been cancelled.

Task Cancellation Integration:
Tasks seamlessly integrate with cancellation, even in asynchronous contexts:

let task = Task {
try Task.checkCancellation()
print(Thread.current)
}
task.cancel()

The invocation of Task.checkCancellation() within the task’s block throws an error if the task has been cancelled, allowing for ergonomic cancellation checks.

Deep Cancellation Integration:
Tasks exhibit deep integration with cancellation, affecting nested asynchronous operations:

let task = Task {
let (data, _) = try await URLSession.shared
.data(from: .init(string: "http://testurl.com/1MB.zip")!)
print(Thread.current, "Network request finished", data.count)
}
// Wait for a while before cancelling
Thread.sleep(forTimeInterval: 5)
// Cancel the task
task.cancel()

Here, even network requests made through URLSession can detect and handle cancellation, stopping their work and interrupting the parent task’s execution if cancelled during their operation.

Task locals

Task locals in Swift offer a way to manage contextual information within the scope of asynchronous tasks. They’re akin to thread-specific or queue-specific data in traditional concurrency models, allowing you to associate values with a task and access them from anywhere running within that task.

Let’s explore task locals using Swift code:

// Define a type to hold the task locals
enum MyLocals {
// Define task locals using @TaskLocal property wrapper
@TaskLocal static var id: Int!
// You can have multiple task locals within this type
// @TaskLocal static var someOtherValue: String!
}
// Set up the task local variables
MyLocals.$id.withValue(42) {
Task {
// Access the task local value within the task
print("Task Local ID:", MyLocals.id!)
}
}

Here’s what’s happening:
MyLocals is an enum used to organize and hold task local variables.
@TaskLocalproperty wrapper is applied to the `id` variable to make it a task local.
MyLocals.$id.withValue(42) sets the value of id to 42 within the task context using the withValue method.
- Inside the Task block, MyLocals.id is accessed to retrieve the task local value.

Task locals are confined to the lifespan of the task they’re associated with. If you set a value using withValue, it remains valid only within that specific operation or closure. However, when you create a new task within that context, it automatically inherits the task locals, enabling these values to persist and propagate deeper into the asynchronous system.

This capability is incredibly powerful when dealing with asynchronous operations that require contextual information but might span multiple layers of your code. Task locals provide a way to avoid passing these values explicitly through each function call, making your code cleaner and more maintainable.

Task Coorperation

The concept of cooperation in Swift’s concurrency model is about making efficient use of shared resources among asynchronous tasks, ensuring fair execution without overwhelming the system. Let’s delve deeper into this with some code examples.

In previous articles, we’ve explored how in traditional concurrency models, creating numerous threads led to resource contention and inefficient resource usage. Swift’s task-based concurrency aims to mitigate these issues by managing a limited number of threads efficiently.

Consider the code that attempts to create multiple tasks performing infinite loops:

// Creating a bunch of tasks with infinite loops
for _ in 0..<workCount {
Task {
while true {}
}
}
// Starting a task for computing the 50,000th prime number
Task {
print("Starting prime task")
nthPrime(50_000)
}

However, when you execute this, you’ll notice that nothing gets printed. This is because these tasks are getting stuck in infinite loops, blocking the cooperative thread pool.

Swift advocates for cooperation among tasks to avoid monopolizing resources. It encourages non-blocking approaches and provides the await Task.yield() method to allow tasks to yield control, letting other tasks utilize the threads efficiently. For instance:

// Creating tasks that yield control periodically
for _ in 1…workCount {
Task.detached {
while true {
await Task.yield()
}
}
}

By incorporating Task.yield() within the infinite loop of each task, we allow other tasks in the pool to execute. As a result, when executing a computationally intensive task like computing the 50,000th prime number concurrently with these yielding tasks, the prime computation finishes swiftly without being hindered by the other tasks.

This approach ensures that while tasks perform their work, they periodically relinquish control, enabling fair sharing of the cooperative thread pool’s resources. By practicing non-blocking asynchronous techniques and judiciously using Task.yield(), Swift’s concurrency model facilitates efficient execution without resource contention.

Understanding these principles of cooperation and non-blocking behavior becomes crucial to prevent data races or contention when multiple tasks access shared mutable state. It’s about ensuring that asynchronous operations don’t hog resources, allowing others to execute and preventing unexpected results due to concurrent data access.

Summary

Swift’s 5.5 update introduced powerful concurrency tools that revolutionize coding practices, particularly with the introduction of Tasks. These tools offer a robust solution to data race conditions, simplifying asynchronous code and reducing reliance on threads or reactive streams. Tasks, deeply integrated within Swift, revolutionize concurrency by managing how and what work is executed, encouraging non-blocking approaches, yielding control, supporting prioritization, cancellation, and managing task-specific contextual information. This article delves into the intricacies of Tasks, emphasizing their cooperative nature, and explores how their non-blocking behavior and cooperation ensure efficient resource usage, preventing data races in concurrent operations. Understanding these concepts becomes vital for maintaining smooth and fair execution among asynchronous tasks in Swift’s modern concurrency paradigm.

Continue Learning: We’ve covered a lot about concurrency in Swift, but there’s more to explore. Discover how to safeguard your data with @Sendable and Actors in our next article: Swift Concurrency: Safeguarding Data with @Sendable and Actors.

--

--

VINODH KUMAR

📱 Senior iOS Developer | Swift Enthusiast | Tech Blogger 🖥️ Connect with me on linkedin.com/in/vinodhkumar-govindaraj-838a85100