[英]"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.StartNew
和Task.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.StartNew
或Task.Run
的替代品。 他们完全不同。 当您await
Task.Yield
时,您允许当前线程上的其他代码在不阻塞线程的情况下执行。 把它想象成等待Task.Delay
,除了Task.Yield
等到任务完成,而不是等待一个特定的时间。
注意:不要在 UI 线程上使用Task.Yield
并假设 UI 将始终保持响应。 并非总是如此。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.