为什么将等待的异步方法包装到Task.Run中至少可以提高性能两倍?

时间:2016-04-25 22:51:17

标签: c# async-await task-parallel-library httplistener

我有一个简单的HTTP Server实现。代码如下所示。它在具有32个核心的服务器计算机上进行了测试。如果我将processContext方法包装到Task.Run调用中,那么性能会加倍(至少)。考虑到这给了我在这个特定情况下的性能提升,我现在很困惑:有一些方法返回一个我不想等待的任务,我应该遵循什么策略?我应该直接调用它还是应该在Task.Run中包装?

class Program
{
    private static ConcurrentBag<DateTime> _trails = new ConcurrentBag<DateTime>();

    static void Main(string[] args)
    {
        Console.WriteLine($"Is server GC: {(GCSettings.IsServerGC ? "true" : "false")}");

        string prefix = args[0];

        CancellationTokenSource cancellationSource = new CancellationTokenSource();

        HttpListener httpListener = new HttpListener();

        httpListener.Prefixes.Add(prefix);

        httpListener.Start();

        Task.Run(async () =>
        {
            while (!cancellationSource.Token.IsCancellationRequested)
            {
                HttpListenerContext context = null;

                try
                {
                    context = await httpListener.GetContextAsync();

                    if (cancellationSource.Token.IsCancellationRequested)
                    {
                        context.Response.Abort();

                        break;
                    }
                }
                catch (ObjectDisposedException)
                {
                    return;
                }
                catch (HttpListenerException ex)
                {
                    if (cancellationSource.Token.IsCancellationRequested && ex.ErrorCode == 995)
                    {
                        break;
                    }

                    throw;
                }

                // Uncommenting below line and commenting the next one improves the performance at least twice
                // Task childProcessingTask = Task.Run(async () => await processContext(context));

                var dt = processContext(context);
            }
        });

        using (Timer t = new Timer(o => Console.Title = $"Async Server: {_trails.Count}", null, 0, 5000))
        {
            Console.WriteLine("Running...");

            Console.ReadLine();

            cancellationSource.Cancel();

            Console.WriteLine("Stopped accepting new request. Waiting for pending requests...");

            Console.WriteLine("Stopped");

            httpListener.Close();
        }

        var gTrails = _trails.GroupBy(t => new DateTime(t.Year, t.Month, t.Day, t.Hour, t.Minute, 0))
            .Select(g => new { MinuteDt = g.Key, Count = g.Count() })
            .OrderBy(x => x.MinuteDt).ToList();

        gTrails.ForEach(x => Console.WriteLine($"{x.MinuteDt:HH:mm}\t{x.Count}"));

        if (gTrails.Count > 2)
        {
            decimal avg = gTrails.Take(gTrails.Count - 1).Skip(1).Average(g => (decimal)g.Count);
            Console.WriteLine($"Average: {avg:0.00}/min, {avg / 60.0m:00.0}/sec");
        }

        Console.ReadLine();
    }

    private static async Task processContext(HttpListenerContext context)
    {
        DateTime requestDt = DateTime.Now;
        Stopwatch sw = Stopwatch.StartNew();

        string requestId = context.Request.QueryString["requestId"];

        byte[] requestIdBytes = Encoding.ASCII.GetBytes(requestId);

        context.Response.ContentLength64 = requestIdBytes.Length;

        await context.Response.OutputStream.WriteAsync(requestIdBytes, 0, requestIdBytes.Length);

        try
        {
            context.Response.Close();
        }
        catch { }

        _trails.Add(requestDt);
    }
}

1 个答案:

答案 0 :(得分:1)

  

为什么将等待的异步方法包装到Task.Run中会至少提高两次性能?

许多异步方法都有一个同步部分,它在调用者的线程上同步运行。 对于processContext方法,从开始到第一个await的该方法的代码在调用者的线程上运行。

如果您不使用Task.Run ,则在接受连接后,您的软件首先运行processContext方法的同步部分。它调用await,任务的上下文进入堆,线程自由并恢复while (!cancellationSource.Token.IsCancellationRequested)循环的另一次迭代。 很快任务就完成了,调度程序想要恢复它。

但它无法在它开始的同一个线程上恢复它,因为该线程可能正在忙于侦听新连接并启动另一个子任务。

你有很多核心,任务将在另一个核心上恢复非常好的变化。如果它发生在同一CPU的另一个核心上,则核心必须等待来自L3缓存的数据(如局部变量,HttpListenerContext实例变量等),因为L1和L2缓存是每个核心。如果它发生在另一个CPU上,核心将不得不等待系统RAM,这甚至更慢。

如果您使用Task.Run ,则运行该无限while(!IsCancellationRequested)循环的线程会继续执行此操作,并立即继续执行该循环的另一次迭代,所有数据都已打开该核心的缓存。

processContext方法将从一开始就在其他核心上运行。如果您只发送几个字节,则await WriteAsync将以非常快的速度返回。调度程序并不愚蠢。如果您有32个核心且没有那么多任务,则调度程序可能会在启动它的同一核心上恢复processContext任务,并且所有特定于会话的数据已经存在于该核心的缓存中。

  

我应该遵循什么策略?

  1. 测试几种技术,选择适合您工作负载的更好的方法。 Task.Run并不总是更快,只是在你的情况下。

  2. 或者,了解事情是如何运作的,你将能够做出有根据的猜测。实施起来不太精确,但要快得多。