代码之家  ›  专栏  ›  技术社区  ›  Mike Henry

为什么是任务。当对同一个TaskCompletionSource调用多次时,什么时候会这么慢?

  •  4
  • Mike Henry  · 技术社区  · 7 年前

    TaskCompletionSource<TResult> m_tcs 具有较长的生命周期和if任务。什么时候用 m_tcs.Task 作为其论点之一,当调用数超过50000个左右时,性能似乎呈指数级下降。

    为什么在这种情况下速度这么慢?有没有另一种运行速度更快但不需要多使用4倍内存的方法?

    Task.WhenAny 很可能在中添加和删除这么多连续体 在那里的某个地方,它导致了O(N)的复杂性。

    我发现了一种性能更高的替代方法,将TCS封装在一个等待的异步函数中 m_tcs.Task 。它使用了大约4倍的内存,但可以运行

    下面的示例代码(为了获得准确的结果,请直接编译并运行.exe,而不附带调试器)。请注意 WhenAnyMemberTcsDirect 有性能问题, WhenAnyMemberTcsIndirect 是更快的替代方案,并且 WhenAnyLocalTcs

    using System;
    using System.Diagnostics;
    using System.Runtime.CompilerServices;
    using System.Threading.Tasks;
    
    public class WithTcs
    {
        // long-lived TaskCompletionSource
        private readonly TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();
    
        // this has performance issues for large N - O(N^2)
        public async Task WhenAnyMemberTcsDirectAsync(Task task)
        {
            await await Task.WhenAny(task, m_tcs.Task).ConfigureAwait(false);
        }
    
        // performs faster - O(N), but uses 4x memory
        public async Task WhenAnyMemberTcsIndirectAsync(Task task)
        {
            await await Task.WhenAny(task, AwaitTcsTaskAsync(m_tcs)).ConfigureAwait(false);
        }
    
        private async Task<TResult> AwaitTcsTaskAsync<TResult>(TaskCompletionSource<TResult> tcs)
        {
            return await tcs.Task.ConfigureAwait(false);
        }
    
        // baseline for comparison using short-lived TCS
        public async Task WhenAnyLocalTcsAsync(Task task)
        {
            var tcs = new TaskCompletionSource<bool>();
            await await Task.WhenAny(task, tcs.Task).ConfigureAwait(false);
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            show_warning_if_debugger_attached();
    
            MainAsync().GetAwaiter().GetResult();
    
            show_warning_if_debugger_attached();
            Console.ReadLine();
        }
    
        static async Task MainAsync()
        {
            const int n = 100000;
    
            Console.WriteLine("Running Task.WhenAny tests ({0:#,0} iterations)", n);
            Console.WriteLine();
    
            await WhenAnyLocalTcs(n).ConfigureAwait(false);
    
            await Task.Delay(1000).ConfigureAwait(false);
    
            await WhenAnyMemberTcsIndirect(n).ConfigureAwait(false);
    
            await Task.Delay(1000).ConfigureAwait(false);
    
            await WhenAnyMemberTcsDirect(n).ConfigureAwait(false);
        }
    
        static Task WhenAnyLocalTcs(int n)
        {
            Func<WithTcs, Task, Task> function =
                (instance, task) => instance.WhenAnyLocalTcsAsync(task);
    
            return RunTestAsync(n, function);
        }
    
        static Task WhenAnyMemberTcsIndirect(int n)
        {
            Func<WithTcs, Task, Task> function =
                (instance, task) => instance.WhenAnyMemberTcsIndirectAsync(task);
    
            return RunTestAsync(n, function);
        }
    
        static Task WhenAnyMemberTcsDirect(int n)
        {
            Func<WithTcs, Task, Task> function =
                (instance, task) => instance.WhenAnyMemberTcsDirectAsync(task);
    
            return RunTestAsync(n, function);
        }
    
        static async Task RunTestAsync(int n, Func<WithTcs, Task, Task> function, [CallerMemberName] string name = "")
        {
            Console.WriteLine(name);
    
            var tasks = new Task[n];
            var sw = new Stopwatch();
            var startBytes = GC.GetTotalMemory(true);
            sw.Start();
    
            var instance = new WithTcs();
            var step = n / 78;
            for (int i = 0; i < n; i++)
            {
                var iTemp = i;
                Task primaryTask = Task.Run(() => { if (iTemp % step == 0) Console.Write("."); });
                tasks[i] = function(instance, primaryTask);
            }
    
            await Task.WhenAll(tasks).ConfigureAwait(false);
            Console.WriteLine();
    
            var endBytes = GC.GetTotalMemory(true);
            sw.Stop();
            GC.KeepAlive(instance);
            GC.KeepAlive(tasks);
    
            Console.WriteLine("  Time: {0,7:#,0} ms, Memory: {1,10:#,0} bytes", sw.ElapsedMilliseconds, endBytes - startBytes);
            Console.WriteLine();
        }
    
        static void show_warning_if_debugger_attached()
        {
            if (Debugger.IsAttached)
                Console.WriteLine("WARNING: running with the debugger attached may result in inaccurate results\r\n".ToUpper());
        }
    }
    

    样本结果:

    Iterations | WhenAny* Method   | Time (ms) | Memory (bytes)
    ---------: | ----------------- | --------: | -------------:
         1,000 | LocalTcs          |        21 |         58,248
         1,000 | MemberTcsIndirect |        54 |        217,268
         1,000 | MemberTcsDirect   |        21 |         52,496
        10,000 | LocalTcs          |        91 |        545,836
        10,000 | MemberTcsIndirect |        98 |      2,141,836
        10,000 | MemberTcsDirect   |       140 |        545,640
       100,000 | LocalTcs          |       210 |      4,898,512
       100,000 | MemberTcsIndirect |       502 |     21,426,316
       100,000 | MemberTcsDirect   |    14,090 |      5,085,396
       200,000 | LocalTcs          |       366 |      9,630,872
       200,000 | MemberTcsIndirect |       659 |     41,450,916
       200,000 | MemberTcsDirect   |    42,599 |     10,069,248
       500,000 | LocalTcs          |       808 |     23,670,492
       500,000 | MemberTcsIndirect |     1,906 |     97,339,192
       500,000 | MemberTcsDirect   |   288,373 |     24,968,436
     1,000,000 | LocalTcs          |     1,642 |     47,272,744
     1,000,000 | MemberTcsIndirect |     3,149 |    200,480,888
     1,000,000 | MemberTcsDirect   | 1,268,030 |     48,064,772
    

    注:针对.NET 4.6.2版本(任何CPU),在Windows 7 SP1 64位Intel Core i7-4770上测试。

    1 回复  |  直到 7 年前
        1
  •  1
  •   Mike Henry    7 年前

    我找到了一个解决方案,通过使用一个成员,该解决方案似乎运行速度很快(O(N)时间),并且在大约相同的内存空间中运行 CancellationTokenSource m_cts 沿着 TaskCompletionSource .任何以前的通话设置 m_tcs 取消/故障/结果需要附带 m_cts.Cancel()

    解决方案:

    public class WithTcs
    {
        // ... same as above, plus below
    
        private readonly CancellationTokenSource m_cts = new CancellationTokenSource();
    
        public async Task WhenAnyMemberCtsAsync(Task task)
        {
            var ct = m_cts.Token;
            var tcs = new TaskCompletionSource<bool>();
            using (ct.Register(() => tcs.TrySetFrom(m_tcs)))
                await await Task.WhenAny(task, tcs.Task).ConfigureAwait(false);
        }
    }
    
    public static class TcsExtensions
    {
        public static bool TrySetFrom<TResult>(this TaskCompletionSource<TResult> dest, TaskCompletionSource<TResult> source)
        {
            switch (source.Task.Status)
            {
                case TaskStatus.Canceled:
                    return dest.TrySetCanceled();
                case TaskStatus.Faulted:
                    return dest.TrySetException(source.Task.Exception.InnerExceptions);
                case TaskStatus.RanToCompletion:
                    return dest.TrySetResult(source.Task.Result);
                default:
                    return false; // TCS has not yet completed
            }
        }
    }
    

    这就回答了这样一个问题:是否有一种快速的替代方法可以节省内存。我仍然很好奇 WhenAnyMemberTcsDirect