“等待Task.Yield()”及其替代品
如果我需要推迟代码执行,直到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.Run
更喜欢Task.Factory.StartNew
和Task.Run
,因为它们都明确定义了延续代码的范围。
那么,在什么情况下await Task.Yield()
实际上是有用的?
考虑您希望异步任务返回值的情况。
现有的同步方法:
public int DoSomething() { return SomeMethodThatReturnsAnInt(); }
要进行异步,请添加async关键字并更改返回类型:
public async Task DoSomething()
要使用Task.Factory.StartNew(),请将方法的单行主体更改为:
// start new task var task = Task.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()
非常适合在async
方法的其他同步部分中“打孔”。
就个人而言,我发现它在我有一个自我取消async
方法(一个管理自己相应的CancellationTokenSource
并在每次后续调用中取消先前创建的实例)的情况下很有用,可以在极短的时间内多次调用(即通过相互依赖的UI元素的事件处理程序。 在这种情况下, Task.Yield()
交换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); } } }
这里的目标是允许在非等待的异步方法调用返回(当它遇到第一次await
)后立即在同一SynchronizationContext
上SynchronizationContext
执行的代码,以更改影响异步方法执行的状态。 这种限制很像Task.Delay
实现的(我在这里讨论的是非零延迟期),但没有实际的 ,可能明显的延迟,这在某些情况下可能是不受欢迎的。
Task.Yield()
实际上有用的一种情况是当你await
递归调用同步完成的Task
。 因为csharp的async
/ await
通过在可能的情况下同步运行continuation来“释放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 = null; task = getOwnTaskAsync(); var foundTask = await task; Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}"); async 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(); var task = getOwnTaskAsync(ownTaskSource.Task); ownTaskSource.SetResult(task); var foundTask = await task; Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}"); async Task getOwnTaskAsync( Task ownTaskTask) { // This might be clearer. return await ownTaskTask; } } }
输出:
2 == 2: True
Task.Yield
不是Task.Factory.StartNew
或Task.Run
的替代品。 他们完全不同。 await
Task.Yield
,允许当前线程上的其他代码执行而不阻塞该线程。 可以把它想象为等待Task.Delay
,除了Task.Yield
等待任务完成,而不是等待特定的时间。
注意:不要在UI线程上使用Task.Yield
,并假设UI始终保持响应。 情况并非总是如此。