async/await 和 TaskCompletionSource 的奇怪堆栈跟踪增长

Weird stack trace growth with async/await and TaskCompletionSource

以下 C# 代码:

class Program
{
    static readonly List<TaskCompletionSource<bool>> buffer = 
                new List<TaskCompletionSource<bool>>();
    static Timer timer;

    public static void Main()
    {
        var outstanding = Enumerable.Range(1, 10)
            .Select(Enqueue)
            .ToArray();

        timer = new Timer(x => Flush(), null, 
                         TimeSpan.FromSeconds(1),
                         TimeSpan.FromMilliseconds(-1));
        try
        {
            Task.WaitAll(outstanding);
        }
        catch {}

        Console.ReadKey();
    }

    static Task Enqueue(int i)
    {
        var task = new TaskCompletionSource<bool>();
        buffer.Add(task);
        return task.Task;
    }

    static void Flush()
    {
        try
        {
            throw new ArgumentException("test");
        }
        catch (Exception e)
        {
            foreach (var each in buffer)
            {
                var lenBefore = e.StackTrace.Length;
                each.TrySetException(e);
                var lenAfter = e.StackTrace.Length;
                Console.WriteLine($"Before - After: {lenBefore} - {lenAfter}");
                Console.WriteLine(e.StackTrace);

            }
        }
    }
}

产生:

Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149

但是当我将 Enqueue 方法更改为异步时:

static async Task Enqueue(int i)
{
    var task = new TaskCompletionSource<bool>();
    buffer.Add(task);
    return await task.Task;
}

结果是:

Before - After: 149 - 643
Before - After: 643 - 1137
Before - After: 1137 - 1631
Before - After: 1631 - 2125
Before - After: 2125 - 2619
Before - After: 2619 - 3113
Before - After: 3113 - 3607
Before - After: 3607 - 4101
Before - After: 4101 - 4595
Before - After: 4595 - 5089

看起来每个缓冲项目的堆栈跟踪递归增长。对于第一项异常堆栈跟踪将是:

   at Program.Flush() in C:\src\Program.cs:line 41
   --- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34

而第二个将如下所示,依此类推:

   at Program.Flush() in C:\src\Program.cs:line 41
   --- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
   --- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34

这是怎么回事,如何解决?

简短回答:await 尝试解包结果,而方法 withod await 不尝试访问任务结果。

更长的答案:

  1. 调用堆栈的重复部分如下所示:

  1. ValidateEnd method of TaskAwaiter is being inlined, and the HandleNonSuccessAndDebuggerNotification causes a call to ThrowForNonSuccess which seems to be inlined too and, as single exception is used to set result for 10 TaskCompletionSources, the reason of growing stack of that exception can be seen here.

简单的解决方案是在每次 TrySetException 调用时使用 new Exception("Some descriptive message", originalException)

问题是您正在为每个任务重复使用相同的异常,因此它将所有堆栈附加在一起,假设这是它们进行的顺序。

如果您改为为每个

创建一个新的例外
var ex = new Exception(e.Message, e);
each.TrySetException(ex);

然后你会得到

Before - After: 87 - 341
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
Before - After: 87 - 341
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
Before - After: 87 - 341
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
Before - After: 87 - 341

如果您还使用 Ben.Demystifier

var ex = new Exception(e.Message, e);
each.TrySetException(ex);
var lenAfter = ex.Demystify().StackTrace.Length;

然后会进一步下降:

Before - After: 87 - 105
   at async Task Program.Enqueue(int i) in C:\src\Program.cs:line 34
Before - After: 92 - 105
   at async Task Program.Enqueue(int i) in C:\src\Program.cs:line 34
Before - After: 92 - 105
   at async Task Program.Enqueue(int i) in C:\src\Program.cs:line 34
Before - After: 92 - 105