异步/等待和TaskCompletionSource导致堆栈跟踪增长异常

9
以下是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所建议的那样。在我看来,这是解决这个问题最简洁和正确的方法。直到更好的时候... - Yevhen Bobrov
2个回答

7
简短回答: await 尝试解包结果,而没有使用 await 的方法则不尝试访问任务结果。
更长的回答:
  1. 调用栈的重复部分如下所示:

RecurrringCallStack

  1. TaskAwaiterValidateEnd 方法被内联,HandleNonSuccessAndDebuggerNotification 调用 ThrowForNonSuccess,这似乎也被内联了,并且由于单个异常用于设置 10 个 TaskCompletionSource 的结果,因此异常堆栈增长的原因可以在此处看到

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


很好的解释!但是我需要保留原始异常,因为这是通用解决方案的一部分,消费者可能想要捕获原始异常,即它可能是AzureSDK抛出的ConcurrencyConflictException。有更明智的方法来修复它吗? - Yevhen Bobrov
将原始异常包装在新异常中需要所有使用者进行解包并进行额外的检查。 - Yevhen Bobrov
如果在 .Net core 中需要极端情况下的支持,可以提取 https://github.com/dotnet/orleans/blob/master/src/Orleans/Serialization/ILBasedExceptionSerializer.cs。 - Dmytro Vakulenko
不行,那样做不起作用。克隆会导致堆栈跟踪丢失。 - Yevhen Bobrov
@YevhenBobrov 嗯,我应该知道的...无论如何 - 这里是可行的解决方案 https://gist.github.com/dVakulen/a8d0acaf1e7ba1835c27792230ddc3e5 。不得不使用一些魔法,但对于这种情况应该没问题。 - Dmytro Vakulenko
显示剩余6条评论

1
问题在于您每个任务都重复使用相同的异常,因此它会将所有堆栈附加在一起,假设这是它们按顺序进行的序列。
如果您改为为每个任务创建新的异常
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

谢谢,本!Demystifier 真是太棒了!)) - Yevhen Bobrov
我明白这个解决方案,但它并不能完全解决问题。想象一下,如果那个异常来自第三方代码(比如Azure SDK),我就不能为每个任务创建新的实例。最终,我只能创建特殊的BatchItemException,但这仍然是一个有损的解决方案。 - Yevhen Bobrov

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