异步等待(async await)性能问题?

40
< p > < em >(只是一个理论问题-针对非 GUI 应用程序)

假设我有许多awaits的代码:

public async Task<T> ConsumeAsync()
    {
          await A();
          await b();
          await c();
          await d();
          //..
    }

每个任务都可能需要非常短的时间,

问题(再次强调,这是理论问题)

情况可能会出现整体处理时间与单个线程相比会更长,因为需要处理所有这些“释放回线程”和“取回线程”的操作(如下面红色和绿色所示的操作):

enter image description here

这样做是否需要花费更多的时间,而单个线程只需要少量延迟就可以完成所有工作?

我的意思是,我想成为最有效率的人,但实际上,由于所有这些来回切换,我实际上失去了生产力。

这种情况可能发生吗?


这是性能和灵活性之间的权衡。 - user703016
1
为什么不试一下呢?让你的方法调用变得极快(在方法体中什么也不放)。然后看看它是否比同步调用需要更多时间。运行代码一千次并绘制结果图表。但是话说回来,你永远不会有空的方法体,对吧?当你需要执行几个I/O任务时,Asyncawait可能是最好的选择。 - mason
2
顺便说一句,不要预先优化你的代码。一旦出现瓶颈或性能问题,再去进行优化。只有在确定可以获得好处时才应该进行预先优化。 - mason
3
使用异步几乎从来不会更快,但它更具可扩展性。 - i3arnon
2
http://msdn.microsoft.com/en-us/magazine/hh456402.aspx - Abhitalks
显示剩余2条评论
5个回答

28

理论上是可以的,但在实际情况下通常不会这样做。

通常情况下,async 用于I/O绑定操作,与线程管理相比,线程管理的开销微不足道。大多数情况下,异步操作要么需要很长时间(与线程管理相比),要么已经完成(例如缓存)。请注意,如果操作已经完成,async 就有了一个“快速路径”,它不会让出线程。

有关更多信息,请参见异步之道异步性能


1
即使以同步方式执行,由于TPL代码必须运行,因此它将需要更长时间。请记住,正如OP指出的那样,这是一个理论问题。 - jgauffin
1
@jgauffin:我不太确定你所说的“TPL代码必须运行”的意思。如果你是这个意思,那么并没有调度在进行。 - Stephen Cleary
我的意思是,如果你使用TPL来进行基准测试代码和直接执行操作的代码,后者会更快,因为TPL会增加一些开销(即使很小)。 - jgauffin
5
@jgauffin的意思是,使用快速路径的await代码只使用了极少量的TPL(相当于Task.FromResult()Task.Result())。这确实与Task.Run()或类似内容没有任何相似之处。 - svick
1
当我写入文件时,会有成千上万个小等待。但是有一个8MB的缓冲区。因此,真正的等待只有在这个缓冲区满了并且必须写入磁盘时才会出现。这大大提高了性能,而不是一次又一次地写入磁盘。 - M.kazem Akhgary

19
一个Task对象代表了一个未完成操作的延迟结果。如果你没有任何未完成操作,那么你不需要使用任务和async/await。否则,我相信async/await代码通常比其裸的TPL ContinueWith方法更高效。
现在我们来测试一下时间:
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {
        // async/await version
        static async Task<int> Test1Async(Task<int> task)
        {
            return await task;
        }

        // TPL version
        static Task<int> Test2Async(Task<int> task)
        {
            return task.ContinueWith(
                t => t.Result,
                CancellationToken.None,
                TaskContinuationOptions.ExecuteSynchronously,
                TaskScheduler.Default);
        }

        static void Tester(string name, Func<Task<int>, Task<int>> func)
        {
            var sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            for (int i = 0; i < 10000000; i++)
            {
                func(Task.FromResult(0)).Wait();
            }
            sw.Stop();
            Console.WriteLine("{0}: {1}ms", name, sw.ElapsedMilliseconds);
        }

        static void Main(string[] args)
        {
            Tester("Test1Async", Test1Async);
            Tester("Test2Async", Test2Async);
        }
    }
}

输出结果:
Test1Async: 1582毫秒
Test2Async: 4975毫秒
因此,默认情况下,使用await关键字的延续比ContinueWith延续处理效率更高。让我们稍微优化一下这段代码:
// async/await version
static async Task<int> Test1Async(Task<int> task)
{
    if (task.IsCompleted)
        return task.Result;
    return await task;
}

// TPL version
static Task<int> Test2Async(Task<int> task)
{
    if (task.IsCompleted)
        return Task.FromResult(task.Result);

    return task.ContinueWith(
        t => t.Result,
        CancellationToken.None,
        TaskContinuationOptions.ExecuteSynchronously,
        TaskScheduler.Default);
}

输出结果:
Test1Async:1557毫秒 Test2Async:429毫秒
现在非异步版本获胜。在使用async版本时,我相信这种优化已经在内部通过async/await基础设施完成了。
无论如何,到目前为止我们只处理了已完成的任务(Task.FromResult)。让我们引入实际的异步性(自然地,这次我们将做更少的迭代):
static Task<int> DoAsync()
{
    var tcs = new TaskCompletionSource<int>();
    ThreadPool.QueueUserWorkItem(_ => tcs.SetResult(0));
    return tcs.Task;
}

static void Tester(string name, Func<Task<int>, Task<int>> func)
{
    ThreadPool.SetMinThreads(200, 200);
    var sw = new System.Diagnostics.Stopwatch();
    sw.Start();
    for (int i = 0; i < 1000000; i++)
    {
        func(DoAsync()).Wait();
    }
    sw.Stop();
    Console.WriteLine("{0}: {1}ms", name, sw.ElapsedMilliseconds);
}

输出结果:

Test1Async: 4207毫秒
Test2Async: 4734毫秒

现在两者的差距非常小,虽然async版本仍然稍微更快一些。但我认为这样的收益是微不足道的,与异步操作的实际成本或恢复捕获上下文的成本相当。

总之,如果你处理异步任务,出于易用性、可读性和可维护性的考虑,请选择async/await,而不是出于性能原因。


3
“这种情况是否可能发生?”
完全有可能。因此,您应该慎重考虑在何处使用异步代码。通常来说,最好将其用于实际执行异步操作(例如磁盘或网络I/O)的方法。这些操作所需的时间通常远远超过在线程上安排任务的成本。此外,在操作系统级别上,这些类型的操作本质上是异步的,因此使用异步方法实际上会消除一层抽象。
即使在这些情况下,除非能够利用并发性,否则切换到异步代码也可能不会看到明显的性能差异。例如,您发布的代码可能不会获得任何实际的性能收益,除非将其更改为类似以下内容:”
await Task.WhenAll(new[]{A(), B(), C(), D(), ...});

2
但这假定异步总是利用线程,而事实并非如此。 - Kirk Woll
@KirkWoll:请澄清一下。我特别说了,当您执行实际异步操作时,应该使用async。我想不出任何非异步操作可以转换为async方法而不利用线程的情况。 - StriplingWarrior
2
看看 Stephen Clearly 的文章,There is no thread;Erik Meijer 曾经赞同地链接到它。 :) - Kirk Woll
@KirkWoll:抱歉,我几年后才注意到你在这里的回复。那篇文章特别讨论了固有的异步操作,正如我在我的答案中所说,无论它们是否多线程,都可以从async/await和并发(例如Task.WhenAll())中受益。为什么你说我的答案假定异步总是利用线程呢? - StriplingWarrior

2

是的,这种情况可能会发生。还要记住 - 所有你可以编程的效率 - 任务系统确实有开销。

如果你对这样的事情过于细化,同步开销可能会让你崩溃。也就是说:任务编程非常高效。

但是旧规则依然适用:不要过度细化。有时优化是有帮助的。


2

当然可能会发生这种情况。由于创建状态机的开销,来回传递控制并使用IOCP线程等因素,会增加一些开销。但正如所说,TPL已经进行了优化。例如,如果您的TaskAwaitable完成得很快,则可能没有开销,它将同步执行,这在快速操作时经常发生。


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