【问题标题】:Weird stack trace growth with async/await and TaskCompletionSource使用 async/await 和 TaskCompletionSource 的奇怪堆栈跟踪增长
【发布时间】:2017-11-12 08:51:43
【问题描述】:

以下 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

这里发生了什么以及如何解决它?

【问题讨论】:

  • 更新:我最终创建了像 BatchException 这样的特殊异常,并按照@Dmitry 的建议将原始异常作为内部异常传递。在我看来,这是解决这个问题的最简洁和正确的方法。直到更好的时代......

标签: c# .net asynchronous async-await task-parallel-library


【解决方案1】:

简答:await 尝试解包结果,而带有 await 的方法不尝试访问任务结果。

更长的答案:

  1. 调用堆栈的循环部分如下所示:

  1. TaskAwaiterValidateEnd 方法被内联,HandleNonSuccessAndDebuggerNotification 导致对 ThrowForNonSuccess 的调用,这似乎也被内联,因为单个异常用于为 10 TaskCompletionSources 设置结果, the reason of growing stack of that exception can be seen here.

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

【讨论】:

  • 很好的解释!但我需要保留原始异常,因为这是通用解决方案的一部分,消费者可能想要捕获原始异常,即它可能是 AzureSDK 抛出的 ConcurrencyConflictException。有没有更理智的方法来解决它?
  • 将原始异常包装在新异常中需要所有消费者打开包装并进行额外检查((
  • 在完整的 .Net 框架上,有一种方法 - 可以使用 BinaryFormatter 对异常进行深度复制,但这样的解决方案不适用于 .Net Core
  • 如果 .Net 核心有迫切需要 - 可以提取 github.com/dotnet/orleans/blob/master/src/Orleans/Serialization/…
  • 不,这行不通。克隆导致堆栈跟踪丢失
【解决方案2】:

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

如果您改为为每个例外创建一个新的例外

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

【讨论】:

  • 谢谢,本!解谜者太棒了! ))
  • 我了解解决方案,但并不能完全解决问题。想象一下,如果该异常来自第 3 方代码(如 Azure SDK)。我不能(重新)为每个任务创建新实例。我最终创建了特殊的 BatchItemException 但它仍然是一个有损解决方案
猜你喜欢
  • 2012-12-02
  • 2013-04-29
  • 1970-01-01
  • 1970-01-01
  • 2018-02-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-05
相关资源
最近更新 更多