Do you think IIT Guwahati certified course can help you in your career?
No
Introduction
Concurrency is a term that describes events or occurrences that occur or exist at the same moment. Concurrent programming is a programming technique in which two or more processes start, operate in parallel through context switching, and finish in an overlapping timespan by managing access to shared resources, such as a single CPU core.
This doesn't always imply that numerous processes are executing at the same time, even if the data suggest otherwise.
Swift concurrency is a built-in language feature that promises asynchronous code that is easy to write, understand, and, most importantly, free of race conditions.
Async/Await
Async is short for asynchronous, and it's a method attribute that indicates whether or not a method performs asynchronous work. People always suggest that we should use async/await to allow our program to run concurrently. However, this isn't quite true. Async/await are simple keywords added in Swift 5.5 that instruct the compiler that a given block of code should run asynchronously.
Let us understand the above topic using an example:
Consider a function “heavyProcess” that performs some heavy task and takes a while to finish. We will mark that function as asynchronous using the async keyword, as shown below.
func heavyProcess() async {
// heavy process
}
Asynchronous functions are a sort of function that can be paused in the middle of their execution. An asynchronous function, like any other function, can both throw an error and return a value.
func heavyProcess() async throws -> String {
// heavy process
return "somestring"
}
If a function is tagged as async, we must use the await keyword to invoke it, like in:
await heavyProcess()
The await keyword signals that the heavyPrpcess() method may be suspended because of its asynchronous nature. If we try to call the heavyPrpcess() function as a regular (synchronous) function, we'll get an error that says - 'async' call in a function that doesn't support concurrency.
Task
Swift 5.5 introduces the Task keyword. Apple defines a Task as an asynchronous work unit. Code can be paused and run asynchronously inside the context of a task. As a result, we may make a task to call our heavyProcess() function. Here's how to do it:
Assume we have two async functions, each of which returns an integer value, as seen below:
func taskA() async -> Int {
// Waiting for four seconds
await Task.sleep(4 * 1000000000)
return 4
}
func taskB() async -> Int {
// Waiting for five seconds
await Task.sleep(5 * 1000000000)
return 5
}
We can get the sum of the values returned by both of these functions by doing the following:
func doSum() {
Task {
let a = await taskA()
let b = await taskB()
let sum = a + b
print(sum) // Output: 9
}
}
As taskA() and taskB() run in sequential sequence, taskA() must complete first before taskB() may begin.
As you've probably observed, the code above isn't ideal. Because taskA() and taskB() are independent of one another, we can improve the execution time by performing both taskA() and taskB() at the same time, resulting in a 5-second completion time. Structured concurrency comes into play here.
We'll use structured concurrency to build two child tasks that run taskA() and taskB() at the same time. There are two main techniques to create a child task in Swift 5.5:
Using async-let binding
Using task group
In this article, we'll concentrate on the more easy approach that is using async-let binding.
Async-let Binding
func doSum() {
Task {
// Creating & starting a child task
async let a = taskA()
async let b = taskB()
let sum = await (a + b)
print(sum) // Output: 9
}
}
The above code demonstrates how to apply async-let binding in the previous example. On both the taskA() and taskB() functions, observe how we use the async and let keywords build an async-let binding. As a result, two-child tasks will be created, each of which will do both of these functions at the same time.
Because taskA() and taskB() are both marked as async, we'll have to wait for both of them to finish before we can access the values of a and b. As a result, we must use the await keyword when getting the value of a and b to indicate that the code may pause while waiting for taskA() and taskB() to complete.
Actor
Data races and deadlock are the most prevalent issues that arise when working with asynchronous and concurrent programmes. These kinds of issues are incredibly tough to diagnose and resolve. With the addition of actors in Swift 5.5, we can now rely on the compiler to warn us about any possible race situations in our code. So, how do actors work?
How does the Actor Work?
Actors are reference types that work in the same way as classes do. However, unlike classes, actors will ensure that only one job at a time can change the actors' state, removing the core cause of a race condition: multiple tasks changing/accessing the same object state at the same moment.
To make an actor, we need to use the actor keyword to annotate it. Here's an example of a Counter actor with a mutable count state that can be changed with the increment() method:
actor Counter {
private let name: String
private var count = 0
init(name: String) {
self.name = name
}
func increment() {
count += 1
}
func getName() -> String {
return name
}
}
We can create an actor in the same way that we can create a class:
let counter = Counter(name: "My Counter")
If we try to call the increment() method from outside the Counter actor, we'll get a compiler error that says – Actor-isolated instance method 'increment()' can't be referenced from a non-isolated context.
The compiler is attempting to safeguard the state of the Counter actor, which is why we're getting this issue. A race condition will arise if multiple threads call increment() at the same time. As a result, we can't merely call an actor's method like a regular instance method.
To enforce this constraint, we must use the await keyword to indicate that the increment() method may be suspended when called. This makes sense since, in order to ensure mutual exclusion on the count variable, the call site of the increment() may need to suspend and wait for other processes to complete before continuing.
Keeping this in mind, we can use what we learned in the async/await section to call increment() as follows:
let counter = Counter(name: "My Counter")
Task {
await counter.increment()
}
The Nonisolated Keyword
Now I'd like to direct your attention to the Counter actor's getName() method. Calling the getName() method, like calling the increment() method, will require the await annotation.
However, if you look closely, the getName() method just accesses the Counter's name constant; it does not change the Counter's state, so a race condition cannot be created.
In this case, we can mark the getName() function as nonisolated and so remove it from the actor's protection.
nonisolated func getName() -> String {
return name
}
Now we can use the getName() method like any other instance method:
let counter = Counter(name: "My Counter")
let x = counter.getName()
Frequently Asked Questions
What is concurrency in Swift?
Tasks in concurrent queues are executed one after the other without waiting for the previous job to finish. This does not imply that all jobs will be completed at the same time. Tasks will be executed based on system resources or threads from the thread pool that is available.
What are async and await in Swift?
Swift's async-await allows for structured parallelism, which makes complex asynchronous code easier to read. There's no need for completion closures, and calling many asynchronous functions one after the other is a lot more legible.
What is an actor in Swift?
The actor keyword is used to construct actors. Like structs, classes, and enums, this is a concrete nominal type in Swift. Actors, like classes, are reference types. As a result, they can be used to share state in your programme.
What is a task in Swift?
Swift's concurrency system, which was introduced at WWDC 2021, includes tasks. We can use a task to generate a concurrent environment from a non-concurrent procedure by executing async/await methods. When you first start working with tasks, you may notice certain similarities between dispatch queues and tasks.
Is concurrency the same as parallelism?
Concurrency is the task of running and managing multiple computations at the same time. While parallelism is the task of running multiple computations simultaneously.
Conclusion
In this article, we have discussed the following topics:
Concurrency in swift
Async/Await
Structured Concurrency
Actor in swift
After reading about the concurrency in swift, are you not feeling excited to read/explore more articles on the topic of swift? Don't worry; Coding Ninjas has you covered. To learn, see Swift syntax and program, Swift environment setup, and Swift protocols.