CodeGym /Các khóa học /C# SELF /Cấu trúc của phương thức bất đồng bộ (

Cấu trúc của phương thức bất đồng bộ ( async/ await)

C# SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Hãy tưởng tượng mã của bạn — giống như một hàng đợi trong cửa hàng. Nếu bạn chạy một phương thức đồng bộ, cả dòng (hàng của bạn) sẽ chờ cho tới khi thu ngân xong với một khách rồi mới tới người tiếp theo. Còn nếu phương thức bất đồng bộ, bạn chỉ đặt đồ lên quầy rồi đi làm việc khác, khi thu ngân xong — họ sẽ gọi bạn.

Phương thức bất đồng bộ (phương thức async) trong .NET — là một phương thức bình thường có một "nhãn" đặc biệt phía trước (async), cho phép bên trong sử dụng toán tử await để chờ các thao tác bất đồng bộ khác (ví dụ: mạng hoặc file) mà không chặn luồng chính. Chính "nhãn" này biến cửa hàng đồng bộ nhàm chán thành dịch vụ hiện đại không cần xếp hàng.

Chữ ký cơ bản của phương thức bất đồng bộ

Phương thức bất đồng bộ bắt buộc phải khai báo với từ khóa async trong chữ ký. Đây là ví dụ cơ bản nhất:

public async Task MyAsyncMethod()
{
    // Mã bất đồng bộ của bạn ở đây
}

Các kiểu trả về của phương thức bất đồng bộ:

Kiểu trả về Mô tả Ví dụ
Task
Phương thức thực hiện công việc bất đồng bộ nhưng không trả về kết quả
public async Task SaveFileAsync()
Task<T>
Phương thức thực hiện công việc bất đồng bộ và trả về kết quả kiểu T
public async Task<int> GetSumAsync()
void
Chỉ dùng cho sự kiện. Không khuyến nghị dùng ở chỗ khác!
public async void Button_Click(...)
ValueTask / ValueTask<T>
Cho các kịch bản hiệu năng cao, khi kết quả thường đã sẵn sàng đồng bộ

Quan trọng: Đừng dùng async void ở đâu khác ngoài handler sự kiện! Nếu không bạn có nguy cơ mất cơ chế xử lý lỗi bình thường.

2. Ví dụ đơn giản về phương thức bất đồng bộ

Quay lại ứng dụng luyện tập của chúng ta (giả sử là chương trình console đơn giản), thêm một phương thức bất đồng bộ lấy dữ liệu từ mạng. Chúng ta sẽ giả lập độ trễ.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("Bắt đầu phương thức bất đồng bộ...");
        await MyFakeDownloadAsync();
        Console.WriteLine("Hoạt động bất đồng bộ hoàn tất!");
    }

    static async Task MyFakeDownloadAsync()
    {
        Console.WriteLine("Bắt đầu tải (giả lập độ trễ 2 giây)...");
        await Task.Delay(2000); // Giả lập thao tác dài
        Console.WriteLine("Tải xong!");
    }
}

Chú thích cho ví dụ:

  • Từ khóa async được đặt ở MainMyFakeDownloadAsync.
  • Toán tử await chỉ dùng được bên trong phương thức được đánh dấu async.
  • Phương thức Task.Delay(2000) giả lập thao tác lâu (ví dụ gọi mạng).
  • Sau khi gặp await, luồng không bị chặn — thực thi của Main "tạm dừng" cho tới khi độ trễ kết thúc.

3. Phương thức bất đồng bộ hoạt động như thế nào bên trong?

Khi compiler thấy phương thức có từ khóa async, nó biến phương thức đó thành một "máy trạng thái", lưu lại chỗ dừng, biến cục bộ và tiếp tục thực thi sau khi thao tác awaited hoàn thành.

Đây là sơ đồ rất đơn giản:


+-----------------------------------------------------------+
|          Gọi phương thức bất đồng bộ (ví dụ, await X)     |
+-------------------------+---------------------------------+
                          |
                          v
            Thao tác đã hoàn thành? (không phải Task.Completed)
                |                  |
                | có               | không
                v                  v
          Tạm dừng               Trả về ngay
          thực thi,              kết quả
          ghi nhớ                (hoặc ném lỗi)
          context
                |
      Thao tác bất đồng bộ hoàn thành...
                |
                v
        Máy trạng thái "đánh thức" phương thức,
        thực thi tiếp từ vị trí sau await

Không cần thuộc hết chi tiết triển khai: context (chỗ dừng và biến cục bộ) được lưu và tự động phục hồi khi thao tác hoàn thành.

Nơi có thể (và không nên) dùng từ khóa asyncawait

  • async đặt trước khai báo phương thức (hoặc lambda).
  • Bên trong phương thức async có thể (và nên) dùng await.
  • Nếu cố gắng viết await trong phương thức không có async — sẽ lỗi biên dịch.
  • Nếu khai báo async nhưng trong phương thức không có bất kỳ await nào — bạn sẽ nhận cảnh báo; phương thức sẽ chạy đồng bộ và trả về một Task đã hoàn thành.

4. Các biến thể giá trị trả về và đặc điểm của chúng

Kết quả Task

Nếu phương thức thực hiện hành động bất đồng bộ nhưng không trả về giá trị, dùng Task:

public async Task SaveToFileAsync(string path)
{
    await Task.Delay(1000);
    // Lưu gì đó vào file
}

Kết quả Task<T>

Nếu cần kết quả, dùng Task<T>:

public async Task<int> CalculateAsync()
{
    await Task.Delay(500);
    return 42;
}

Gọi:

int result = await CalculateAsync();

Kết quả async void

Dùng chỉ cho sự kiện! Ví dụ handler click của nút:

private async void Button_Click(object sender, EventArgs e)
{
    await Task.Delay(1000);
    // Làm gì đó thêm
}

Tại sao không dùng async void trong các phương thức bình thường? Vì bạn không biết khi nào phương thức đó kết thúc, và không thể bắt exception bên trong nó đúng cách.

Kết quả ValueTaskValueTask<T>

Xuất hiện cho các thư viện hiệu năng cao, khi kết quả thường đã sẵn sàng đồng bộ — giúp tránh allocation Task thừa. Ở giai đoạn bắt đầu học có thể chưa cần dùng: không gặp nhiều.

5. Ví dụ: máy tính bất đồng bộ trả về kết quả

Giả sử chương trình tính tổng hai số "có độ trễ", như thể đó là phép tính rất phức tạp:

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("Nhập số thứ nhất:");
        int a = int.Parse(Console.ReadLine()!);

        Console.WriteLine("Nhập số thứ hai:");
        int b = int.Parse(Console.ReadLine()!);

        Console.WriteLine("Đang tính toán...");
        int sum = await CalculateSumAsync(a, b);
        Console.WriteLine($"Tổng: {sum}");
    }

    static async Task<int> CalculateSumAsync(int x, int y)
    {
        await Task.Delay(2000); // Thao tác "dài"
        return x + y;
    }
}

Điểm quan trọng ở đây:

  • CalculateSumAsync — phương thức bất đồng bộ, trả về Task<int>, bên trong có await.
  • Gọi trong Main dùng await (nhắc lại: từ C# 7.1 phương thức Main có thể là bất đồng bộ).
  • Trong lúc tính tổng, luồng không bị chặn — có thể hiển thị tiến trình hoặc làm việc khác.

6. Phương thức lồng nhau và await lồng nhau

Phương thức bất đồng bộ có thể gọi phương thức bất đồng bộ khác, và phương thức đó lại gọi tiếp. Toàn bộ chuỗi sẽ tiếp tục tự động:

public async Task<int> StartWorkAsync()
{
    int data = await GetDataAsync();
    int result = await ProcessDataAsync(data);
    return result;
}

Đơn giản thôi: chờ xong thao tác này rồi tới thao tác kia.

7. Bẫy và lỗi thường gặp

1. Đừng quên async!
Nếu quên async, compiler sẽ "phàn nàn" khi thấy await bên trong phương thức và sẽ không cho build.

2. Đừng dùng async void trong phương thức bình thường
Các phương thức void bất đồng bộ (ngoại trừ handler sự kiện) — là "hố đen" cho exception. Lỗi trong đó có thể "chìm", bạn có thể không biết.

3. Phương thức bất đồng bộ KHÔNG có await
Có thể làm vậy, nhưng vô nghĩa — phương thức sẽ chạy đồng bộ và trả về một Task đã hoàn thành.

public async Task DoNothingAsync()
{
    // Ôi, không có await nào!
}

4. Trả Task từ phương thức KHÔNG-bất đồng bộ
Người ta làm vậy để đồng nhất giao diện: một số thao tác thực sự bất đồng bộ, một số thao tác là tức thì.

public Task<int> Foo()
{
    // return 42; // Thế này không được!
    return Task.FromResult(42); // Được, nhưng không có bất đồng bộ thực sự.
}
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION