使用Task.Run是一种不好的做法吗?

时间:2016-02-22 15:55:19

标签: c# asynchronous async-await

通常,在类上实现异步方法时,我会这样写:

public Task<Guid> GetMyObjectIdAsync(string objectName)
{
    return Task.Run(() => GetMyObjectId(objectName));
}

private Guid GetMyObjectId(string objectName)
{
    using (var unitOfWork = _myUnitOfWorkFactory.CreateUnitOfWork())
    {
        var myObject = unitOfWork.MyObjects.Single(o => o.Name == objectName);
        return myObject.Id;
    }
}

这种模式允许我同步和异步地使用相同的逻辑,具体取决于具体情况(我的大多数工作都在旧的代码库中,而不是很多支持异步调用),因为我可以公开同步方法如果需要,公开并获得最大兼容性。

最近我读过几篇SO帖子,建议使用Task.Run()是一个坏主意,只应在某些情况下使用,但这些情况似乎不太清楚。

我上面描述的模式实际上是个坏主意吗?我是否会以这种方式失去异步调用的一些功能/预期目的?或者这是一个合法的实施?

3 个答案:

答案 0 :(得分:14)

您正在做的是将同步操作卸载到另一个线程。如果你的线程是“特殊的”,那就完全没问题了。 “特殊”线程的一个示例是UI线程。在这种情况下,您可能希望卸载它的工作以保持UI响应(另一个例子是某种监听器)。

在大多数情况下,你只是将工作从一个线程转移到另一个线程。这不会增加任何值,并且会增加不必要的开销。

所以:

  

我上面描述的模式实际上是个坏主意吗?

是的,确实如此。将同步工作卸载到Add to Favourites并假装它是异步的是一个坏主意。

  

我是否会以这种方式失去异步调用的某些功能/预期用途?

开始时,这个操作实际上并没有异步。如果您在远程计算机上执行此操作并且您可以从异步操作中获益,则操作本身需要真正异步,这意味着:

ThreadPool

您目前正在做的事情称为“异步过同步”,您可能不应该这样做。更多Should I expose asynchronous wrappers for synchronous methods?

答案 1 :(得分:10)

  

最近我读了几篇SO帖子,建议使用Task.Run()是一个坏主意,只能在某些情况下使用,但这些情况似乎不太清楚。

绝对简单的经验法则我告诉那些不熟悉异步的人是:

首先,了解目的。 异步是为了减轻高延迟操作的重要低效率。

你是否正在做低延迟的事情?然后不要以任何方式使它异步。做这个工作吧。它很快。使用工具来缓解低延迟任务的延迟只会使您的程序变得不必要地复杂。

您是否正在进行高延迟,因为它正在等待磁盘旋转或数据包显示?使这个异步,但不要把它放在另一个线程上。你不雇佣工人坐在你的邮箱旁等待信件到达;邮政系统已经异步运行给你了。您不需要雇用人员来使其更加异步。阅读&#34; There Is No Thread&#34;如果那不清楚。

高延迟工作是否等待CPU进行大量计算?就像计算需要超过10毫秒?然后将该任务卸载到线程上,以便可以将线程调度到空闲CPU。

答案 2 :(得分:3)

public Task<Guid> GetMyObjectIdAsync(string objectName)

当我看到这一点时,我希望使用这种方法有一些优势,而不仅仅是将它包装在Task.Run()中。

特别是,我希望它在遇到某些I / O时释放该线程,否则就有机会这样做。

现在考虑我是否有代码:

_resource = GetResourceForID(GetMyObjectIdAsync(SomeLongRunningWayToGetName()));

如果我有理由需要在任务中完成此操作,并且我处于Task.Run()确实有意义的情况下(我有理由将其卸载到另一个线程上) )这样做的最好方法是包装整个事情:

Task task = Task.Run(() => _resource = GetResourceForID(GetMyObjectIdAsync(SomeLongRunningWayToGetName())));

Task.Run() 可能对我来说是一个坏主意作为来电者,或者它可能是好事,因为我真的从它给我的东西中获益。

但是,如果我看到您的签名,我将认为使用您的代码执行此操作的最佳方法是将其转换为使用该方法的代码。

Task task = SomeLongRunningWayToGetName()
  .ContinueWith(t => GetMyObjectIdAsync(t.Result))
  .ContinueWith(t => _resource = GetResourceForIDAsync(t.Result));

(或类似地使用asyncawait)。

充其量,Task.Run()的分块效果较差。更糟糕的是,我await这只是为了从更好的异步性中获益,它在一个可以利用它的环境中提供,如果真的存在的话。 (例如,我可能在MVC操作中使用了这个,因为我认为额外的开销会在更好的线程池使用中得到回报)。

因此虽然Task.Run()有时很有用,但在这种情况下,它总是很糟糕。如果你不能给我提供比我自己更多的异步性,那就不要让我相信你。

如果确实调用异步I / O,则仅提供公共XXXAsync()方法。

如果您确实需要将异步方法存根到例如匹配共享基础或接口的签名,然后它会更好:

public Task<Guid> GetMyObjectIdAsync(string objectName)
{
  return Task.FromResult(GetMyObjectId(objectName);
}

这也很糟糕(调用者最好只是直接调用GetMyObjectId()),但至少如果代码await,那么当它在同一个线程上运行时就会出现问题。没有使用另一个线程来完成工作的开销,所以如果它与其他await混合在一起,则会减少负面影响。因此,如果您真的需要返回Task,但无法添加对您调用它有用的任何内容,那么它非常有用。

但如果你不真的需要提供它,那就不要。

(调用Run()的私有方法,因为您的每个呼叫网站都受益于它,并且您只是添加了便利而不是在多个地方调用Run(),但这应该是详细记录如下)。