Câu trả lời:
Khi bạn sử dụng async
/ await
, không có gì đảm bảo rằng phương thức bạn gọi khi bạn await FooAsync()
thực sự sẽ chạy không đồng bộ. Việc thực hiện nội bộ là miễn phí để trở lại bằng cách sử dụng một đường dẫn hoàn toàn đồng bộ.
Nếu bạn đang tạo một API trong đó điều quan trọng là bạn không chặn và bạn chạy một số mã không đồng bộ và có khả năng phương thức được gọi sẽ chạy đồng bộ (chặn hiệu quả), sử dụng await Task.Yield()
sẽ buộc phương thức của bạn không đồng bộ và trả về kiểm soát tại thời điểm đó. Phần còn lại của mã sẽ thực thi sau đó (tại thời điểm đó, nó vẫn có thể chạy đồng bộ) trên bối cảnh hiện tại.
Điều này cũng có thể hữu ích nếu bạn thực hiện một phương thức không đồng bộ yêu cầu một số khởi tạo "chạy dài", nghĩa là:
private async void button_Click(object sender, EventArgs e)
{
await Task.Yield(); // Make us async right away
var data = ExecuteFooOnUIThread(); // This will run on the UI thread at some point later
await UseDataAsync(data);
}
Nếu không có Task.Yield()
cuộc gọi, phương thức sẽ thực hiện đồng bộ cho đến cuộc gọi đầu tiên await
.
Task.Run
để thực hiện nó, ExecuteFooOnUIThread
sẽ chạy trên nhóm luồng, không phải luồng UI. Với await Task.Yield()
, bạn buộc nó không đồng bộ theo cách mà mã tiếp theo vẫn chạy trên bối cảnh hiện tại (chỉ tại một thời điểm muộn hơn). Đó không phải là điều bạn thường làm, nhưng thật tuyệt khi có tùy chọn nếu cần vì một lý do lạ.
ExecuteFooOnUIThread()
chạy rất lâu, nó vẫn sẽ chặn luồng UI trong một thời gian dài và khiến UI không phản hồi, điều đó có đúng không?
Trong nội bộ, await Task.Yield()
chỉ cần xếp hàng tiếp tục trên bối cảnh đồng bộ hóa hiện tại hoặc trên một chuỗi nhóm ngẫu nhiên, nếu SynchronizationContext.Current
có null
.
Nó được thực hiện hiệu quả như người phục vụ tùy chỉnh. Một mã kém hiệu quả hơn tạo ra hiệu ứng giống hệt nhau có thể đơn giản như sau:
var tcs = new TaskCompletionSource<bool>();
var sc = SynchronizationContext.Current;
if (sc != null)
sc.Post(_ => tcs.SetResult(true), null);
else
ThreadPool.QueueUserWorkItem(_ => tcs.SetResult(true));
await tcs.Task;
Task.Yield()
có thể được sử dụng như một cách rút gọn cho một số thay đổi dòng thực thi kỳ lạ. Ví dụ:
async Task DoDialogAsync()
{
var dialog = new Form();
Func<Task> showAsync = async () =>
{
await Task.Yield();
dialog.ShowDialog();
}
var dialogTask = showAsync();
await Task.Yield();
// now we're on the dialog's nested message loop started by dialog.ShowDialog
MessageBox.Show("The dialog is visible, click OK to close");
dialog.Close();
await dialogTask;
// we're back to the main message loop
}
Điều đó nói rằng, tôi không thể nghĩ về bất kỳ trường hợp nào Task.Yield()
không thể thay thế bằng Task.Factory.StartNew
lịch trình tác vụ phù hợp.
Xem thêm:
var dialogTask = await showAsync();
?
var dialogTask = await showAsync()
sẽ không biên dịch vì await showAsync()
biểu thức không trả về một Task
(không giống như không có await
). Điều đó nói rằng, nếu bạn làm như vậy await showAsync()
, việc thực thi sau nó sẽ được tiếp tục chỉ sau khi hộp thoại đã được đóng, đó là cách nó khác nhau. Đó là bởi vì window.ShowDialog
API đồng bộ (mặc dù nó vẫn truyền thông điệp). Trong mã đó, tôi muốn tiếp tục trong khi hộp thoại vẫn được hiển thị.
Một cách sử dụng Task.Yield()
là để ngăn chặn tràn ngăn xếp khi thực hiện đệ quy async. Task.Yield()
ngăn chặn sự tiếp tục đồng bộ. Tuy nhiên, lưu ý rằng điều này có thể gây ra ngoại lệ OutOfMemory (như được lưu ý bởi Triynko). Đệ quy vô tận vẫn không an toàn và có lẽ bạn nên viết lại đệ quy như một vòng lặp.
private static void Main()
{
RecursiveMethod().Wait();
}
private static async Task RecursiveMethod()
{
await Task.Delay(1);
//await Task.Yield(); // Uncomment this line to prevent stackoverlfow.
await RecursiveMethod();
}
await Task.Delay(1)
là đủ để ngăn chặn nó. (Ứng dụng bảng điều khiển, .NET Core 3.1, C # 8)
Task.Yield()
có thể được sử dụng trong triển khai giả của các phương thức async.
await Task.Yield()
buộc phương thức không đồng bộ, tại sao chúng ta lại bận tâm viết mã async "thực"? Hãy tưởng tượng một phương pháp đồng bộ nặng. Để làm cho nó không đồng bộ, chỉ cần thêmasync
vàawait Task.Yield()
vào đầu và kỳ diệu, nó sẽ không đồng bộ? Điều đó khá giống với việc gói tất cả mã đồng bộ vàoTask.Run()
và tạo phương thức async giả.