簡體   English   中英

“await Task.Yield()”及其替代方案

[英]"await Task.Yield()" and its alternatives

如果我需要將代碼執行推遲到 UI 線程消息循環的未來迭代之后,我可以這樣做:

await Task.Factory.StartNew(
    () => {
        MessageBox.Show("Hello!");
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    TaskScheduler.FromCurrentSynchronizationContext());

這類似於await Task.Yield(); MessageBox.Show("Hello!"); await Task.Yield(); MessageBox.Show("Hello!"); ,此外,如果我願意,我可以選擇取消任務。

在使用默認同步上下文的情況下,我可以類似地使用await Task.Run在池線程上繼續。

事實上,我更喜歡Task.Factory.StartNewTask.Run而不是Task.Yield ,因為它們都明確定義了延續代碼的范圍。

那么,在什么情況下await Task.Yield()真的有用呢?

Task.Yield()非常適合在async方法的其他同步部分“打孔”。

就我個人而言,我發現它在我有一個可以在極短的時間內多次調用的自取消async方法(管理自己相應的CancellationTokenSource並在每次后續調用中取消先前創建的實例的方法)的情況下很有用(即通過相互依賴的 UI 元素的事件處理程序)。 在這種情況下,一旦CancellationTokenSource被換出,使用Task.Yield()后跟IsCancellationRequested檢查可以防止做潛在的昂貴工作,其結果最終將被丟棄。

這是一個示例,其中只有對SelfCancellingAsync的最后排隊調用才能執行昂貴的工作並運行到完成。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskYieldExample
{
    class Program
    {
        private static CancellationTokenSource CancellationTokenSource;

        static void Main(string[] args)
        {
            SelfCancellingAsync();
            SelfCancellingAsync();
            SelfCancellingAsync();

            Console.ReadLine();
        }

        private static async void SelfCancellingAsync()
        {
            Console.WriteLine("SelfCancellingAsync starting.");

            var cts = new CancellationTokenSource();
            var oldCts = Interlocked.Exchange(ref CancellationTokenSource, cts);

            if (oldCts != null)
            {
                oldCts.Cancel();
            }

            // Allow quick cancellation.
            await Task.Yield();

            if (cts.IsCancellationRequested)
            {
                return;
            }

            // Do the "meaty" work.
            Console.WriteLine("Performing intensive work.");

            var answer = await Task
                .Delay(TimeSpan.FromSeconds(1))
                .ContinueWith(_ => 42, TaskContinuationOptions.ExecuteSynchronously);

            if (cts.IsCancellationRequested)
            {
                return;
            }

            // Do something with the result.
            Console.WriteLine("SelfCancellingAsync completed. Answer: {0}.", answer);
        }
    }
}

這里的目標是允許在對 async 方法的非等待調用返回后立即在同一個SynchronizationContext上同步執行的代碼(當它遇到第一個await時)來更改影響 async 方法執行的狀態。 這與Task.Delay實現的節流非常相似(我在這里談論的是非零延遲期),但沒有實際的、潛在的明顯延遲,這在某些情況下可能不受歡迎。

考慮您希望異步任務返回值的情況。

現有同步方法:

public int DoSomething()
{
    return SomeMethodThatReturnsAnInt();
}

要進行異步,請添加 async 關鍵字並更改返回類型:

public async Task<int> DoSomething()

要使用 Task.Factory.StartNew(),請將方法的單行正文更改為:

// start new task
var task = Task<int>.Factory.StartNew(
    () => {
        return SomeMethodThatReturnsAnInt();
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    TaskScheduler.FromCurrentSynchronizationContext() );

// await task, return control to calling method
await task;

// return task result
return task.Result;

與如果您使用await Task.Yield()則添加一行

// this returns control to the calling method
await Task.Yield();

// otherwise synchronous method scheduled for async execution by the 
// TaskScheduler of the calling thread
return SomeMethodThatReturnsAnInt();

后者更加簡潔、易讀,並且實際上並沒有太多改變現有的方法。

Task.Yield()真正有用的一種情況是當您await遞歸調用同步完成的Task時。 因為 csharp 的async / await通過盡可能同步運行延續來“釋放 Zalgo” ,所以完全同步遞歸場景中的堆棧可以變得足夠大,以至於您的進程死亡。 我認為這也部分是由於Task間接導致無法支持尾調用。 await Task.Yield()安排由調度程序而不是內聯運行延續,從而避免堆棧增長並解決此問題。

此外, Task.Yield()可用於縮短方法的同步部分。 如果調用者需要在您的方法執行某些操作之前接收您的方法的Task ,您可以使用Task.Yield()強制提前返回Task ,否則會自然發生。 例如,在以下本地方法場景中, async方法能夠安全地獲取對其自身Task的引用(假設您在單並發SynchronizationContext上運行它,例如在 winforms 中或通過nito 的AsyncContext.Run() ):

using Nito.AsyncEx;
using System;
using System.Threading.Tasks;

class Program
{
    // Use a single-threaded SynchronizationContext similar to winforms/WPF
    static void Main(string[] args) => AsyncContext.Run(() => RunAsync());

    static async Task RunAsync()
    {
        Task<Task> task = null;
        task = getOwnTaskAsync();
        var foundTask = await task;
        Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}");

        async Task<Task> getOwnTaskAsync()
        {
            // Cause this method to return and let the 「task」 local be assigned.
            await Task.Yield();
            return task;
        }
    }
}

輸出:

3 == 3: True

很抱歉,我想不出任何現實生活中的場景,其中能夠強制縮短async方法的同步部分是做某事的最佳方式。 知道你可以像我剛剛展示的那樣做一個技巧有時會很有用,但它也往往更危險。 通常,您可以以更好、更易讀和更線程安全的方式傳遞數據。 例如,您可以使用TaskCompletionSource向本地方法傳遞對其自身Task的引用:

using System;
using System.Threading.Tasks;

class Program
{
    // Fully free-threaded! Works in more environments!
    static void Main(string[] args) => RunAsync().Wait();

    static async Task RunAsync()
    {
        var ownTaskSource = new TaskCompletionSource<Task>();
        var task = getOwnTaskAsync(ownTaskSource.Task);
        ownTaskSource.SetResult(task);
        var foundTask = await task;
        Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}");

        async Task<Task> getOwnTaskAsync(
            Task<Task> ownTaskTask)
        {
            // This might be clearer.
            return await ownTaskTask;
        }
    }
}

輸出:

2 == 2: True

Task.Yield不是Task.Factory.StartNewTask.Run的替代品。 他們完全不同。 當您await Task.Yield時,您允許當前線程上的其他代碼在不阻塞線程的情況下執行。 把它想象成等待Task.Delay ,除了Task.Yield等到任務完成,而不是等待一個特定的時間。

注意:不要在 UI 線程上使用Task.Yield並假設 UI 將始終保持響應。 並非總是如此。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM