DEV Community

Cover image for A brief history of async/await
max-arshinov
max-arshinov

Posted on • Edited on

A brief history of async/await

The async/await pattern is a syntactic feature of many programming languages that allows an asynchronous, non-blocking function to be structured similarly to an ordinary synchronous function. It is semantically related to the concept of a coroutine and is often implemented using similar techniques.

Perhaps, the first mainstream language that adopted this feature was C#. During the next decade, the feature spread across many other languages:

  • 2012 — C# 5
  • 2015 — Python 3.5, TypeScript 1.7
  • 2017 — ECMAScript 2017
  • 2018 — Kotlin 1.3
  • 2019 — Rust
  • 2020 — C++ 20
  • 2021 — Swift

That being said, in 2022 one can state async/await, implemented one way or another, is an industry standard. Despite that C# was the first mainstream language that popularised the async/await keywords, it wasn’t the language that invented the concept. F# added asynchronous workflows with await points in version 2.0 in 2007 (5 years before C#).

It’s interesting that with version 6 release in 2021 (14 years later) the recommended async pattern in F# was changed from async to task. At first glance, these two pieces of code look almost identical:

// async
let readFilesTask (path1, path2) =
   async {
        let! bytes1 = File.ReadAllBytesAsync(path1) |> Async.AwaitTask
        let! bytes2 = File.ReadAllBytesAsync(path2) |> Async.AwaitTask
        return Array.append bytes1 bytes2
   } |> Async.StartAsTask

// task
let readFilesTask (path1, path2) =
   task {
        let! bytes1 = File.ReadAllBytesAsync(path1)
        let! bytes2 = File.ReadAllBytesAsync(path2)
        return Array.append bytes1 bytes2
   }
Enter fullscreen mode Exit fullscreen mode

Ironically, the new recommended way is less functional (in the sense of functional programming) than the original one. Consider the following code:

let a = async {
   printfn "async"
}
let seq = seq {a; a}

printfn "Begin"
seq |> Seq.iter Async.RunSynchronously
printfn "End"
Enter fullscreen mode Exit fullscreen mode

It will print:

Begin
async
async
End
Enter fullscreen mode Exit fullscreen mode

If we removed seq |> Seq.iter Async.RunSynchronously then the output would be just:

Begin
End
Enter fullscreen mode Exit fullscreen mode

In other words, Async is lazy by default. It will not start unless the code is awaited. Async will not cache the result as well. If we changed this code in favour of using tasks:

let t = task {
   printfn "task"
}

let seq = seq {t; t}

printfn "Begin"
seq |> Seq.iter  (Async.AwaitTask >> Async.RunSynchronously)
printfn "End"
Enter fullscreen mode Exit fullscreen mode

We would see:

task
Begin
End
Enter fullscreen mode Exit fullscreen mode

Not only tasks start as soon as they are created. Tasks' results are also cached. The await or let! keywords would check Task.IsCompleted property. When true, tasks are executed synchronously because the result is already cached in the Task.Result property.

Why does it matter? I highly recommend reading Async as surrogate IO by Mark Seemann and its comments. In a nutshell, I believe that async/await got a lot of inspiration from the Haskel IO monad which was released in Haskell 98. I'm not claiming that F# Async was a direct translation of Haskell's IO, but there are similarities. In this sense, Async has a much more in common with its monadic predecessor than Task.

In the end, F#, the language that brought academic concepts to the object-oriented world, adopted the implementation from C#. The task computation expression works on top of TPL library, so it has better interoperability with Task-based .NET async pattern. Practically speaking, F# fixed discrepancies between the platform and the language syntax. Every other language from the list above also tends to represent the async/await syntax by futures/promises or similar data structures. Referential transparency is not guaranteed in any of these implementations though.

  • P.S. Haskell lead developer Simon Marlow created the async package in 2012, the same year when async/await was introduced in C#.
  • P.P.S. It’s possible to abuse async/await syntax to build monad comprehensions in C#.

Top comments (0)