What is an asynchronous function?
What is an asynchronous function? êŽë š
Updated for Xcode 15
Although Swift functions are synchronous by default, we can make them asynchronous by adding one keyword: async
. Inside asynchronous functions, we can call other asynchronous functions using a second keyword: await
. As a result, youâll often hear Swift developers talk about async/await as a way of coding.
So, this is a synchronous function that rolls a virtual dice and returns its result:
func randomD6() -> Int {
Int.random(in: 1...6)
}
let result = randomD6()
print(result)
And this is an asynchronous or async function:
func randomD6() async -> Int {
Int.random(in: 1...6)
}
let result = await randomD6()
print(result)
The only part of the code that changed is adding the async
keyword before the return type and the await
keyword before calling it, but those changes tell us three important things about async functions.
First, async
is part of the functionâs type. The original, synchronous function returns an integer, which means we canât use it in a place that expects it to return a string. However, by marking the code async
weâve now made it an asynchronous function that returns an integer, which means we canât use it in a place that expects a synchronous function that returns an integer.
This is what I mean when I say that the async nature of the function is part of its type: it affects the way we refer to the function everywhere else in our code. This is exactly how throws
works â you canât use a throwing function in a place that expects a non-throwing function.
Second, notice that the work inside our function hasnât actually changed. The same work is being done as before: this function doesnât actually use the await
keyword at all, and thatâs okay. You see, marking a function with async
means it might do asynchronous work, not that it must. Again, the same is true of throws
â some paths through a function might throw, but others might not.
A third key difference arises when we call randomD6()
, because we need to do so asynchronously. Swift provides a few ways we can do this, but in our example we used await
, which means ârun this function asynchronously and wait for its result to come back before continuing.â
So, whatâs the actual difference between synchronous and asynchronous functions? To answer that, I want to show you a real function that does some async work to fetch a file from a web server:
func fetchNews() async -> Data? {
do {
let url = URL(string: "https://hws.dev/news-1.json")!
let (data, _) = try await URLSession.shared.data(from: url)
return data
} catch {
print("Failed to fetch data")
return nil
}
}
if let data = await fetchNews() {
print("Downloaded \(data.count) bytes")
} else {
print("Download failed.")
}
Later on weâll be digging in more to how that actually works, but for now what matters is that the URLSession.shared.data(from:)
method we call is asynchronous â its job is to fetch some data from a web server, without causing the whole program to freeze up.
Weâve already seen that synchronous functions cause blocking, which leads to performance problems. Async functions do not block: when we call them with await
we are marking a suspension point, which is a place where the function can suspend itself â literally stop running â so that other work can happen. At some point in the future the functionâs work completes, and Swift will wake it back up out of its âsuspended animationâ-like existence and it will carry on working.
This might sound simple on paper, but in practice itâs very clever.
First, when an async function is suspended, all the async functions that called it are also suspended; they all wait quietly while the async work happens, then resume later on. This is really important: async functions have this special ability to be suspended that regular synchronous functions do not. Itâs for this reason that synchronous functions cannot call async functions directly â they donât know how to suspend themselves.
Second, a function can be suspended as many times as is needed, but it wonât happen without you writing await
there â functions wonât suspend themselves by surprise.
Third, a function that is suspended does not block the thread itâs running on, and instead it gives up that thread so that Swift can do other work instead. Note: Although we can tell Swift how important many tasks are, we donât get to decide exactly how the system schedules our work â it automatically takes care of all the threads working under the hood. This means if we call async function A without waiting for its result, then a moment later call async function B, itâs entirely possible B will start running before A does.
Fourth, when the function resumes, it might be running on the same thread as before, but it might not; Swift gets to choose, and you shouldnât make any assumptions here. This means by the time your function resumes all sorts of things might have changed in your program â a few milliseconds might have passed, or perhaps 20 seconds or more.
And finally, I know Iâm repeating myself, but this matters: just because a function is async doesnât mean it will suspend â the await
keyword only marks a potential suspension point. Most of the time Swift knows perfectly well that the function weâre calling is async, so this await
keyword is as much for us as it is for the compiler â itâs a way of clearly marking which parts of the function might suspend, so you can know for sure which parts of the function run as one atomic chunk. (âAtomicâ is a fancy word meaning âindivisibleâ â a chunk of work where all lines of code will execute without being interrupted by other code running.) This requirement for await
is identical to the requirement for try
, where we must mark each line of code that might throw errors.
So, async functions are like regular functions, except they have a superpower: if they need to, they can suspend themselves and all their callers, freeing up their thread to do other work.