Async/await

Trong lập trình máy tính, mô thức async/await là một tính năng cú pháp của nhiều ngôn ngữ lập trình, cho phép cấu trúc nên hàm 'bất đồng bộ, non-blocking' theo lối y hệt ở vẻ ngoài như hàm đồng bộ thông thường. Về mặt ngữ nghĩa, nó gần gũi với khái niệm coroutine và thường hay được thực hiện[a] bằng kỹ thuật tương tự, và chủ yếu là nhằm tạo cơ hội để cho chương trình thực thi phần code khác trong khi chờ đợi tác vụ bất đồng bộ, chạy lâu dài nào đó hoàn thành, tác vụ như thế thường được biểu trưng bằng promise hoặc cấu trúc dữ liệu tương tự. Tính năng này hiện diện trong C# 5.0, C++ 20, Python 3.5, F#, Hack, Julia, Dart, Kotlin 1.1, Rust 1.39,[1] Nim 0.9.4,[2] JavaScript ES2017, Swift 5.5[3]Zig,[4] và trong một số công trình thử nghiệm trong các bản mở rộng, các phiên bản beta và các bản thực hiện[b] nhất định của Scala.[5]

Lịch sử

Vào năm 2007, F# đã thêm workflow bất đồng bộ cùng với cú pháp await trong phiên bản 2.0.[6] Điều này ảnh hưởng đến cơ chế async/await được thêm vào C# sau này.[7]

Microsoft đã phát hành phiên bản C# kèm với cú pháp async/await lần đầu tiên trong bản Async CTP (2011). Và sau này phát hành chính thức trong C# 5 (2012).[8]

Nhà phát triển dẫn dắt của Haskell, Simon Marlow, đã tạo ra package bất đồng bộ vào năm 2012.[9]

Python đã thêm hỗ trợ async/await trong phiên bản 3.5 vào năm 2015[10], thêm 2 từ khóa mới là asyncawait.

TypeScript đã thêm hỗ trợ cho async/await trong phiên bản 1.7 vào năm 2015.[11]

Javascript đã thêm hỗ trợ cho async/await vào năm 2017 làm một phần của bản JavaScript ECMAScript 2017.

Rust đã thêm hỗ trợ cho async/await trong phiên bản 1.39.0 vào năm 2019[12] với 1 từ khóa mới là async và mô thức lazy evaluation cho await.[13]

C++ đã thêm hỗ trợ async/await trong phiên bản 20 vào năm 2020 với 3 từ khóa mới là co_return, co_awaitco_yield.

Swift đã thêm hỗ trợ async/await trong phiên bản 5.5 vào năm 2021, thêm 2 từ khóa mới là asyncawait. Phiên bản này được phát hành cùng với bản thực hiện cụ thể cho Mô hình Actor với từ khóa actor[14] trong đó sử dụng async/await để điều hòa truy cập từ bên ngoài vào từng actor.

Ví dụ C#

Hàm C# bên dưới tải xuống tài nguyên từ URI và trả về độ dài của tài nguyên, sử dụng mô thức async/await như sau:

public async Task<int> FindPageSizeAsync(Uri uri) 
{
    byte[] data = await new HttpClient().GetByteArrayAsync(uri);
    return data.Length;
}
  • Đầu tiên, từ khóa async bảo cho C# rằng phương thức này là bất đồng bộ, nghĩa là phương thức này có thể sử dụng một số lượng tùy ý các biểu thức await và sẽ ràng buộc kết quả vào một promise nào đó.
  • Kiểu trả về, Task<T>, là cái tương tự của C# với với khái niệm promise, và ở đây bảo rằng kết quả của quá trình bất đồng bộ là kiểu int.
  • Khi phương thức này được gọi thì biểu thức đầu tiên là new HttpClient().GetByteArrayAsync(uri) của nó sẽ được thực thi, đấy là một phương thức bất đồng bộ khác trả về kiểu Task<byte[]>. Vì phương thức đấy là bất đồng bộ, nên nó sẽ không tải xuống hết phần dữ liệu rồi mới trả về. Mà thay vào đó, nó sẽ bắt đầu quá trình tải xuống bằng cơ chế non-blocking (chẳng hạn như thread chạy nền), rồi ngay lập tức trả về Task<byte[]> với trạng thái unresolved (chưa phân giải) và unrejected (chưa bị từ chối) cho hàm FindPageSizeAsync().
  • Với từ khóa await gắn vào Task đấy, FindPageSizeAsync() sẽ lập tức tiến hành trả về Task<int> cho caller của nó, rồi caller đó có thể tiếp tục xử lý những gì đó khác nếu cần.
  • Một khi GetByteArrayAsync() hoàn tất việc tải xuống, nó sẽ phân giải Task mà nó đã trả về thành giá trị dữ liệu tải về. Điều này sẽ trigger một callback và khiến FindPageSizeAsync() tiếp tục thực thi bằng việc gán giá trị đó cho data.
  • Cuối cùng, phương thức này trả về data.Length, một số nguyên đơn giản cho biết độ dài của mảng. Trình biên dịch diễn giải lại việc này thành các bước là phân giải cái Task<int> mà nó trả về lúc trước rồi trigger callback trong caller của phương thức này để làm gì đó với giá trị độ dài vừa nêu.

Hàm mà sử dụng async/await thì có thể dùng bao nhiêu biểu thức await cũng được tùy ý muốn, và mỗi biểu thức như thế sẽ được xử trí theo cùng cách (thông qua một promise sẽ chỉ được trả về cho caller cho lần await đầu tiên, còn các lần await sau sẽ tận dụng callback nội bộ). Hàm cũng có thể tự tay giữ lại đối tượng promise và đi xử lý việc khác trước (bao gồm cả việc khởi động những tác vụ bất đồng bộ khác), trì hoãn việc await promise đấy cho đến khi nào cần đến kết quả của promise đấy thì mới thôi. Hàm mà dùng promise thì cũng có phương thức kết tập promise cho phép ta await nhiều promise cùng một lúc hay trong một số mô thức đặc biêt (chẳng hạn như Task.WhenAll() trong .NET có chức năng trả về một Task không có giá trị, nó phân giải khi tất cả tác vụ trong đối số của Task.WhenAll() được phân giải xong). Nhiều kiểu promise cũng có các tính năng vượt xa những điều mà mô thức async/await hay dùng, chẳng hạn có thể thiết đặt nhiều hơn một callback kết quả hay có thể tra duyệt tiến trình của tác vụ nào mà chạy lâu cá biệt.

Trong trường hợp đặc thù như C#, và trong nhiều ngôn ngữ có tính năng này, thì mô thức async/await không phải là một phần cốt lõi trong runtime của ngôn ngữ, mà thay vào đó được thực hiện bằng lambda hoặc được thực hiện bằng 'phần tiếp tục' tại compile time. Thí dụ, trình biên dịch C# khả năng sẽ dịch đoạn code bên trên thành thứ gì đó như sau đây trước khi dịch nó thành định dạng bytecode IL:

public Task<int> FindPageSizeAsync(Uri uri) 
{
    Task<byte[]> dataTask = new HttpClient().GetByteArrayAsync(uri);
    Task<int> afterDataTask = dataTask.ContinueWith((originalTask) => {
        return originalTask.Result.Length;
    });
    return afterDataTask;
}

Bởi vì điều này, nếu phương thức giao diện mà cần trả về đối tượng promise, nhưng bản thân nó lại không yêu cầu await trong phần thân để chờ đợi bất kì tác vụ bất đồng bộ nào, thì nó cũng chẳng cần đến từ khóa tu sức[c] async và thay vào đó có thể trả về đối tượng promise trực tiếp luôn. Thí dụ, hàm có thể đưa ra một promise mà ngay lập tức phân giải thành giá trị kết quả nào đó (chẳng hạn như Task.FromResult() của C#), hoặc nó có thể đơn giản trả về một promise khác mà vô hình trung chính là promise cần.

Tuy nhiên có một điểm quan trọng cần lưu tâm của chức năng này, đó là mặc dù phần code trông giống như code blocking xưa giờ, thì thực ra bản chất nó là code non-blocking và còn có khả năng là đa thread nữa, nghĩa là nhiều sự kiện khác có thể xen vào trong khi chờ đợi promise được phân giải bằng await. Thí dụ, đoạn code sau, tuy luôn luôn chạy thành công nếu viết lại theo mô hình blocking mà không dùng await, nhưng theo đúng thế này thì lại có thể bị các sự kiện bên ngoài can thiệp vào trong quá trình await và do đó có thể xảy ra tình cảnh nội dung trong state chung bị thay đổi khi code chạy đến phần bên dưới await.

var a = state.a;
var data = await new HttpClient().GetByteArrayAsync(uri);
Debug.Assert(a == state.a); // Có khả năng là không được vì giá trị của state.a
                            // có thể đã bị handler của sự kiện xen giữa nào đó thay đổi rồi.
return data.Length;

Trong F#

F# đã thêm workflow bất đồng bộ trong phiên bản 2.0.[15] Workflow bất đồng bộ này được thực hiện thông qua biểu thức computation.[d] Ta có thể định nghĩa nó mà không cần chỉ định bất kì ngữ cảnh đặt biệt nào (như async trong C#). Trong workflow bất đồng bộ của F# thì thêm dấu cảm (!) vào từ khóa để bắt đầu tác vụ bất đồng bộ.

Hàm bất đồng bộ sau đây tải xuống dữ liệu từ URL thông qua workflow bất đồng bộ:

let asyncSumPageSizes (uris: #seq<Uri>) : Async<int> = async {
    use httpClient = new HttpClient()
    let! pages = 
        uris
        |> Seq.map(httpClient.GetStringAsync >> Async.AwaitTask)
        |> Async.Parallel
    return pages |> Seq.fold (fun accumulator current -> current.Length + accumulator) 0
}

Trong C#

Mô thức async/await được Microsoft đưa vào C# kể từ phiên bản 5.0 với tên gọi là 'Mô thức bất đồng bộ dựa trên Task'.[e][16] Phương thức bất đồng bộ cần phải trả về một trong các kiểu void, Task, Task<T>, và ValueTask<T> (kiểu cuối mới được thêm vào từ phiên bản 7.0). Phương thức bất đồng bộ mà trả về void là nhằm dùng cho event handler; trong hầu hết trường hợp khi phương thức không cần phải trả về giá trị gì (tức void), thì thay vào đó nên trả về Task vì làm vậy cho phép việc xử trí ngoại lệ trực quan hơn.[17]

Phương thức mà có sử dụng await thì phải được khai báo với từ khóa async. Trong phương thức khai báo bằng async mà có kiểu giá trị trả về là Task<T> thì trong đó phải có return statement của kiểu gán được vào T chứ không phải là vào Task<T>; trình biên dịch sẽ gói giá trị T đấy vào kiểu generic Task<T>. Bất kì phương thức nào mà có kiểu trả về Task hoặc Task<T> thì cũng có thể gọi phương thức đó với từ khóa await được, không nhất thiết phương thức đó phải được khai báo bằng async.

Phương thức bất đồng bộ sau đây tải xuống dữ liệu từ URL bằng await. Bởi vì phương thức này duyệt qua toàn bộ uri và phát đi tác vụ cho mỗi uri xong hẳn rồi mới chờ các tác vụ đấy hoàn thành bằng từ khóa await, nên tập tài nguyên có thể được tải đồng thời thay vì phải chờ đợi phần tài nguyên cuối hoàn tất trước khi bắt đầu tải phần tiếp theo.

public async Task<int> SumPageSizesAsync(ICollection<Uri> uris) 
{
    var client = new HttpClient();
    var loadUriTasks = new List<Task<byte[]>>();
    foreach (var uri in uris)
    {
        var loadUriTask = client.GetByteArrayAsync(uri);
        loadUriTasks.Add(loadUriTask);
    }
    
    var total = 0;
    foreach (var loadUriTask in loadUriTasks)
    {
        statusText.Text = $"Tìm thấy {total} byte ...";
        var resourceAsBytes = await loadUriTask;
        total += resourceAsBytes.Length;
    }
    statusText.Text = $"Tìm thấy {total} byte cả thảy";

    return total;
}

Trong Scala

Trong bản mở rộng thử nghiệm Scala-async cho Scala, await là một "phương thức", mặc dù nó không vận hành giống như phương thức bình thường.[18] Hơn nữa, không giống như ở C# 5.0 trong đó phương thức phải được đánh dấu là bất đồng bộ, trong Scala-async, ta có khối code được bao quanh bằng "lời gọi"[f] bất đồng bộ.

Cách nó hoạt động

Trong Scala-async, async thực ra được thực hiện thông qua Scala macro, khiến trình biên dịch phát sinh code khác hoàn toàn, và tạo ra bản thực hiện dựa trên máy trạng thái hữu hạn (được coi là có hiệu quả hơn bản thực hiện dựa trên monad, song viết bằng tay thì không tiện bằng).

Có mấy kế hoạch nhằm để Scala-async hỗ trợ nhiều loại thực hiện khác nhau, bao gồm cả loại phi bất đồng bộ.

Trong Python

Python 3.5 (2015)[19] đã thêm hỗ trợ cho async/await như đã mô tả trong PEP 492 (https://www.python.org/dev/peps/pep-0492/).

import asyncio

async def main():
    print("hello")
    await asyncio.sleep(1)
    print("world")

asyncio.run(main())

Trong JavaScript

Toán tử await trong Javascript chỉ có thể được dùng ở bên trong hàm bất đồng bộ. Nếu tham số là promise thì sự thực thi của hàm bất đồng bộ đấy sẽ tiếp tục khi promise đấy được phân giải (song trong trường hợp nếu như promise bị từ chối, thì một lỗi sẽ được ném ra và có thể xử trí lỗi đó thông qua cơ chế xử trí ngoại lệ của Javascript). Nếu tham số đấy không phải là promise thì tham số đấy bản thân nó sẽ được trả về ngay lập tức.[20]

Nhiều thư viện cung cấp đối tượng promise có thể dùng được với await, miễn là nó khớp với đặc tả promise bản địa của Javascript. Tuy nhiên, promise từ thư viện jQuery thì không tương thích với tiêu chuẩn Promises/A+ mãi đến bản jQuery 3.0.[21]

Đây là một ví dụ (được điều chỉnh lại từ bài viết này[22]):

async function createNewDoc() {
  let response = await db.post({}); // đăng tài liệu mới
  return db.get(response.id); // tìm bằng id
}

async function main() {
  try {
    let doc = await createNewDoc();
    console.log(doc);
  } catch (err) {
    console.log(err);
  }
}
main();

Trong C++

Trong C++, await (có tên là co_await trong C++) đã chính thức được hợp nhất vào phiên bản 20.[23] Coroutine hỗ trợ cho nó, cùng với các từ khóa như co_await thì sẵn có trong hai trình biên dịch GCCMSVC còn Clang thì mới chỉ hỗ trợ một phần.

Đáng lưu ý rằng std::promise và std::future, mặc dù có vẻ sẽ là hai loại đối tượng khả await, nhưng lại không hề thực hiện những cơ chế cần thiết để khi trả về chúng từ coroutine thì có thể await lên chúng được bằng co_await. Lập trình viên phải tự thực hiện một số lượng các hàm thành viên public, chẳng hạn như await_ready, await_suspend, và await_resume lên kiểu trả về để kiểu đó có thể await lên được. Có thể xem chi tiết tại cppreference.

#include <iostream>
#include "CustomAwaitableTask.h"

using namespace std;

CustomAwaitableTask<int> add(int a, int b)
{
    int c = a + b;
    co_return c;
}

CustomAwaitableTask<int> test()
{
    int ret = co_await add(1, 2);
    cout << "return " << ret << endl;
    co_return ret;
}

int main()
{
    auto task = test();

    return 0;
}

Trong C

Chưa có hỗ trợ chính thức cho async/await trong ngôn ngữ C. Một số thư viện coroutine chẳng hạn như s_task mô phỏng từ khóa await/async bằng macro.

#include <stdio.h>
#include "s_task.h"

// định nghĩa bộ nhớ stack cho các tác vụ
int g_stack_main[64 * 1024 / sizeof(int)];
int g_stack0[64 * 1024 / sizeof(int)];
int g_stack1[64 * 1024 / sizeof(int)];

void sub_task(__async__, void* arg) {
    int i;
    int n = (int)(size_t)arg;
    for (i = 0; i < 5; ++i) {
        printf("task %d, delay seconds = %d, i = %d\n", n, n, i);
        s_task_msleep(__await__, n * 1000);
        //s_task_yield(__await__);
    }
}

void main_task(__async__, void* arg) {
    int i;

    // tạo hai tác vụ con
    s_task_create(g_stack0, sizeof(g_stack0), sub_task, (void*)1);
    s_task_create(g_stack1, sizeof(g_stack1), sub_task, (void*)2);

    for (i = 0; i < 4; ++i) {
        printf("task_main arg = %p, i = %d\n", arg, i);
        s_task_yield(__await__);
    }

    // đợi cho đến khi các tác vụ con kết thúc
    s_task_join(__await__, g_stack0);
    s_task_join(__await__, g_stack1);
}

int main(int argc, char* argv) {

    s_task_init_system();

    // tạo tác vụ chính
    s_task_create(g_stack_main, sizeof(g_stack_main), main_task, (void*)(size_t)argc);
    s_task_join(__await__, g_stack_main);
    printf("all task is over\n");
    return 0;
}

Trong Perl 5

Module Future::AsyncAwait là chủ đề của một khoản chi cho Quỹ Perl vào tháng 9 năm 2018.[24]

Trong Rust

Vào ngày 7 tháng 11 năm 2019, async/await được phát hành vào phiên bản ổn định của Rust.[25] Hàm bất đồng bộ trong Rust được biến đổi[g] từ cú pháp tiện lợi sang hàm số có trả về giá trị có thực hiện trait Future. Hiện nay nó được thực hiện bằng máy trạng thái hữu hạn.[26]

// Trong file Cargo.toml của crate, ta cần `futures = "0.3.0"` trong mục các dependency
// để cho chúng ta có thể sử dụng crate futures

extern crate futures; // Hiện nay không có executor nào trong thư viện `std` cả.

// Cái này sẽ "khử đường" (desugar) về thành cái giống như
// `fn async_add_one(num: u32) -> impl Future<Output = u32>`
async fn async_add_one(num: u32) -> u32 {
    num + 1
}

async fn example_task() {
    let number = async_add_one(5).await;
    println!("5 + 1 = {}", number);
}

fn main() {
    // Tạo ra Future sẽ không khởi động execution
    let future = example_task();

    // `Future` chỉ thực thi khi ta thực sự poll nó chứ không như Javascript.
    futures::executor::block_on(future);
}

Trong Swift

Swift 5.5 (2021)[27] đã thêm hỗ trợ cho async/await như đã mô tả trong SE-0296 (https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md).

func getNumber() async throws -> Int {
    try await Task.sleep(nanoseconds: 1_000_000_000)
    return 42
}

Task {
    let first = try await getNumber()
    let second = try await getNumber()
    print(first + second)
}

Lợi ích và phê phán

Lợi điểm đáng kể của mô thức async/await trong những ngôn ngữ có hỗ trợ nó là nó tạo điều kiện viết code 'bất đồng bộ, non-blocking' với overhead tối thiểu mà lại trông hầu như giống hệt code 'đồng bộ, blocking' xưa giờ. Nói cụ thể, người ta cho rằng await là cách tốt nhất để viết code bất đồng bộ trong những chương trình có sử dụng kỹ thuật truyền thông điệp; hơn nữa, do gần với kiểu code blocking, nên await được nêu là mang ưu điểm dễ đọc và chỉ cần lượng boilerplate code tối thiểu.[28] Vì thế, async/await giúp cho lập trình viên dễ suy lý về chương trình của mình hơn, và await có xu hướng thúc đẩy nên code non-blocking tốt hơn, kiên cố hơn trong những ứng dụng cần đến nó. Các chương trình như vậy trải dài từ chương trình biểu thị giao diện đồ họa người dùng đến những chương trình stateful phía server có khả năng nới quy mô đến mức đồ sộ, chẳng hạn như game và ứng dụng tài chính.

Còn khi phê phán await, người ta cho thấy rằng await có xu hướng khiến cho code bao quanh phải trở nên bất đồng bộ luôn; về mặt khác, người ta biện luận rằng bản tính "truyền nhiễm" này của code await (có khi được đem so sánh với "vi-rút zombie") thực ra là điều cố hữu trong tất cả các loại lập trình bất đồng bộ, nên await như vậy không hề có gì độc lạ ở đây cả.[29]

Xem thêm

Ghi chú thuật ngữ

  1. ^ Implement
  2. ^ Implementation
  3. ^ Modifier
  4. ^ Computation Expressions
  5. ^ Task-based asynchronous pattern
  6. ^ Tức giống như gọi hàm, gọi phương thức.
  7. ^ Desugar

Tham khảo

  1. ^ “Announcing Rust 1.39.0” (bằng tiếng Anh). Truy cập ngày 7 tháng 11 năm 2019.
  2. ^ “Version 0.9.4 released - Nim blog” (bằng tiếng Anh). Truy cập ngày 19 tháng 1 năm 2020.
  3. ^ “Concurrency — The Swift Programming Language (Swift 5.5)”. docs.swift.org. Truy cập ngày 28 tháng 9 năm 2021.
  4. ^ “Zig Language Reference”.
  5. ^ “Scala Async”. GitHub. Truy cập ngày 20 tháng 10 năm 2013.
  6. ^ Syme, Don; Petricek, Tomas; Lomov, Dmitry (2011). The F# Asynchronous Programming Model. Springer Link. Lecture Notes in Computer Science (bằng tiếng Anh). 6539. tr. 175–189. doi:10.1007/978-3-642-18378-2_15. ISBN 978-3-642-18377-5. Truy cập ngày 29 tháng 4 năm 2021.
  7. ^ “The Early History of F#, HOPL IV”. ACM Digital Library (bằng tiếng Anh). Truy cập ngày 29 tháng 4 năm 2021.
  8. ^ Hejlsberg, Anders. “Anders Hejlsberg: Introducing Async – Simplifying Asynchronous Programming”. Channel 9 MSDN (bằng tiếng Anh). Microsoft. Truy cập ngày 5 tháng 1 năm 2021.
  9. ^ “async: Run IO operations asynchronously and wait for their results”. Hackage.
  10. ^ “What's New In Python 3.5 — Python 3.9.1 documentation”. docs.python.org. Truy cập ngày 5 tháng 1 năm 2021.
  11. ^ Gaurav, Seth (30 tháng 11 năm 2015). “Announcing TypeScript 1.7”. TypeScript. Microsoft. Truy cập ngày 5 tháng 1 năm 2021.
  12. ^ Matsakis, Niko. “Async-await on stable Rust! | Rust Blog”. blog.rust-lang.org (bằng tiếng Anh). Rust Blog. Truy cập ngày 5 tháng 1 năm 2021.
  13. ^ “Rust Gets Zero-Cost Async/Await Support in Rust 1.39”.
  14. ^ “Concurrency — the Swift Programming Language (Swift 5.6)”.
  15. ^ “Introducing F# Asynchronous Workflows”.
  16. ^ “Task-based asynchronous pattern”. Microsoft. Truy cập ngày 28 tháng 9 năm 2020.
  17. ^ Stephen Cleary, Async/Await - Best Practices in Asynchronous Programming
  18. ^ “Scala Async”. GitHub. Truy cập ngày 20 tháng 10 năm 2013.
  19. ^ “Python Release Python 3.5.0”.
  20. ^ “await - JavaScript (MDN)”. Truy cập ngày 2 tháng 5 năm 2017.
  21. ^ “jQuery Core 3.0 Upgrade Guide”. Truy cập ngày 2 tháng 5 năm 2017.
  22. ^ “Taming the asynchronous beast with ES7”. Truy cập ngày 12 tháng 11 năm 2015.
  23. ^ “ISO C++ Committee announces that C++20 design is now feature complete”. 25 tháng 2 năm 2019.
  24. ^ “September 2018 Grant Votes - The Perl Foundation”. news.perlfoundation.org. Truy cập ngày 26 tháng 3 năm 2019.
  25. ^ Matsakis, Niko. “Async-await on stable Rust!”. Rust Blog. Truy cập ngày 7 tháng 11 năm 2019.
  26. ^ Oppermann, Philipp. “Async/Await”. Truy cập ngày 28 tháng 10 năm 2020.
  27. ^ “Bản sao đã lưu trữ”. Bản gốc lưu trữ ngày 23 tháng 1 năm 2022. Truy cập ngày 27 tháng 6 năm 2022.
  28. ^ 'No Bugs' Hare. Eight ways to handle non-blocking returns in message-passing programs CPPCON, 2018
  29. ^ Stephen Cleary, Async/Await - Best Practices in Asynchronous Programming