Task.ContinueWith混淆

6

我正在使用ASP.NET 4.5尝试使用新的异步/等待工具。我有一个实现IDataReader接口的类,它封装了一个供应商特定的读取器(例如SqlDataReader)。我有一个简单的ExecuteSql()方法,它像以下同步操作:

public IDataReader ReaderForSql(string sql)
{
    var cmd = NewCommand(sql, CommandType.Text);
    return DBReader.ReaderFactory(cmd.ExecuteReader());
}

我想要的是这个的异步版本。这是我的第一次尝试:
public Task<IDataReader> ReaderForSqlAsync(string sql, CancellationToken ct)
{
    var cmd = NewCommand(sql, CommandType.Text);
    return cmd.ExecuteReaderAsync(ct)
              .ContinueWith(t => DBReader.ReaderFactory(t.Result));
}

我使用它:

using (var r = await connection.ReaderForSqlAsync("SELECT ...", cancellationToken))
{
    ...
}

到目前为止,在我的有限测试中表现得非常好。但是在观看了这个Cloud9视频几次之后: http://channel9.msdn.com/Events/aspConf/aspConf/Async-in-ASP-NET,我开始担心他们提到的警告:

  • ContinueWith将消耗额外的线程池资源 - Readerfactory非常轻量级!
  • Task.Result会造成阻塞

而且由于我正在向ExecuteReaderAsync()传递一个ContinuationToken,所以似乎取消操作只是ExecuteReaderAsync()可能失败的另一个原因(毕竟它是SQL!)

当我尝试继续对其进行操作时,任务的状态将是什么?t.Result会阻塞吗?抛出异常?做错误的事情吗?


1
+1 给这个很棒的标题。"Task.ContinueWith 混淆"。(同时你写了一篇非常好的帖子) - Kevin Johnson
2个回答

5

ContinueWith默认使用当前任务计划程序(线程池线程),但您可以通过传递TaskContinuationOptions.ExecuteSynchronously和显式的TaskScheduler来更改它。

话虽如此,我建议首先尝试以下方法:

public async Task<IDataReader> ReaderForSqlAsync(string sql, CancellationToken ct)
{
  var cmd = NewCommand(sql, CommandType.Text);
  var readerResult = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
  return DBReader.ReaderFactory(readerResult);
}

asyncawait可以为您处理所有ContinueWith的细节和边缘情况,并以一致的方式呈现。如果性能测试表明这是一个严重问题,那么可能会有可能使这段代码更快。


你是否使用了.ConfigureAwait(false),因为ReaderForSqlAsync()的调用者已经在等待它了? - n8wrl
1
不是的。对于不需要上下文的方法,这是最佳实践,正如我在博客中所描述的 - Stephen Cleary
那篇博客文章正是我所需要的,Stephen - 非常感谢你! - n8wrl

2
Result会在任务未完成时阻塞,但在继续处理程序中已经完成了,所以不会阻塞。你做得很对。
当你在故障的任务上调用 Result (而你说这可能会发生) 时,异常会被重新抛出。这会导致你的继续处理程序也出现故障,并使最终从 ReaderForSqlAsync 返回的任务也出现故障。这是一件好事:整个任务链都出现了故障,所有异常都被观察到了(与被吞噬相反)。因此,这也是最佳实践。
对于计算密集型工作,始终使用线程是可以的。所以再次强调,你使用 ContinueWith 是正确的。毕竟,你必须在所有情况下计算出 IDataReader

谢谢!你对于使用.ContinueWith消耗线程池资源有什么想法? - n8wrl
无论是手动启动的线程还是线程池线程,都没有关系。当您执行任何代码时,您都在消耗一个线程。所以这没问题。异步旨在避免阻塞,而不是计算。因此,这是完全可以的,也没有更好的方法(在您的想法中可能有更好的方法吗?)。 - usr
Result 不会重新抛出异常,而是将其包装在 AggregateException 中。这就是我不建议使用 Result 的原因之一。 - Stephen Cleary
@StephenCleary 我猜你的意思是在有 await 的情况下不鼓励使用。我同意。但如果 OP 不使用 C# 5,那么绕过这个问题就变得复杂了。而且这是否必要也是一个问题。 - usr
@usr: 是的,我只会在有await可用时才不建议使用 Result。 :) - Stephen Cleary
显示剩余2条评论

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