Hướng dẫn Toàn diện về Lập trình Bất đồng bộ (Async) trong .NET – Phân tích từ davidfowl/AspNetCoreDiagnosticScenarios

Lập trình bất đồng bộ đã xuất hiện trên nền tảng .NET từ nhiều năm trước, nhưng trong quá khứ, việc thực hiện nó một cách hiệu quả là rất khó khăn. Kể từ khi async/await được giới thiệu trong C# 5, lập trình bất đồng bộ đã trở thành xu hướng chủ đạo. Các framework hiện đại (như ASP.NET Core) hoàn toàn bất đồng bộ và rất khó để tránh từ khóa async khi viết dịch vụ web. Kết quả là, có rất nhiều nhầm lẫn về các phương pháp hay nhất cho async và cách sử dụng nó đúng cách. Bài viết này sẽ đưa ra một số hướng dẫn kèm ví dụ về các mẫu tốt và xấu trong việc viết mã bất đồng bộ.

Tính lây lan của Bất đồng bộ (Asynchrony is viral)

Một khi bạn đã sử dụng async, tất cả các caller của bạn NÊN cũng sử dụng async, bởi vì nỗ lực sử dụng async sẽ trở nên vô nghĩa trừ khi toàn bộ call stack là async. Trong nhiều trường hợp, việc chỉ bất đồng bộ một phần còn có thể tệ hơn là đồng bộ hoàn toàn. Do đó, tốt nhất là bạn nên đi toàn bộ và làm mọi thứ async cùng một lúc.

❌ XẤU – Ví dụ này sử dụng Task.Result và kết quả là block thread hiện tại để chờ kết quả. Đây là một ví dụ về sync over async.

public int DoSomethingAsync()
{
    var result = CallDependencyAsync().Result;
    return result + 1;
}

✅ TỐT – Ví dụ này sử dụng từ khóa await để lấy kết quả từ CallDependencyAsync.

public async Task<int> DoSomethingAsync()
{
    var result = await CallDependencyAsync();
    return result + 1;
}

Async void

Việc sử dụng async void trong ứng dụng ASP.NET Core LUÔN LUÔN là xấu. Hãy tránh nó, không bao giờ làm điều đó. Thông thường, nó được sử dụng khi các nhà phát triển cố gắng triển khai các mẫu fire-and-forget được kích hoạt bởi một action của controller. Các phương thức async void sẽ làm crash process nếu có exception được ném ra.

❌ XẤU – Các phương thức async void không thể được theo dõi và do đó các exception không được xử lý có thể dẫn đến crash ứng dụng.

public class MyController : Controller
{
    [HttpPost("/start")]
    public IActionResult Post()
    {
        BackgroundOperationAsync();
        return Accepted();
    }
    
    public async void BackgroundOperationAsync()
    {
        var result = await CallDependencyAsync();
        DoSomething(result);
    }
}

✅ TỐT – Các phương thức trả về Task tốt hơn vì các exception không được xử lý sẽ kích hoạt sự kiện TaskScheduler.UnobservedTaskException.

public class MyController : Controller
{
    [HttpPost("/start")]
    public IActionResult Post()
    {
        Task.Run(BackgroundOperationAsync);
        return Accepted();
    }
    
    public async Task BackgroundOperationAsync()
    {
        var result = await CallDependencyAsync();
        DoSomething(result);
    }
}

Ưu tiên Task.FromResult hơn Task.Run cho dữ liệu đã tính toán sẵn

Đối với các kết quả đã được tính toán trước, không cần phải gọi Task.Run, nó sẽ xếp một work item vào thread pool và hoàn thành ngay lập tức với giá trị đã tính toán sẵn. Thay vào đó, hãy sử dụng Task.FromResult để tạo một task bao bọc dữ liệu đã được tính toán.

❌ XẤU – Ví dụ này lãng phí một thread-pool thread để trả về một giá trị được tính toán đơn giản.

public class MyLibrary
{
   public Task<int> AddAsync(int a, int b)
   {
       return Task.Run(() => a + b);
   }
}

✅ TỐT – Ví dụ này sử dụng Task.FromResult để trả về giá trị đã tính toán. Nó không sử dụng bất kỳ thread phụ nào.

public class MyLibrary
{
   public Task<int> AddAsync(int a, int b)
   {
       return Task.FromResult(a + b);
   }
}

💡 LƯU Ý: Sử dụng Task.FromResult sẽ dẫn đến việc cấp phát một đối tượng Task. Sử dụng ValueTask<T> có thể loại bỏ hoàn toàn việc cấp phát đó.

public class MyLibrary
{
   public ValueTask<int> AddAsync(int a, int b)
   {
       return new ValueTask<int>(a + b);
   }
}

Tránh sử dụng Task.Run cho công việc dài hạn làm block thread

Công việc dài hạn trong ngữ cảnh này đề cập đến một thread chạy trong suốt vòng đời của ứng dụng để làm công việc nền (như xử lý queue items, hoặc sleep và thức dậy để xử lý dữ liệu). Task.Run sẽ xếp một work item vào thread pool. Giả định là công việc đó sẽ kết thúc nhanh chóng. Lấy một thread-pool thread cho công việc dài hạn là xấu vì nó lấy đi thread đó khỏi các công việc khác. Thay vào đó, hãy tạo một thread mới thủ công để làm công việc blocking dài hạn.

❌ XẤU – Ví dụ này chiếm dụng một thread-pool thread vĩnh viễn để xử lý công việc trên BlockingCollection<T>.

public class QueueProcessor
{
    private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();
    
    public void StartProcessing()
    {
        Task.Run(ProcessQueue);
    }
    
    public void Enqueue(Message message)
    {
        _messageQueue.Add(message);
    }
    
    private void ProcessQueue()
    {
        foreach (var item in _messageQueue.GetConsumingEnumerable())
        {
             ProcessItem(item);
        }
    }
}

✅ TỐT – Ví dụ này sử dụng một thread riêng để xử lý message queue thay vì thread-pool thread.

public class QueueProcessor
{
    private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();
    
    public void StartProcessing()
    {
        var thread = new Thread(ProcessQueue) 
        {
            IsBackground = true
        };
        thread.Start();
    }
    
    public void Enqueue(Message message)
    {
        _messageQueue.Add(message);
    }
    
    private void ProcessQueue()
    {
        foreach (var item in _messageQueue.GetConsumingEnumerable())
        {
             ProcessItem(item);
        }
    }
}

Tránh sử dụng Task.Result và Task.Wait

Có rất ít cách để sử dụng Task.Result và Task.Wait một cách chính xác, vì vậy lời khuyên chung là hoàn toàn tránh sử dụng chúng trong mã của bạn.

⚠️ Sync over async

Sử dụng Task.Result hoặc Task.Wait để block chờ một hoạt động bất đồng bộ hoàn thành là TỆ HƠN NHIỀU so với việc gọi một API đồng bộ thực sự. Hiện tượng này được gọi là “Sync over async”. Đây là những gì xảy ra ở mức cao:

  • Một hoạt động bất đồng bộ được bắt đầu.
  • Thread gọi bị block chờ hoạt động đó hoàn thành.
  • Khi hoạt động bất đồng bộ hoàn thành, nó unblock mã đang chờ. Việc này diễn ra trên một thread khác.

Kết quả là chúng ta cần sử dụng 2 thread thay vì 1 để hoàn thành các hoạt động đồng bộ. Điều này thường dẫn đến thread-pool starvation và gây ra gián đoạn dịch vụ.

⚠️ Deadlocks

SynchronizationContext là một abstraction cho phép các mô hình ứng dụng kiểm soát nơi các continuation bất đồng bộ chạy. ASP.NET (non-core), WPF và Windows Forms đều có implementation có thể dẫn đến deadlock nếu Task.Wait hoặc Task.Result được sử dụng trên thread chính.

💡 LƯU Ý: ASP.NET Core không có SynchronizationContext và không dễ bị deadlock.

❌ XẤU – Dưới đây là tất cả các ví dụ cố gắng tránh deadlock nhưng vẫn mắc phải vấn đề “sync over async”.

public string DoOperationBlocking()
{
    return Task.Run(() => DoAsyncOperation()).Result;
}

public string DoOperationBlocking2()
{
    return Task.Run(() => DoAsyncOperation()).GetAwaiter().GetResult();
}

public string DoOperationBlocking3()
{
    return Task.Run(() => DoAsyncOperation().Result).Result;
}

public string DoOperationBlocking5()
{
    return DoAsyncOperation().Result;
}

public string DoOperationBlocking6()
{
    return DoAsyncOperation().GetAwaiter().GetResult();
}

Ưu tiên await hơn ContinueWith

Task đã tồn tại trước khi từ khóa async/await được giới thiệu và do đó cung cấp các cách để thực thi continuation mà không cần dựa vào ngôn ngữ. Mặc dù các phương thức này vẫn hợp lệ để sử dụng, chúng tôi khuyên bạn nên ưu tiên async/await hơn ContinueWith. ContinueWith cũng không capture SynchronizationContext và do đó khác biệt về mặt ngữ nghĩa so với async/await.

❌ XẤU – Sử dụng ContinueWith thay vì async.

public Task<int> DoSomethingAsync()
{
    return CallDependencyAsync().ContinueWith(task =>
    {
        return task.Result + 1;
    });
}

✅ TỐT – Sử dụng từ khóa await.

public async Task<int> DoSomethingAsync()
{
    var result = await CallDependencyAsync();
    return result + 1;
}

Luôn tạo TaskCompletionSource với RunContinuationsAsynchronously

TaskCompletionSource<T> là một building block quan trọng cho các thư viện cố gắng chuyển đổi những thứ không thể await thành có thể await thông qua Task. Theo mặc định, các continuation của Task sẽ chạy inline trên cùng thread gọi Try/Set(Result/Exception/Canceled). Điều này cực kỳ nguy hiểm và có thể dẫn đến deadlock, thread-pool starvation, hỏng trạng thái và nhiều vấn đề khác.

Luôn sử dụng TaskCreationOptions.RunContinuationsAsynchronously khi tạo TaskCompletionSource<T>.

❌ XẤU – Không sử dụng TaskCreationOptions.RunContinuationsAsynchronously.

public Task<int> DoSomethingAsync()
{
    var tcs = new TaskCompletionSource<int>();
    
    var operation = new LegacyAsyncOperation();
    operation.Completed += result =>
    {
        tcs.SetResult(result);
    };
    
    return tcs.Task;
}

✅ TỐT – Sử dụng TaskCreationOptions.RunContinuationsAsynchronously.

public Task<int> DoSomethingAsync()
{
    var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
    
    var operation = new LegacyAsyncOperation();
    operation.Completed += result =>
    {
        tcs.SetResult(result);
    };
    
    return tcs.Task;
}

Luôn Dispose CancellationTokenSource được dùng cho timeout

Các đối tượng CancellationTokenSource được sử dụng cho timeout (được tạo với timer hoặc sử dụng phương thức CancelAfter) có thể gây áp lực lên timer queue nếu không được dispose.

❌ XẤU – Không dispose CancellationTokenSource.

public async Task<Stream> HttpClientAsyncWithCancellationBad()
{
    var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

    using (var client = _httpClientFactory.CreateClient())
    {
        var response = await client.GetAsync("http://backend/api/1", cts.Token);
        return await response.Content.ReadAsStreamAsync();
    }
}

✅ TỐT – Dispose CancellationTokenSource đúng cách.

public async Task<Stream> HttpClientAsyncWithCancellationGood()
{
    using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
    {
        using (var client = _httpClientFactory.CreateClient())
        {
            var response = await client.GetAsync("http://backend/api/1", cts.Token);
            return await response.Content.ReadAsStreamAsync();
        }
    }
}

Luôn truyền CancellationToken cho các API chấp nhận CancellationToken

Cancellation là cooperative trong .NET. Mọi thứ trong chuỗi gọi phải được truyền CancellationToken một cách rõ ràng để nó hoạt động tốt.

❌ XẤU – Quên truyền CancellationToken vào Stream.ReadAsync.

public async Task<string> DoAsyncThing(CancellationToken cancellationToken = default)
{
   byte[] buffer = new byte[1024];
   int read = await _stream.ReadAsync(buffer, 0, buffer.Length);
   return Encoding.UTF8.GetString(buffer, 0, read);
}

✅ TỐT – Truyền CancellationToken vào Stream.ReadAsync.

public async Task<string> DoAsyncThing(CancellationToken cancellationToken = default)
{
   byte[] buffer = new byte[1024];
   int read = await _stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
   return Encoding.UTF8.GetString(buffer, 0, read);
}

Huỷ các hoạt động không thể huỷ (Cancelling uncancellable operations)

Một trong các mẫu mã khi lập trình bất đồng bộ là hủy một hoạt động không thể hủy. Điều này thường có nghĩa là tạo một task khác hoàn thành khi timeout hoặc CancellationToken được kích hoạt, sau đó sử dụng Task.WhenAny để phát hiện hoạt động đã hoàn thành hoặc bị hủy.

Sử dụng CancellationTokens

❌ XẤU – Sử dụng Task.Delay(-1, token) nhưng không có cách nào dispose CancellationTokenRegistration.

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    var delayTask = Task.Delay(-1, cancellationToken);
    var resultTask = await Task.WhenAny(task, delayTask);
    if (resultTask == delayTask)
    {
        throw new OperationCanceledException();
    }
    return await task;
}

✅ TỐT – Dispose CancellationTokenRegistration khi một trong các Task hoàn thành.

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);

    using (cancellationToken.Register(state =>
    {
        ((TaskCompletionSource<object>)state).TrySetResult(null);
    },
    tcs))
    {
        var resultTask = await Task.WhenAny(task, tcs.Task);
        if (resultTask == tcs.Task)
        {
            throw new OperationCanceledException(cancellationToken);
        }
        return await task;
    }
}

✅ TỐT – Ưu tiên sử dụng Task.WaitAsync trên .NET >= 6.

Sử dụng timeout

❌ XẤU – Không hủy timer ngay cả khi hoạt động hoàn thành thành công.

public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
{
    var delayTask = Task.Delay(timeout);
    var resultTask = await Task.WhenAny(task, delayTask);
    if (resultTask == delayTask)
    {
        throw new OperationCanceledException();
    }
    return await task;
}

✅ TỐT – Hủy timer nếu hoạt động hoàn thành thành công.

public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
{
    using (var cts = new CancellationTokenSource())
    {
        var delayTask = Task.Delay(timeout, cts.Token);
        var resultTask = await Task.WhenAny(task, delayTask);
        if (resultTask == delayTask)
        {
            throw new OperationCanceledException();
        }
        else
        {
            cts.Cancel();
        }
        return await task;
    }
}

Luôn gọi FlushAsync trên StreamWriter/Stream trước khi Dispose

Khi ghi vào Stream hoặc StreamWriter, ngay cả khi sử dụng overload bất đồng bộ để ghi, dữ liệu cơ bản có thể được lưu trong bộ đệm. Khi dữ liệu được lưu đệm, việc dispose Stream hoặc StreamWriter thông qua phương thức Dispose sẽ ghi/flush đồng bộ, dẫn đến blocking thread và có thể gây thread-pool starvation.

❌ XẤU – Block request bằng cách ghi đồng bộ vào HTTP-response body.

app.Run(async context =>
{
    using (var streamWriter = new StreamWriter(context.Response.Body))
    {
        await streamWriter.WriteAsync("Hello World");
    }
});

✅ TỐT – Sử dụng DisposeAsync (qua await using) để flush bất đồng bộ.

app.Run(async context =>
{
    await using (var streamWriter = new StreamWriter(context.Response.Body))
    {
        await streamWriter.WriteAsync("Hello World");
    }
});

✅ TỐT – Gọi FlushAsync trước khi dispose.

app.Run(async context =>
{
    using (var streamWriter = new StreamWriter(context.Response.Body))
    {
        await streamWriter.WriteAsync("Hello World");
        await streamWriter.FlushAsync();
    }
});

Ưu tiên async/await hơn việc trả về trực tiếp Task

Có những lợi ích khi sử dụng từ khóa async/await thay vì trả về trực tiếp Task:

  • Exception đồng bộ và bất đồng bộ được chuẩn hóa thành luôn bất đồng bộ.
  • Mã dễ sửa đổi hơn (ví dụ: thêm using).
  • Chẩn đoán các phương thức bất đồng bộ dễ dàng hơn.
  • Exceptions được ném ra sẽ tự động được bọc trong Task trả về.
  • Async locals sẽ không bị rò rỉ ra khỏi phương thức async.

❌ XẤU – Trả về trực tiếp Task.

public Task<int> DoSomethingAsync()
{
    return CallDependencyAsync();
}

✅ TỐT – Sử dụng async/await.

public async Task<int> DoSomethingAsync()
{
    return await CallDependencyAsync();
}

💡 LƯU Ý: Có những cân nhắc về hiệu suất khi sử dụng async state machine thay vì trả về trực tiếp Task. Trả về trực tiếp Task luôn nhanh hơn vì nó thực hiện ít công việc hơn, nhưng bạn sẽ thay đổi hành vi và có thể mất một số lợi ích của async state machine.

AsyncLocal<T>

Async locals là một cách để lưu trữ/truy xuất trạng thái môi trường xung quanh trong ứng dụng. Đây có thể là một giải pháp thay thế rất hữu ích cho việc truyền trạng thái rõ ràng ở khắp mọi nơi. Mặc dù mạnh mẽ, nhưng nó cũng nguy hiểm nếu sử dụng không đúng cách. Async locals được gắn vào execution context, nó chảy mọi nơi một cách ngầm định.

Tạo AsyncLocal<T>

Nếu bạn có thể tránh async locals, hãy làm như vậy bằng cách truyền trạng thái một cách rõ ràng hoặc sử dụng các kỹ thuật như inversion of control.

Nếu không thể tránh, hãy đảm bảo rằng bất cứ thứ gì đưa vào async local đều:

  1. Không phải là disposable
  2. Immutable/read-only/thread-safe

❌ XẤU – Đối tượng disposable được lưu trong async local.

using (var thing = new DisposableThing())
{
    DisposableThing.Current = thing;
    Dispatch();
    DisposableThing.Current = null;
}

❌ XẤU – Đối tượng không thread-safe được lưu trong async local.

AmbientValues.Current = new Dictionary<int, string>();

Parallel.For(0, 10, i =>
{
    AmbientValues.Current[i] = "processing";
    LogCurrentValues();
    AmbientValues.Current[i] = "done";
});

✅ TỐT – Sử dụng ConcurrentDictionary thay vì Dictionary thông thường.

class AmbientValues
{
    private static readonly AsyncLocal<ConcurrentDictionary<int, string>> _current = new();

    public static ConcurrentDictionary<int, string> Current
    {
        get => _current.Value!;
        set => _current.Value = value;
    }
}

Đừng làm rò rỉ AsyncLocal của bạn

Async locals chảy qua các await một cách tự động và có thể bị capture bởi bất kỳ API nào gọi ExecutionContext.Capture. Điều này có thể dẫn đến rò rỉ bộ nhớ trong một số tình huống nhất định.

Các API thường capture ExecutionContext:

  • Timer
  • CancellationToken.Register
  • new FileSystemWatcher
  • SocketAsyncEventArgs
  • Task.Run
  • ThreadPool.QueueUserWorkItem

❌ XẤU – Rò rỉ execution context gây áp lực bộ nhớ.

// Trong NumberCache.Add():
cts.Token.Register((_, _) => _cache.TryRemove(key, out _), null);

✅ TỐT – Sử dụng CancellationToken.UnsafeRegister để tránh capture execution context.

// Trong NumberCache.Add():
cts.Token.UnsafeRegister((_, _) => _cache.TryRemove(key, out _), null);

Tránh đặt giá trị AsyncLocal bên ngoài phương thức async

Các phương thức async có hành vi đặc biệt cho async locals để đảm bảo các giá trị không lan truyền ra bên ngoài phương thức async.

❌ XẤU – Đặt giá trị async local bên ngoài phương thức async.

var local = new AsyncLocal<int>();
MethodA();
Console.WriteLine(local.Value);  // In ra 2

void MethodA()
{
    local.Value = 1;
    MethodB();
    Console.WriteLine(local.Value);  // In ra 2
}

void MethodB()
{
    local.Value = 2;
    Console.WriteLine(local.Value);  // In ra 2
}

✅ TỐT – Đặt async locals trong phương thức async.

var local = new AsyncLocal<int>();
await MethodA();
Console.WriteLine(local.Value);  // In ra 0

async Task MethodA()
{
    local.Value = 1;
    await MethodB();
    Console.WriteLine(local.Value);  // In ra 1
}

async Task MethodB()
{
    local.Value = 2;
    Console.WriteLine(local.Value);  // In ra 2
}

Các Kịch bản (Scenarios)

Timer callbacks

❌ XẤU – Sử dụng async void trong Timer callback, có thể crash process nếu có exception.

public class Pinger
{
    private readonly Timer _timer;
    private readonly HttpClient _client;
    
    public Pinger(HttpClient client)
    {
        _client = client;
        _timer = new Timer(Heartbeat, null, 1000, 1000);
    }

    public async void Heartbeat(object state)
    {
        await _client.GetAsync("http://mybackend/api/ping");
    }
}

❌ XẤU – Block Timer callback (sync over async).

public void Heartbeat(object state)
{
    _client.GetAsync("http://mybackend/api/ping").GetAwaiter().GetResult();
}

✅ TỐT – Sử dụng async Task và discard Task trong Timer callback.

public void Heartbeat(object state)
{
    _ = DoAsyncPing();
}

private async Task DoAsyncPing()
{
    await _client.GetAsync("http://mybackend/api/ping");
}

✅ TỐT – Sử dụng PeriodicTimer (.NET 6+).

public class Pinger : IDisposable
{
    private readonly PeriodicTimer _timer;
    private readonly HttpClient _client;

    public Pinger(HttpClient client)
    {
        _client = client;
        _timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
        _ = Task.Run(DoAsyncPings);
    }

    public void Dispose() => _timer.Dispose();

    private async Task DoAsyncPings()
    {
        while (await _timer.WaitForNextTickAsync())
        {
            await _client.GetAsync("http://mybackend/api/ping");
        }
    }
}

Implicit async void delegates

❌ XẤU – API chỉ chấp nhận Action (callback đồng bộ).

public class BackgroundQueue
{
    public static void FireAndForget(Action action) { }
}

❌ XẤU – Caller tạo ra delegate async void ngầm định.

BackgroundQueue.FireAndForget(async () =>
{
    await httpClient.GetAsync("http://pinger/api/1");
});

✅ TỐT – Cung cấp cả overload đồng bộ và bất đồng bộ.

public class BackgroundQueue
{
    public static void FireAndForget(Action action) { }
    public static void FireAndForget(Func<Task> action) { }
}

ConcurrentDictionary.GetOrAdd

❌ XẤU – Block thread bằng Task.Result trong callback của GetOrAdd.

var person = _cache.GetOrAdd(id, (key) => _db.People.FindAsync(key).Result);

✅ TỐT – Lưu trữ Task<Person> thay vì Person.

private static ConcurrentDictionary<int, Task<Person>> _cache = new();

var person = await _cache.GetOrAdd(id, (key) => _db.People.FindAsync(key));

✅ TỐT – Sử dụng mẫu AsyncLazy để tránh thực thi delegate nhiều lần.

private static ConcurrentDictionary<int, AsyncLazy<Person>> _cache = new();

var person = await _cache.GetOrAdd(id, (key) => 
    new AsyncLazy<Person>(() => _db.People.FindAsync(key))).Value;

private class AsyncLazy<T> : Lazy<Task<T>>
{
    public AsyncLazy(Func<Task<T>> valueFactory) : base(valueFactory) { }
}

Constructors

Constructor là đồng bộ. Nếu bạn cần khởi tạo logic có thể bất đồng bộ, có một số mẫu để xử lý.

❌ XẤU – Sử dụng Task.Result trong constructor.

public Service(IRemoteConnectionFactory connectionFactory)
{
    _connection = connectionFactory.ConnectAsync().Result;
}

✅ TỐT – Sử dụng mẫu static factory cho phép khởi tạo bất đồng bộ.

public class Service : IService
{
    private readonly IRemoteConnection _connection;

    private Service(IRemoteConnection connection)
    {
        _connection = connection;
    }

    public static async Task<Service> CreateAsync(IRemoteConnectionFactory connectionFactory)
    {
        return new Service(await connectionFactory.ConnectAsync());
    }
}

WindowsIdentity.RunImpersonated

❌ XẤU – Chờ kết quả bên ngoài phạm vi RunImpersonated.

Task<IEnumerable<Product>> products = null;
WindowsIdentity.RunImpersonated(
    safeAccessTokenHandle,
    context => { products = _db.QueryAsync("SELECT Name from Products"); });
return await products;

❌ XẤU – Sử dụng Task.Result (sync over async).

return WindowsIdentity.RunImpersonated(
    safeAccessTokenHandle,
    context => _db.QueryAsync("SELECT Name from Products").Result);

✅ TỐT – Await kết quả của RunImpersonated (khuyến nghị cho .NET < 5.0).

return await WindowsIdentity.RunImpersonated(
    safeAccessTokenHandle, 
    context => _db.QueryAsync("SELECT Name from Products"));

✅ TỐT – Sử dụng RunImpersonatedAsync (.NET 5.0+).

return await WindowsIdentity.RunImpersonatedAsync(
    safeAccessTokenHandle, 
    context => _db.QueryAsync("SELECT Name from Products"));

Kết luận

Lập trình bất đồng bộ trong .NET là một kỹ năng thiết yếu khi xây dựng các ứng dụng hiện đại, đặc biệt là với ASP.NET Core. Bằng cách tuân theo các hướng dẫn trên – tránh async void, không block trên các hoạt động bất đồng bộ, luôn dispose đúng cách các tài nguyên, và hiểu rõ về cách hoạt động của AsyncLocal và ExecutionContext – bạn sẽ viết được mã bất đồng bộ an toàn, hiệu quả và dễ bảo trì hơn.

Bài viết này được dịch và biên tập từ repository AspNetCoreDiagnosticScenarios của davidfowl.