How to migrate your code to Swift’s Concurrency - Part II

Co-author: Ignacio Cerviño LinkedIn

Do you know how async functions are suspended and how async functions give up their executing thread to the system while waiting for its scheduled work to finish?

In this article, we’ll explore Swift’s new concurrency model, breaking down the differences between synchronous and asynchronous code, and how async/await functions manage thread suspension. We’ll also cover the practical side by explaining how to work with continuations and how to use the “add async alternative” shortcut. By the end of this article, you’ll have a solid understanding of how async code works in Swift and how to update your existing code that uses completion handlers, making it easier to modernize your projects.

Requirements:

if you haven’t, please take a look at the previous article before continue reading.

What is Concurrency? Asynchronous Code vs Synchronous Code.

Concurrency allows multiple tasks to run seemingly at the same time, improving performance and responsiveness in your apps. By handling multiple operations concurrently, you can ensure that tasks like network requests or file downloads don’t interrupt the main thread from further execution, keeping your app smooth and responsive.

Synchronous functions run tasks one at a time, occupying the thread until each task is completed. Asynchronous functions, however, allow tasks to be scheduled without interrupting the current thread, whether it’s the main thread or any other, enabling efficient concurrency.

In this section, we’ll break down what concurrency means and how it helps streamline the execution of tasks, followed by an explanation of synchronous vs. asynchronous functions and their role in managing concurrency.

Synchronous Code

Synchronous code is executed sequentially. One task is completed before the next begins. If a task takes time, the program waits, blocking further execution until the task is finished.

The fetchData() function interrupts the continuation of the flow until the data is fetched. When the function finishes fetching the data, it moves to the following line of code.

Asynchronous Code

Asynchronous code allows the program to continue executing without waiting for a task to complete. This is crucial for maintaining a responsive user interface, especially in applications where tasks like network calls or heavy computations are involved.

Note: DispatchQueue.global().async schedules the completion work into an arbitrary thread allowing the program to execute another code.

Key Differences

  • Execution Flow: Synchronous code blocks the current thread until the task (method or function) is complete, while asynchronous code allows the program to continue executing by scheduling the asynchronous code in an arbitrary thread. For example, an expensive task could block the UI if it is executed synchronously on the main thread. If the operation does not involve any UI elements, then it could be executed asynchronously.

  • Performance: Asynchronous code generally leads to better performance in applications that require heavy or time-consuming tasks, as it keeps the UI responsive.

  • Code Complexity: While synchronous code is straightforward and easier to reason about, asynchronous code requires handling of completion handlers, tasks, and error propagation, which can add complexity.

How Concurrency works on Swift’s Concurrency’s async/await.

Swift’s Concurrency model with async/await simplifies writing asynchronous code by managing background tasks without interrupting the main thread. The system handles thread suspension and resumption automatically, leading to cleaner code and better app performance.

Note: By using “Task” the system will schedule the work in an arbitrary thread. Then the await keyword will suspend, giving up the control of its thread to the system while waiting for URLSession.shared.data(from: to be resolved by the system. After URLSession.shared.data(from: function is complete, the work will continue in the same thread or any other thread decided by the system, in this case Data fetched: \(data) will be printed on the debug console.

Note: Please do not to confuse task with Swift Concurrency’s “Task”, in this context task refers to any operation (method or function) that is being executed.

Async Functions: Suspending Threads

Unlike synchronous functions, async functions can give up control of the thread by suspending.

When you call an async function, it takes control of the thread, just like a normal function. However, an async function can suspend, releasing the thread back to the system. This allows the system to use the thread for other tasks while the async function waits for something, such as a network response.

Once the async function is ready to continue, the system resumes it, and the function takes control of the thread again. An async function can suspend and resume multiple times, or not at all. Even if you see an await, it doesn't always mean the function will suspend.

Eventually, the async function finishes and returns control of the thread along with its result or an error.

Important takeaways

  • When you mark a function async, you’re allowing it to suspend. And when a function suspends itself, it suspends its callers too. So its callers must be async as well.

  • The await keyword shows where an async function might suspend during its execution.

  • While an async function is suspended, the thread is free to handle other tasks, so the app’s state can drastically change before the async function resumes.

  • When an async function resumes, it may continue on a completely different thread than the one it started on, depending on how the system schedules it.

  • Finally, when an async function resumes, it continues execution from where it stopped, using the result of the suspended call.

Continuations - How to bridge completion handlers with async/await code

Continuations are used to bridge the gap between the old-style callback-based asynchronous APIs and the modern async/await syntax. When you're working with existing APIs that use closures or callbacks to handle asynchronous operations, continuations allow you to convert these into async/await style functions, making your code cleaner and more in line with Swift's concurrency model.

A continuation is a mechanism that lets you pause a function's execution and resume it later. In the context of async/await, Swift provides types like withCheckedThrowingContinuation  to handle these pauses and resumptions.

Example Code

Explanation of the code

  1. withCheckedThrowingContinuation: This function creates a continuation that can be used to resume the suspended function. It’s called "throwing" because the continuation can return a value or throw an error, matching the behavior of the original callback-based function.

  2. Suspending the Function: Inside withCheckedThrowingContinuation, the function execution is suspended until continuation.resume is called. This is where the callback will "continue" the function after the asynchronous operation is done.

  3. Resuming the Function: Depending on whether the callback receives success or failure, the continuation is resumed by calling resume(returning:) for success or resume(throwing:) for failure. This allows the function to return the result to the caller as if it were a normal synchronous function.

Refactoring tip - Add async alternative shortcut

Let’s look at some techniques when migrating to async/await.

When refactoring a function that currently uses a completion handler to async/await, it's preferable to create a second version of the function as async while keeping the original completion handler version. This ensures that other parts of your code, which still rely on the completion handler, continue to work while you gradually refactor.

We can easily do this by using the “Create Async Alternative” refactoring action. This option is available in the Code action menu (Cmd-Shift-A). Simply choose the option to add the async alternative.

Notice that the async refactoring has added a deprecation warning to the original one. These are going to help guide me to parts of my code that could next benefit from refactoring to call this new async version.

Next
Next

How to migrate your code to Swift’s Async/Await - Part I