Khi nào tôi sẽ sử dụng Task.Yield ()?


218

Tôi đang sử dụng async / await và Taskrất nhiều nhưng chưa bao giờ sử dụng Task.Yield()và thành thật ngay cả với tất cả các giải thích tôi không hiểu tại sao tôi lại cần phương pháp này.

Ai đó có thể đưa ra một ví dụ tốt nơi Yield()được yêu cầu?

Câu trả lời:


241

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.


26
Tôi cảm thấy như mình đang hiểu sai điều gì đó ở đây. Nếu 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êm asyncawait 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ào Task.Run()và tạo phương thức async giả.
Krumelur

14
@Krumelur Có một sự khác biệt lớn - hãy xem ví dụ của tôi. Nếu bạn sử dụng một Task.Runđể thực hiện nó, ExecuteFooOnUIThreadsẽ 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ạ.
Sậy Copsey

7
Một câu hỏi nữa: nếu 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?
Krumelur

7
@Krumelur Vâng, nó sẽ. Chỉ cần không ngay lập tức - nó sẽ xảy ra sau đó.
Sậy Copsey

33
Mặc dù câu trả lời này là đúng về mặt kỹ thuật, nhưng tuyên bố rằng "phần còn lại của mã sẽ thực thi sau đó" quá trừu tượng và có thể gây hiểu nhầm. Lịch trình thực thi của mã sau Task.Yield () phụ thuộc rất nhiều vào SyncisationContext cụ thể. Và tài liệu MSDN nêu rõ rằng "Bối cảnh đồng bộ hóa có trong luồng UI trong hầu hết các môi trường UI thường sẽ ưu tiên công việc được đăng lên ngữ cảnh cao hơn công việc nhập và kết xuất. Vì lý do này, đừng dựa vào việc chờ đợi Task.Yield () ; để giữ cho giao diện người dùng phản ứng nhanh. "
Vitaliy Tsvayer

36

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.Currentnull.

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.StartNewlịch trình tác vụ phù hợp.

Xem thêm:


Trong ví dụ của bạn, sự khác biệt giữa những gì ở đó và var dialogTask = await showAsync();?
Erik Philips

@ErikPhilips, 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.ShowDialogAPI đồ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ị.
chơi

5

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();
    }

4
Điều này có thể ngăn chặn tràn ngăn xếp, nhưng điều này cuối cùng sẽ hết bộ nhớ hệ thống nếu bạn để nó chạy đủ lâu. Mỗi lần lặp sẽ tạo ra một Nhiệm vụ mới không bao giờ hoàn thành, vì Nhiệm vụ bên ngoài đang chờ một Nhiệm vụ bên trong, đang chờ một Nhiệm vụ bên trong khác, v.v. Điều này không ổn. Ngoài ra, bạn có thể chỉ cần có một Nhiệm vụ ngoài cùng không bao giờ hoàn thành và chỉ cần lặp lại thay vì lặp lại. Nhiệm vụ sẽ không bao giờ hoàn thành, nhưng sẽ chỉ có một trong số họ. Trong vòng lặp, nó có thể mang lại hoặc chờ đợi bất cứ điều gì bạn muốn.
Triynko

Tôi không thể tái tạo tràn ngăn xếp. Có vẻ như 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)
Theodor Zoulias

-8

Task.Yield() có thể được sử dụng trong triển khai giả của các phương thức async.


4
Bạn nên cung cấp một số chi tiết.
PJProudhon

3
Với mục đích này, tôi muốn sử dụng Task.CompletedTask - xem phần Task.CompletedTask trong bài đăng trên blog msDN này để xem xét thêm.
Grzegorz Smulko

2
Vấn đề với việc sử dụng Task.CompletedTask hoặc Task.FromResult là bạn có thể bỏ lỡ các lỗi chỉ xuất hiện khi phương thức thực thi không đồng bộ.
Joakim MH
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.