+3

Async/Await Best Practices trong C#

Hi anh em, lại là mình đây. Tiếp tục series chia sẻ kinh nghiệm lập trình và các best practice, hôm nay mình sẽ nói về async/await trong C#.

Trong quá trình làm việc, mình thấy việc sử dụng async/await không đúng cách có thể gây ra nhiều vấn đề nghiêm trọng như memory leak, thread pool starvation hay thậm chí crash ứng dụng. Trong bài viết này, chúng ta sẽ tìm hiểu về các best practices khi sử dụng async/await trong C#..

I. Lý thuyết căn bản

1. CPU-bound vs I/O-bound Operations

  • CPU-bound operations: Là các tác vụ nặng về tính toán CPU như xử lý logic (if/else), thực hiện các phép tính số học, xử lý chuỗi,...
  • I/O-bound operations: Là các tác vụ liên quan đến input/output như đọc/ghi file, gọi network (HTTP, RPC, Database). Khi thực hiện các tác vụ I/O, ứng dụng sẽ yêu cầu HĐH thực hiện và chờ kết quả trả về.

Lập trình bất đồng bộ chủ yếu tập trung vào việc tối ưu các tác vụ I/O-bound, giúp tận dụng tốt hơn các thread trong ứng dụng.

2. ThreadPool

ThreadPool là một tập hợp các thread được CLR (Common Language Runtime) tạo sẵn khi khởi chạy ứng dụng. Các thread trong ThreadPool được gọi là Managed Thread và được quản lý bởi CLR. Việc sử dụng ThreadPool giúp:

  • Tối ưu cho các tác vụ I/O hoặc các tác vụ CPU ngắn (< 1s)
  • Hạn chế việc tạo và hủy thread thường xuyên, giúp tăng hiệu năng
  • Tái sử dụng thread hiệu quả

3. Task vs Thread

Nhiều người thường nhầm lẫn giữa Task và Thread. Task đại diện cho một tác vụ bất đồng bộ và có thể được xử lý trên một hoặc nhiều thread. Task tương tự như concept Promise trong JavaScript - nó đại diện cho một công việc sẽ được hoàn thành trong tương lai.

II. Best Practices

1. Không sử dụng async void

Async void chỉ nên được sử dụng cho event handlers. Trong ASP.NET, async void có thể gây crash ứng dụng vì exception trong method async void sẽ không được xử lý, kể cả khi đã try-catch.

// BAD
public async void HandleEvent()
{
    await Task.Delay(1000);
    throw new Exception("Error"); // Exception sẽ không được bắt
}

// GOOD
public async Task HandleEvent()
{
    await Task.Delay(1000);
    throw new Exception("Error"); // Exception có thể được bắt
}

2. Sử dụng async mọi nơi có thể

Đảm bảo sử dụng async/await với mọi tác vụ I/O và trong toàn bộ call stack.

// BAD
public async Task<string> GetDataAsync()
{
    var result = GetDataFromDbSync(); // Blocking call
    return result;
}

// GOOD
public async Task<string> GetDataAsync()
{
    var result = await GetDataFromDbAsync(); // Non-blocking call
    return result;
}

3. Sử dụng Task.FromResult thay vì Task.Run cho các tính toán đơn giản

// BAD
public Task<int> GetValueAsync()
{
    return Task.Run(() => 42);
}

// GOOD
public Task<int> GetValueAsync()
{
    return Task.FromResult(42);
}

4. Tránh blocking calls

Không sử dụng Task.Wait(), Task.Result, hay Task.GetAwaiter().GetResult(). Các lệnh này sẽ block thread và có thể gây deadlock.

// BAD
public void DoWork()
{
    var task = LongRunningTaskAsync();
    var result = task.Result; // Blocking call
}

// GOOD
public async Task DoWorkAsync()
{
    var result = await LongRunningTaskAsync();
}

5. Sử dụng CancellationToken

public async Task ProcessDataAsync(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        await Task.Delay(100, cancellationToken);
        // Process data
    }
}

6. Start task sớm và await khi cần

// BAD
public async Task<string> GetDataAsync()
{
    var data1 = await GetFirstDataAsync();
    var data2 = await GetSecondDataAsync();
    return $"{data1}-{data2}";
}

// GOOD
public async Task<string> GetDataAsync()
{
    var task1 = GetFirstDataAsync();  // Start first task
    var task2 = GetSecondDataAsync(); // Start second task
    var data1 = await task1;          // Await when needed
    var data2 = await task2;
    return $"{data1}-{data2}";
}

7. Sử dụng ConfigureAwait(false) khi viết SDK

public async Task<string> LibraryMethodAsync()
{
    await Task.Delay(100).ConfigureAwait(false);
    return "Result";
}

8. Luôn gọi FlushAsync() với Stream

// GOOD
await using (var writer = new StreamWriter("file.txt"))
{
    await writer.WriteAsync("data");
    await writer.FlushAsync();
}

III. Kết luận

Việc sử dụng async/await đúng cách sẽ giúp tăng hiệu năng và độ ổn định của ứng dụng. Tuy nhiên, cần phải hiểu rõ các concepts cơ bản và tuân thủ các best practices để tránh các vấn đề tiềm ẩn.

Hy vọng bài viết này sẽ giúp anh em hiểu rõ hơn về async/await trong C# và áp dụng hiệu quả trong dự án của mình. Nếu anh em quan tâm đến những best practice về lập trình có thể theo dõi mình tại:

Tham khảo

  1. Async/Await and the Generated StateMachine
  2. I/O Completion Ports
  3. .NET async/await in a single picture
  4. ConfigureAwait FAQ

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí