任务如何知道它已完成?

3

Task不保留等待处理程序是出于性能原因,只有在代码需要时才懒惰地构建一个。

那么,Task如何知道它已经完成了呢?

有人会说,在实现中,实现者在TaskCompletionSource上设置结果,但这仅解释了现代实现和重写,例如System.IO.FileStream.Begin/EndReadTask

我遵循Task.IsComplete属性;几乎在每个实例中,内部位标志字段(m_stateFlags)由TrySetResult / TrySetException方法设置以指示任务的状态。

但这并不涵盖所有情况。

像这样的方法呢?

public async Task FooAsync()
{
    await Task.Run(() => { });
}
2个回答

4

那么,任务如何知道自己已经完成了呢?

正如我在博客中所描述的(概述更多细节),有两种类型的任务:委托任务(执行代码)和 Promise 任务(表示事件)。

当委托任务的委托完成时,它们会自行完成。

Promise 任务通过外部信号使用 TaskCompletionSource<T>(或等效于 BCL 内部的方法)来完成。


既然我们正在谈论内部机制:似乎还有另一种内部机制来完成任务。这似乎是一种针对延迟和取消包装等事物的优化。 - usr

0

我在回答自己的问题,因为我突然想起来我知道答案了。

当使用C#语言支持特性时

这是状态机。

如果异步方法的实现者使用了C#语言支持的特性,例如在方法声明中使用async关键字,在方法体内使用await关键字等待与任务本质相关的操作,则就实现的任务而言,状态机通过设置任务结果来发出任务完成信号。

例如他的实现如下:

// client code
public async void TopLevelMethod()
{
  await MyMethodAsync();
}

// library code -- his implementation
public async Task MyMethodAsync()
{
    await AnotherOperationAsync();
}

然后,MyMethodAsync 的完成将被委托给编译器生成的状态机。

当然,AnotherOperationAsync 完成的信号也将由编译器生成的状态机处理,但这不是重点。

回想一下,在 MoveNext 方法内部的状态指示任务完成状态,并且在调用继续回调的块内部,它还在 AsyncXXXMethodBuilder 上调用了 SetResult

不使用 C# 语言支持功能时

然而,如果异步方法的实现者没有使用 C# 语言特性,则实现者有责任通过在 TaskCompletionSource 对象上设置相关结果、异常或取消属性来发出任务完成的信号。

例如:

public Task MyMethodAsync()
{
  var tcs = new TaskCompletionSource<object>();
  try
  {
    AnotherOperation();
    tcs.SetResult();
  }
  catch(Exception ex)
  {
    tcs.SetException(ex);
  }

  return tcs.Task; 
}

如果实现者没有使用TPL支持或使用旧的.NET API异步调用另一个操作,那么实现者有责任通过其中一个Try/SetResult/Exception等方法显式地设置任务状态来信号任务完成。

例如:

public Task MyMethodAsync()
{
  var tcs = new TaskCompletionSource...
  var autoReseEvent = ...
  ThreadPool.QueueUserWorkItem(new WaitCallback(() => 
                                   { 
                                     /* Work */
                                     Thread.SpinWait(1000);
                                     tcs.SetResult(...);
                                     autoResetEvent.Set();
                                    };)...;

  return tcs.Task;
}

一个不明智的案例

等待任务的最佳方式当然是使用await关键字。然而,如果在实现异步API时,实施者这样做:

public Task MyMethodAsync()
{
  return Task.Run(...);
}

这会让他的API使用者感到不满意,我想是吧?

Task.Run 只应在您不关心任务何时完成的情况下,用于fire and forget场景。

唯一的例外是,如果您使用await关键字等待调用Task.Run返回的任务,就像下面显示的代码片段一样,那么您将使用第一部分中描述的语言支持。

public async Task MyMethodAsync()
{
  await Task.Run(...);
}

我并不介意被踩票,但是不愿意给出踩票的解释让我更加困惑。如果我的理解有误,知道原因只会对我有所帮助,我也乐于接受批评。如果是其他原因,比如堆栈溢出政策等,那么知道这一点也会让我知道我的问题思路没有错。不附带踩票原因并不是很有帮助。 - Water Cooler v2
@acelent 我不理解你使用的一些短语。例如,很多时候,人们会将阻塞 I/O 绑定调用称为异步甚至无线程任务,但有时,一些文章会用术语同步工作来指代相同的工作。当你说:“调用者不希望工作被卸载,特别是同步工作”时,你是指如果执行的工作是阻塞 I/O 绑定操作,则将其卸载到线程池线程中不会有任何好处吗?如果这就是你的意思,我同意。续... - Water Cooler v2
@acelent 在我的示例中,我没有指定每个任务执行的工作性质。对于最后两个片段,我假设工作将是 CPU 绑定型工作。 - Water Cooler v2
@acelent 我同意你的评论,即在最后一个片段中没有必要使用 asyncawait,因为这可能会产生新的堆分配,并且如果调用者有同步上下文并且我卸载到线程池的工作是 CPU 绑定的,则会切换同步上下文。 - Water Cooler v2
我没有太多要补充的,但我会澄清一下。在“同步工作”中,我指的是任何形式的CPU密集型处理、阻塞I/O和阻塞等待。你能提供那些文章的链接吗?如果你需要在图形应用程序中后台执行CPU密集型工作,那么Task.Run()可能是必要的,并且在你可以使用的最高级别上使用它是可以的。如果你的库执行阻塞I/O或阻塞等待而没有异步替代方案,那么同样适用。但仅限于此,例如库不应该使用Task.Run(),或者如果必须使用,应该明确记录。 - acelent
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接