在异步方法中,使用Task.Run或TaskFactory.StartNew总是不合适的吗?

3

我听说在多线程方面的责任应该落在应用程序上,而在异步方法中不应使用Task.Run或者TaskFactory.StartNew。

然而,如果我有一个库提供了做相当重的计算的方法,那么为了释放一些正在接受asp .net core http请求的线程,我能否将这个方法变成异步并使其运行一个长时间运行的task?或者这应该是一个同步方法,而asp .net core应用程序应该负责启动任务呢?

2个回答

4
首先,让我们考虑为什么我们需要异步?
异步通常是为了可扩展性或卸载而需要的。
在可扩展性方面,暴露异步调用的版本没有任何作用。因为您通常仍会消耗与同步调用相同的资源,甚至更多。但是,通过减少使用的资源量来实现可扩展性。而使用 Task.Run() 并没有降低资源使用量。
在卸载方面,可以公开同步方法的异步包装器。这对于响应性非常有用,因为它允许您将长时间运行的操作卸载到不同的线程中。以这种方式,您可以从方法的异步包装器中获得一些好处。
结果:
使用简单的异步外观包装同步方法不会产生任何可扩展性的好处,但会带来卸载的好处。但在这种情况下,仅公开同步方法会获得一些不错的好处。例如:
- 减少库的表面积。 - 您的用户将知道是否实际上有使用公开的异步API的可扩展性好处。 - 如果同时公开同步方法和围绕它的异步包装器,则开发人员将面临思考他们应该出于可扩展性(?)原因调用异步版本,但实际上将通过支付额外的卸载开销而没有可扩展性好处来损害吞吐量。
源自 Stepen Toub 的 Should I expose asynchronous wrappers for synchronous methods?。我强烈推荐您阅读它。
更新:
评论中的问题:
这篇文章很好地解释了可扩展性,并提供了一个例子。让我们考虑Thread.Sleep。有两种可能的实现异步版本的方法:
public Task SleepAsync(int millisecondsTimeout)
{
    return Task.Run(() => Sleep(millisecondsTimeout));
}

还有另一种新的实现:

public Task SleepAsync(int millisecondsTimeout)
{
    TaskCompletionSource<bool> tcs = null;
    var t = new Timer(delegate { tcs.TrySetResult(true); }, null, –1, -1);
    tcs = new TaskCompletionSource<bool>(t);
    t.Change(millisecondsTimeout, -1);
    return tcs.Task;
}

这两种实现提供了相同的基本行为,都会在超时后完成返回的任务。然而,从可伸缩性的角度来看,后者更具可扩展性。前一种实现在等待时间内消耗线程池中的线程,而后者仅依赖于高效的计时器,在持续时间到期时向任务发出信号。
因此,在您的情况下,仅使用 Task.Run 包装调用并不会暴露可扩展性,而是转移负载。但是,该库的用户并不知道这一点。您的库的用户可以自己使用 Task.Run 包装该调用。我真的认为他必须这样做。

就可扩展性而言,你确定吗?如果这些调用非常少,释放处理请求的线程可能是有益的。除非你指的是在多次启动此操作时的可扩展性? - Michał Zegan
@MichałZegan 更新了答案,请查看“更新”部分。 - Farhad Jabiyev
在我的这个具体例子中,方法的第一次调用会进行大量计算,然后可能会缓存其结果。而这个方法就像是 GetSomething。它进行大量计算然后缓存的事实,不是库的实现细节吗? - Michał Zegan
@MichałZegan 这取决于情况。当然,库也可以实现缓存。例如,在Entity Framework中存在缓存,并且它在EF内部。但是,有一些情况需要考虑。您可以查看该问题的答案。https://dev59.com/onXYa4cB1Zd3GeqP1w0o?rq=1 - Farhad Jabiyev
这并不完全是当前的问题。我的意思是,即使卸载是应该由应用程序完成的事情,在这种特定情况下,在执行此计算后,结果将被缓存。我相信在这种情况下缓存应该属于库。我问过是否可能是不包装同步方法的异步方法规则的某种例外。但也许不是...有时很难说什么是正确的,老实说。 - Michał Zegan
@MichałZegan 当然,这不是规则。即使您只是该库的用户。这是一个选择问题。如果您阅读了那篇文章,在结尾处作者会给出一些例外的示例,这些例外甚至包含在 .net 框架本身中。 - Farhad Jabiyev

0

并不完全回答问题(我认为其他答案已经足够好了),但是要添加一些额外的建议:在库中使用Task.Run时要小心,因为它可能会导致库用户意想不到的线程池耗尽。例如,开发人员正在使用许多第三方库,它们都使用Task.Run()等。现在开发人员也尝试在他的应用程序中使用Task.Run,但它会减慢他的应用程序,因为线程池已经被第三方库使用完了。

当您想要使用Parallel.ForEach并行处理时,这是一个不同的问题。


我不确定我所指的计算是否运行足够长时间以保证使用长时间运行任务。并且它们会创建一个真正的线程。如果涉及阻塞,我会说是的。但在这种情况下没有阻塞,这取决于需要搜索的模型的大小,因此没有好的答案。 - Michał Zegan

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