Kai Sawano
Microsoft MVP for .NET. Interest-oriented developer. Well you might be confused as “Kai Sawano” isn’t in the MVP directory — I don’t use my real name online.
14 min read
Aug 13, 2026
Mục lục
Traditional async/await
Trong thời gian dài, async/await đã là nền tảng của lập trình bất đồng bộ trong .NET. Nó cho phép chúng ta viết mã bất đồng bộ trông giống như mã đồng bộ, làm cho một vấn đề phức tạp trở nên dễ quản lý hơn đáng kể.
Ở tầng thấp hơn, async/await được triển khai bằng biến đổi CPS (Continuation-Passing Style).
Đầu tiên, từ khóa async cho compiler biết rằng một phương thức là bất đồng bộ. Điều này đánh dấu điểm bắt đầu cho biến đổi CPS. Nói chính xác, mô hình async/await không yêu cầu từ khóa async. C# yêu cầu nó để compiler biết rằng await là một từ khóa đánh dấu một điểm có thể bị tạm dừng thay vì một định danh thông thường. C++ sử dụng mô hình async/await tương tự mà không cần async.
Từ khóa await cho compiler biết, “việc thực thi có thể dừng lại ở đây.” Compiler chia phương thức xung quanh mỗi await, sau đó chạy phần mã còn lại sau khi thao tác đã chờ hoàn tất.
Dưới đây là một ví dụ đơn giản:
public async Task<int> GetDataAsync()
{
// Stand in for an asynchronous operation by waiting for one second.
await Task.Delay(1000);
return 42;
}
Nếu Task.Delay(1000) chưa hoàn tất, await tạm dừng GetDataAsync. Sau một giây, việc thực thi tiếp tục và phương thức trả về 42. Nói một cách大致, compiler chia một phương thức bất đồng bộ tại mỗi await và sắp xếp để phần tiếp theo chạy khi thao tác hoàn tất.
class StateMachine
{
private int state = 0;
// Create a Task<int> to hold the result.
// It completes when the current asynchronous method completes.
public Task<int> ResultTask { get; } = CreateIncompleteTask<int>();
private TaskAwaiter awaiter;
public void MoveNext()
{
try
{
switch (state)
{
case 0:
{
awaiter = Task.Delay(1000).GetAwaiter();
if (!awaiter.IsCompleted)
{
// Record where execution should resume.
state = 1;
// Register the continuation.
// When Task.Delay completes, it invokes the registered continuation,
// causing the state machine to call MoveNext again.
// Where the continuation eventually runs depends on the awaiter,
// the current SynchronizationContext, the TaskScheduler, and so on.
awaiter.OnCompleted(MoveNext);
return;
}
goto case 1;
}
case 1:
{
state = -1;
// Confirm that the awaited operation completed successfully.
// If it failed, GetResult throws the exception here.
awaiter.GetResult();
// Complete the Task<int> with a result of 42.
CompleteTask(ResultTask, 42);
return;
}
}
}
catch (Exception ex)
{
// If MoveNext throws, complete the Task<int> in a faulted state.
// The caller then receives the exception from await GetDataAsync().
FailTask(ResultTask, ex);
}
}
}
Phương thức bất đồng bộ trở thành một state machine, với trạng thái tương ứng với mỗi await. Khi thao tác đã chờ hoàn tất, state machine chạy phần mã tiếp theo. Về mặt khái niệm, GetDataAsync được biên dịch thành dạng như sau:
public Task<int> GetDataAsync()
{
var stateMachine = new StateMachine();
stateMachine.MoveNext();
return stateMachine.ResultTask;
}
CreateIncompleteTask và CompleteTask chỉ là pseudocode được sử dụng để minh họa cơ chế. Mã được tạo bởi C# compiler không thao tác trực tiếp Task. Nó sử dụng AsyncTaskMethodBuilder<int> để tạo và hoàn tất Task<int> đại diện cho phương thức bất đồng bộ.
Giới hạn của async truyền thống
async/await cực kỳ tiện lợi. Nhưng sự tiện lợi đó không miễn phí.
Khi C# compiler biến đổi một phương thức bất đồng bộ, nó không thể biết liệu một lệnh gọi nhất định có thực sự bị tạm dừng hay không. Nhiều phương thức bất đồng bộ hoàn tất đồng bộ mà không bị tạm dừng chút nào:
public async Task<int> GetDataAsync()
{
return await GetValueAsync();
}
public async Task<int> GetValueAsync()
{
return 42;
}
C# compiler tạo state machine và Task<int> cho cả hai phương thức. Nó thực hiện biến đổi này từng phương thức một, vì vậy nó không thể nhìn thấy qua GetDataAsync vào triển khai của GetValueAsync.
Đến lúc này, bạn có thể tự hỏi một cách hợp lý: nếu C# compiler không thể tìm ra, JIT chắc chắn có thể giải quyết tại thời điểm chạy?
Thật không may, không thể. C# compiler đã viết lại phương thức bất đồng bộ thành một state machine được điều khiển bởi MoveNext, awaiter và method builder. Đến khi mã đến JIT, cấu trúc gọi A -> await B -> await C gốc đã bị tháo rời. JIT thấy một cuộc trò chuyện phức tạp giữa các state machine, đối tượng Task, awaiter và continuation. Các tối ưu hóa có thể vượt qua ranh giới phương thức giờ khó thực hiện hơn nhiều.
Nếu toàn bộ chuỗi gọi bất đồng bộ hoàn tất mà không bị tạm dừng, về nguyên tắc, nó có thể chạy như một chuỗi lệnh gọi đồng bộ thông thường. Nhưng C# compiler đã tháo rời luồng điều khiển gốc, và yêu cầu JIT tái tạo nó sau đó là một nhiệm vụ khó khăn.
Để mọi việc trở nên tồi tệ hơn, MoveNext chứa logic cho toàn bộ phương thức bất đồng bộ và có thể phát triển khá lớn. JIT thường từ chối nội dung hóa nó vì kích thước của nó, điều này ẩn giấu nhiều hơn nữa chuỗi gọi. Nội dung hóa nhiều lệnh gọi bất đồng bộ vào nhau cũng khó vì lý do tương tự.
Phân bổ là một vấn đề khác. Một phương thức bất đồng bộ trả về Task hoặc Task<T>, điều này thường có nghĩa là tạo một đối tượng Task mới trên mỗi lệnh gọi. Chi phí đó quan trọng trong mã nhạy cảm với hiệu suất. Đây là lý do tại sao ValueTask tồn tại: nó giảm phân bổ thông qua kết quả kiểu giá trị và tái sử dụng thông qua IValueTaskSource.
Nếu thao tác thực sự bị tạm dừng, không có gì đặc biệt đáng lo ngại. Ngay cả với khả năng hiển thị đầy đủ vào chuỗi gọi, JIT không thể tối ưu hóa việc tạm dừng đi. Runtime phải thực thi lại mã sau đó, và nó cần một cái gì đó như đối tượng Task để giữ kết quả và trạng thái hoàn tất.
Vấn đề thực sự là các phương thức bất đồng bộ hoàn tất đồng bộ thường xuyên hơn mong đợi. Điều này đặc biệt phổ biến trong các chuỗi gọi sâu và hệ thống phân tán được xây dựng xung quanh API bất đồng bộ:
- Chỉ phương thức trong cùng có thể thực sự bị tạm dừng, trong khi mọi phương thức ở trên chỉ truyền kết quả.
- Một phương thức bất đồng bộ có thể gọi một phương thức đồng bộ gọi một phương thức bất đồng bộ khác, để lại toàn bộ chuỗi về cơ bản là đồng bộ.
- Một hệ thống phân tán có thể trông hoàn toàn bất đồng bộ trong khi hầu hết tải thực tế vẫn là đồng bộ.
async/await truyền thống vẫn xây dựng state machine cho mọi phương thức bất đồng bộ mà nó gặp. Ngay cả các phương thức không bao giờ bị tạm dừng cũng nhận được hóa đơn đầy đủ.
Green Threads
Trước khi đến Runtime Async,值得 đi một vòng ngắn. Đội .NET trước đây đã thử nghiệm Green Threads, các luồng nhẹ tương tự goroutines và Java Virtual Threads. Mục tiêu là giảm chi phí chuyển đổi luồng.
Ý tưởng hấp dẫn, nhưng việc triển khai đã phát hiện ra một số vấn đề khó tránh.
Green Thread có thể nhẹ, nhưng đó vẫn là một ngữ cảnh thực thi đầy đủ. Ở mức tối thiểu, nó phải bảo tồn trạng thái register, ngăn gọi và metadata được sử dụng bởi runtime scheduler. Một goroutine, ví dụ, bắt đầu với khoảng 2 KB stack người dùng và mở rộng nó khi cần. Hai kilobyte đủ chỗ cho hàng trăm state machine bất đồng bộ, vì vậy “nhẹ” đang làm một công việc ở đây.
Green Threads cũng yêu cầu runtime xử lý lập lịch ở chế độ người dùng. Nói cách khác, runtime giữ tay lái. Các nhà phát triển có quyền kiểm soát hạn chế đối với hành vi lập lịch kết quả.
Các system call làm tình hình kém hấp dẫn hơn. Green Thread không phải là luồng OS, vì vậy luồng OS bên dưới nó vẫn phải thực hiện system call thực tế. Chuyển đổi, tạm dừng và tiếp tục giữa Green Thread và luồng OS làm tăng công việc, đôi khi làm cho một system call tốn kém gấp hàng chục lần. Trong thí nghiệm .NET, 100 triệu system call tăng từ khoảng 300 ms lên khoảng 1.800 ms. Đó là sự chậm lại hơn 5 lần.
Green Threads cũng tương tác kém với các tính năng bảo mật phần cứng. Intel CET Shadow Stack, ví dụ, duy trì ngăn bảo vệ địa chỉ trả về trong phần cứng và kiểm tra nó với ngăn gọi thông thường khi một hàm trả về. Vì Green Threads chuyển đổi ngăn gọi ở chế độ người dùng, runtime sẽ phải duy trì không chỉ con trỏ ngăn thông thường mà còn cả trạng thái Shadow Stack liên kết với luồng OS cơ sở. Tích hợp với bảo vệ luồng điều khiển phần cứng trở nên phức tạp hơn và thậm chí có thể yêu cầu hỗ trợ hệ điều hành chuyên dụng.
Luồng affinity cũng là một vấn đề đau đầu. Runtime lập lịch Green Threads, vì vậy một Green Thread được tiếp tục không được đảm bảo chạy trên cùng luồng OS. Tuy nhiên, nhiều API phụ thuộc vào một luồng nhất định, bao gồm GUI API, một phần của OS và mã được xây dựng xung quanh trạng thái local của luồng. Runtime sẽ phải cố định Green Thread vào luồng OS hoặc thực hiện lập lịch và chuyển đổi thêm bất cứ khi nào mã đó chạy. Vòng lặp thông báo GUI có thể gọi các API affinity luồng hàng chục nghìn lần mỗi giây. Với tần suất đó, việc lập lịch Green Thread có thể trở nên tốn kém hơn so với việc sử dụng đơn giản các luồng OS.
Kết quả trong ASP.NET Core là đòn cuối cùng. RPS (yêu cầu mỗi giây) không tăng; nó thực sự giảm. Chấp nhận tất cả các ràng buộc đó chỉ để cuối cùng chậm hơn async/await truyền thống không phải là một thỏa thuận hấp dẫn. Kể từ đó mọi người đã mất hứng thú với Green Threads, và đội .NET kết thúc thí nghiệm Green Thread và chuyển sang Runtime Async.
Runtime Async
Vậy giải pháp thay thế là gì? Câu trả lời đơn giản đáng ngạc nhiên: đừng để C# compiler xây dựng state machine. Bảo tồn luồng điều khiển bất đồng bộ gốc và giao nó trực tiếp cho JIT. Ý tưởng đó trở thành Runtime Async.
Runtime Async giới thiệu một quy ước gọi mới cho .NET runtime: Async Calling Convention. Theo nội bộ, các phương thức sử dụng nó được đánh dấu bằng MethodImplOptions.Async, nhưng người dùng không thể áp dụng trực tiếp đánh dấu đó. Mã C# chúng ta viết vẫn sử dụng cú pháp async/await quen thuộc:
async Task<int> A()
{
return await B();
}
Với async truyền thống, JIT thấy state machine MoveNext được phát ra bởi C# compiler. Với Runtime Async, nó thấy luồng điều khiển bất đồng bộ gốc của phương thức và tạo mã tuân theo Async Calling Convention đặc biệt. Ngoài các đối số thông thường, quy ước này truyền một đối tượng Continuation.
Giả sử một lệnh gọi phương thức thông thường trông như thế này:
result = B(args);
Runtime Async thêm một thành phần tham gia khác, continuation:
(result, continuation) = B(continuation, args);
continuation giữ trạng thái cần thiết để tiếp tục chuỗi gọi sau khi tạm dừng.
Lệnh gọi đầu tiên đến một phương thức bất đồng bộ truyền null làm Continuation. Chưa có trạng thái nào cần khôi phục, vì vậy phương thức bắt đầu ở đầu như một phương thức đồng bộ thông thường. Nếu nó đến cuối mà không bị tạm dừng, nó trả về kết quả thông thường cùng với Continuation null. Người gọi có thể sử dụng giá trị null đó để biết rằng phương thức đã hoàn tất đồng bộ.
Nếu phương thức đến một await mà thao tác chưa hoàn tất, chuỗi gọi bị tạm dừng. Runtime lưu trữ trạng thái cần thiết để tiếp tục trong một đối tượng Continuation và trả lại cho người gọi. Một Continuation khác null cho người gọi biết, “cái này đã bị tạm dừng.” Khi thao tác đã chờ hoàn tất, runtime sử dụng đối tượng đó để tiếp tục thực thi.
Tại thời điểm đó, runtime gọi lại phương thức Runtime Async và cung cấp Continuation đã lưu làm đối số bổ sung. Phương thức tiếp tục từ nơi nó rời đi và tiếp tục cho đến khi toàn bộ chuỗi gọi hoàn tất.
Đây là ý tưởng chính. Cả kết quả thông thường và Continuation bổ sung đều là một phần của quy ước gọi. Chúng có thể di chuyển trực tiếp qua register thay vì được bọc trong đối tượng trước. Runtime Async bảo tồn ABI hiện có cho giá trị trả về thông thường và sử dụng một kênh trả về khác cho Continuation. Khi kiến trúc mục tiêu cho phép, cả hai giá trị đều ở trong register.
Đối với một chuỗi gọi không bao giờ bị tạm dừng, dữ liệu di chuyển gần giống như trong mã đồng bộ. Đối số, giá trị trả về và Continuation đều di chuyển qua register. Không cần phân bổ một gói bọc kết quả tại mỗi lệnh gọi bất đồng bộ, vì vậy chi phí đó biến mất.
Nói cách khác, một phương thức có thể được khai báo là trả về Task<T>, nhưng không có đối tượng Task<T> nào xuất hiện nếu chuỗi gọi không bao giờ bị tạm dừng. Theo nội bộ, giá trị kiểu T được trả về trực tiếp.
Việc cung cấp cho JIT luồng điều khiển bất đồng bộ đầy đủ cũng quan trọng không kém. Nó có thể tối ưu hóa qua ranh giới phương thức và thậm chí có thể nội dung hóa các lệnh gọi bất đồng bộ vào nhau.
Xem mã được tạo
Hãy xem Runtime Async thực sự tạo ra cái gì. Ví dụ dưới đây tính số Fibonacci một cách đệ quy trong một phương thức bất đồng bộ:
class Program
{
async Task<int> Fib(int n)
{
if (n <= 1)
return n;
return await Fib(n - 1) + await Fib(n - 2);
}
}
Sau khi biên dịch assembly, chúng ta có thể dịch ngược IL của nó bằng ILSpy và có được kết quả sau:
internal class Program
{
[MethodImpl(MethodImplOptions.Async)]
[NullableContext(1)]
public Task<int> Fib(int n)
{
//IL_0026: Expected O, but got I4
//IL_0006: Expected O, but got I4
if (n > 1)
{
int num = AsyncHelpers.Await(Fib(n - 1));
int num2 = AsyncHelpers.Await(Fib(n - 2));
return (Task<int>)(num + num2);
}
return (Task<int>)n;
}
}
Ngoài logic gốc, không có state machine nào hiện diện.
Nhận các câu chuyện của Kai Sawano trong hộp thư của bạn
Tham gia Medium miễn phí để nhận cập nhật từ nhà văn này.
Đăng ký
Đăng ký
- Nhớ tôi để đăng nhập nhanh hơn
Khi chúng tôi chạy nó, JIT tạo ra mã tương tự như sau. Nó dài, nhưng không cần hoảng sợ. Chúng tôi sẽ chọn ra các phần quan trọng sau.
Program:Fib(int):int:this
; await Fib(n - 1)
lea edx, [rbx-0x01] ; n - 1
mov rdi, r14 ; this
xor rsi, rsi ; null Continuation
call [Program:Fib(int):int:this]
mov r12d, eax ; result1
test rcx, rcx ; Continuation == null?
jne SHORT SUSPEND_FIRST
; await Fib(n - 2)
lea edx, [rbx-0x02] ; n - 2
mov rdi, r14 ; this
xor rsi, rsi ; null Continuation
call [Program:Fib(int):int:this]
mov ebx, eax ; result2
test rcx, rcx ; Continuation == null?
jne SHORT SUSPEND_SECOND
; Both calls completed synchronously, so return the result directly.
add ebx, r12d
mov eax, ebx ; return value
xor ecx, ecx ; null Continuation
ret
SUSPEND_FIRST:
; Fib(n - 1) suspended, so create a Continuation to save the current state.
mov rdi, rcx
mov rsi, 0x... ; Continuation
call [CORINFO_HELP_ALLOC_CONTINUATION]
mov r12, rax
mov dword ptr [r12+0x48], ebx ; save n
; ... save any other required state ...
mov rcx, r12 ; return Continuation
ret
SUSPEND_SECOND:
; Fib(n - 2) suspended, so create a Continuation to save the current state.
mov rdi, rcx
mov rsi, 0x... ; Continuation type
call [CORINFO_HELP_ALLOC_CONTINUATION]
mov r15, rax
mov dword ptr [r15+0x4C], r12d ; save the result of Fib(n - 1)
; ... save any other required state ...
mov rcx, r15 ; return Continuation
ret
; --------------------------------------------
Program:Fib(int):Task<int>:this
mov rdi, rbx ; this
mov edx, r15d ; n
xor rsi, rsi ; null Continuation
call [Program:Fib(int):int:this] ; call the actual Runtime Async method
mov ebx, eax ; result
test rcx, rcx ; Continuation == null?
jne THUNK_SUSPENDED
; return Task.FromResult(ebx)
mov rax, <Task<int>>
ret
THUNK_SUSPENDED:
; var task = new RuntimeAsyncTask<int>();
; connect the continuation to the task;
; return task;
Điều đầu tiên cần lưu ý là phương thức nội bộ Program:Fib(int):int:this, sử dụng Async Calling Convention. Kiểu trả về của nó là int, không phải Task<int> gốc.
Trên x64, phương thức truyền con trỏ this, con trỏ Continuation và giá trị của n trong register:
mov r14, rdi ; this
mov r15, rsi ; Continuation
mov ebx, edx ; n
Trong lệnh gọi đầu tiên, chưa có trạng thái nào cần khôi phục, vì vậy phương thức nhận được Continuation null.
Xem xét lệnh gọi đệ quy đầu tiên:
await Fib(n - 1)
JIT biên dịch nó thành:
lea edx, [rbx-0x01] ; n - 1
mov rdi, r14 ; this
xor rsi, rsi ; Continuation = null
call [Program:Fib(int):int:this]
Lệnh gọi này đến Fib(n - 1) thực sự trả về hai giá trị:
eax = kết quả int của Fib
rcx = Continuation
Đây không phải là tuple (int, Continuation). Theo ABI x64, int và Continuation được trả về độc lập trong các register khác nhau.
Người gọi chỉ cần vài lệnh để thu thập kết quả và xác định liệu lệnh gọi có bị tạm dừng:
mov r12d, eax
test rcx, rcx ; Is Continuation null?
jne SUSPEND ; If not, the call suspended
Nếu rcx == null, lệnh gọi đã hoàn tất đồng bộ và eax chứa kết quả hợp lệ. Việc thực thi có thể tiếp tục ngay lập tức:
lea edx, [rbx-0x02]
mov rdi, r14
xor rsi, rsi
call [Program:Fib(int):int:this] ; second recursive call: Fib(n - 2)
Trong pseudocode giống C#, luồng trông như thế này:
var (result1, continuation1) = Fib(null, n - 1);
if (continuation1 != null)
Suspend(continuation1);
var (result2, continuation2) = Fib(null, n - 2);
// ...
Điều gì xảy ra khi Continuation không phải null? Mã được tạo cho thấy con đường đó rõ ràng ngay.
Ngay sau lệnh gọi đệ quy đầu tiên, chúng tôi tìm thấy kiểm tra này:
call [Program:Fib(int):int:this]
mov r12d, eax
test rcx, rcx
jne SHORT SUSPEND
Nếu rcx != null, Fib được gọi không hoàn tất đồng bộ. Lệnh gọi hiện tại của Fib cũng phải bị tạm dừng.
Chỉ lúc này JIT mới tạo Continuation cần thiết để bảo tồn trạng thái thực thi hiện tại. Nó không phân bổ trước chỉ vì tạm dừng có thể xảy ra:
mov rdi, rcx
mov rsi, 0x... ; Continuation type
call [CORINFO_HELP_ALLOC_CONTINUATION]
mov r12, rax
Sau đó nó lưu trạng thái local vẫn cần sau khi tiếp tục:
mov dword ptr [r12+0x48], ebx
Cuối cùng, nó đặt Continuation mới vào rcx và trả lại cho người gọi theo Async Calling Convention:
mov rcx, r12
ret
Khác với Green Thread, Continuation của Runtime Async nhỏ. Nó chỉ cần lưu trữ vài thông tin: biến local vẫn hoạt động qua await, điểm tiếp tục và kết quả hoặc ngoại lệ từ thao tác đã chờ. Thông thường chỉ cần vài chục byte. So với ngăn ban đầu khoảng 2 KB của Green Thread, nó mang theo rất ít.
Gộp tất cả lại, pseudocode giống C trông như thế này:
var (result1, continuation1) = Fib(null, n - 1);
if (continuation1 != null)
Suspend(continuation1);
var (result2, continuation2) = Fib(null, n - 2);
if (continuation2 != null)
Suspend(continuation2);
return result1 + result2;
Tuy nhiên, trong ví dụ Fibonacci này, mọi lệnh gọi đều hoàn tất đồng bộ. Đường dẫn thông thường chỉ là các lệnh gọi đệ quy, vì vậy hành vi của nó tương đương với mã đồng bộ sau:
var result1 = Fib(n - 1);
var result2 = Fib(n - 2);
return result1 + result2;
Toàn bộ chuỗi gọi không phân bổ đối tượng Task nào và không chịu chi phí state machine. Chi phí của cách abstraction async/await biến mất, để lại mã chạy như các lệnh gọi đồng bộ thông thường. Điều này khác cơ bản so với async truyền thống.
Đến lúc này, bạn có thể thắc mắc tại sao Program:Fib(int):System.Threading.Tasks.Task`1[int]:this vẫn tồn tại cùng với Program:Fib(int):int:this.
Runtime Async sử dụng Async Calling Convention mới ở tầng nội bộ, nhưng mã C# thông thường vẫn coi phương thức là Task<int> Fib(int). Một lớp bọc mỏng gọi là async thunk nối liền hai quy ước. Trong ví dụ này, nó chuyển đổi kết quả thông thường cùng với Continuation được trả về bởi Runtime Async thành Task<int> mà người gọi bên ngoài mong đợi.
Thunk chứa mã tương tự những gì chúng ta đã thấy:
xor rsi, rsi
call [Program:Fib(int):int:this]
mov ebx, eax
test rcx, rcx ; Is Continuation null?
Nó gọi trước phương thức Runtime Async thực, sau đó kiểm tra xem Continuation trả về có phải null không. Giá trị null có nghĩa là phương thức đã hoàn tất đồng bộ, vì vậy thunk có thể bọc kết quả trong Task<int> và trả về nó. Nếu thunk cuối cùng có thể được nội dung hóa và JIT có thể chứng minh Task không thoát, escape analysis có thể loại bỏ ngay cả phân bổ này.
Benchmark
Lý thuyết hữu ích, nhưng các con số thú vị hơn. Mã benchmark có sẵn trong GitHub Gist này.
Benchmark bao gồm các trường hợp sau:
Synchronous baselinegọi trực tiếp một phương thức đồng bộ thông thường.Async method, no suspension,Completed Task awaitvàCompleted ValueTask awaitbao gồm các phương thức bất đồng bộ hoàn tất mà không bị tạm dừng.Task.Yield suspension,ThreadPool continuationvàTaskCompletionSource continuationtạm dừng vì các lý do khác nhau.Async state-machine chainxây dựng một chuỗi gọi bất đồng bộ sâu mà phương thức trong cùng bị tạm dừng vớiTask.Yield.
Tôi đã so sánh Runtime Async (Async2) từ bản build .NET 11 daily mới nhất có sẵn khi bài viết này được viết so với async truyền thống (Async1) trên .NET 10. Sau khi warmup, mỗi benchmark chạy 100 triệu lần. Đó là một cách somewhat brute-force, nhưng nó làm cho các khác biệt khó bỏ sót.
Nhấn enter hoặc nhấp để xem hình ảnh đầy đủ

Nhấn enter hoặc nhấp để xem hình ảnh đầy đủ

Dưới đây là dữ liệu kết quả benchmark thô.
Kết quả đáng chú ý. Khi không có việc tạm dừng nào xảy ra, Runtime Async gần như loại bỏ hoàn toàn chi phí của async truyền thống. Nó chạy nhanh hơn gần 20 lần và nằm trong khoảng làm tròn của baseline đồng bộ.
Các lợi ích không giới hạn ở đường dẫn hoàn tất đồng bộ. Benchmark continuation ThreadPool và TaskCompletionSource cải thiện khoảng 3 lần đến 4 lần. Chuỗi state machine bất đồng bộ sâu hơn cải thiện 7.4 lần. Chuỗi càng sâu, Runtime Async càng có nhiều không gian để tỏa sáng.
Runtime Async cũng phân bổ ít bộ nhớ hơn trong mọi benchmark. Trong các kiểm tra không tạm dừng, phân bổ giảm xuống 0 byte mỗi thao tác và không có collection Gen0 nào xảy ra. Các con số xác nhận rằng chuỗi gọi không tạo đối tượng Task nào và không chịu chi phí state machine.
Suy nghĩ cuối cùng
Runtime Async là một mô hình thực thi bất đồng bộ mới sắp ra mắt trong .NET 11. Thay vì để C# compiler chuyển đổi eagerly mỗi phương thức async thành state machine, nó bảo tồn luồng điều khiển bất đồng bộ gốc cho đến runtime, nơi JIT có thể xử lý và tối ưu hóa nó trực tiếp.
Điều này cuối cùng cung cấp pay for play theo nghĩa đen. Nếu mã không bị tạm dừng, nó hầu như không phải trả chi phí thêm vì là bất đồng bộ. Nếu nó bị tạm dừng, nó chỉ trả cho trạng thái thực sự cần phải bảo tồn.
Mã async không dừng lại chạy như mã đồng bộ. Khi nó dừng lại, nó chỉ trả cho những gì nó sử dụng. Cuối cùng, đó là toàn bộ ý tưởng đằng sau Runtime Async.
Viết bởi Kai Sawano
Microsoft MVP for .NET. Interest-oriented developer. Well you might be confused as “Kai Sawano” isn’t in the MVP directory — I don’t use my real name online.
Follow



