如何避免此async/await程序中可能的堆栈溢出?

Mon*_*mas 6 c# multithreading async-await

下面是一个在使用该方法时可能出现堆栈溢出的程序示例DependsOnPreviousTaskAsync,这是令人惊讶的,因为我不相信有任何显式的同步递归.

在编码到方法中的断点处发生溢出之前,您可以看到堆栈的示例DoSomething.将有一大堆潜在的异步状态机调用.

由于任务与其前任之间存在依赖关系,因此存在逻辑链,但我很惊讶这串异步调用在调用堆栈上递归显示!

为了解决这个问题,我将方法编码为DependsOnPreviousTaskAsync2,使用旧式ContinueWith连续处理而不是async/await.在这种情况下,在断点处观察到的调用堆栈永远不会很深.

我的问题是,是否有一些关于使用async/await的问题,这会阻止堆栈溢出?或者我是否只是遇到了需要使用变通方法打破async/await状态机固有的惊人递归的边缘情况?

编辑:我已添加TaskContinuationOptions.ExecuteSynchronously到变通方法版本,虽然观察到的调用堆栈可能更深,但我没有看到使用此方法的任何StackOverflowExceptions.无论堆栈溢出检测逻辑被成功地被施加到ContinueWith并且ExecuteSynchronous在异步/ AWAIT版本被施加.

class Program
{
    public async static Task<int> DependsOnPreviousTaskAsync(Task<int> previousTask)
    {
        if (previousTask == null)
            return 0;

        var result = await DoSomethingAsync(previousTask).ConfigureAwait(false);
        Console.WriteLine(result);
        return result;
    }

    public static Task<int> DependsOnPreviousTaskAsync2(Task<int> previousTask)
    {
        // this is a non async/await version of DependsOnPreviousTaskAsync

        if (previousTask == null)
            return Task.FromResult(0);

        var tcs = new TaskCompletionSource<int>();

        DoSomethingAsync(previousTask)
            .ContinueWith(t =>
            {
                if (t.IsCanceled)
                {
                    tcs.TrySetCanceled();
                }
                else if (t.IsFaulted)
                {
                    tcs.TrySetException(t.Exception);
                }
                else
                {
                    Console.WriteLine(t.Result);
                    tcs.TrySetResult(t.Result);
                }
            }, TaskContinuationOptions.ExecuteSynchronously);

        return tcs.Task;
    }

    public async static Task<int> DoSomethingAsync(Task<int> previousTask)
    {
        var tasksToWaitOn = new Task[]
        {
            previousTask,
            SomethingElseAsync()
        };
        await Task.WhenAll(tasksToWaitOn).ConfigureAwait(false);

        var previous = ((Task<int>)tasksToWaitOn[0]).Result;
        if (previous == 500)
            Debugger.Break();

        return previous + 1;
    }

    public async static Task SomethingElseAsync()
    {
        await Task.Run(() =>
        {
            Thread.Sleep(2);
        });
    }

    static void Main(string[] args)
    {
        const bool causePossibleStackOverflow = true;

        Task<int> previous = null;
        for (var i = 0; i < 100000; i++)
        {
            previous = causePossibleStackOverflow 
                ? DependsOnPreviousTaskAsync(previous) 
                : DependsOnPreviousTaskAsync2(previous);
        }

        Console.WriteLine(previous.Result);
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是使用时断点处的示例调用堆栈DependsOnPreviousTaskAsync:

TestAsyncRecursion.exe!TestAsyncRecursion.Program.DoSomethingAsync(System.Threading.Tasks.Task<int> previousTask) Line 62   C#
[Resuming Async Method] 
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object stateMachine)  Unknown
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run()    Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.OutputAsyncCausalityEvents<System.Runtime.CompilerServices.AsyncTaskMethodBuilder<int>>.AnonymousMethod__0()    Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke()    Unknown
mscorlib.dll!System.Runtime.CompilerServices.TaskAwaiter.OutputWaitEtwEvents.AnonymousMethod__0()   Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke()    Unknown
mscorlib.dll!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(System.Action action, bool allowInlining, ref System.Threading.Tasks.Task currentTask)    Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishContinuations()  Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageThree() Unknown
mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetResult(System.Threading.Tasks.VoidTaskResult result)  Unknown
mscorlib.dll!System.Threading.Tasks.Task.WhenAllPromise.Invoke(System.Threading.Tasks.Task completedTask)   Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishContinuations()  Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageThree() Unknown
mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetResult(System.Threading.Tasks.VoidTaskResult result)  Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>.SetResult(System.Threading.Tasks.VoidTaskResult result)  Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult() Unknown
TestAsyncRecursion.exe!TestAsyncRecursion.Program.SomethingElseAsync() Line 73  C#
[Resuming Async Method] 
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object stateMachine)  Unknown
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run()    Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.OutputAsyncCausalityEvents<System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>>.AnonymousMethod__0()  Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke()    Unknown
mscorlib.dll!System.Runtime.CompilerServices.TaskAwaiter.OutputWaitEtwEvents.AnonymousMethod__0()   Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke()    Unknown
mscorlib.dll!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(System.Action action, bool allowInlining, ref System.Threading.Tasks.Task currentTask)    Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishContinuations()  Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageThree() Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageTwo()   Unknown
mscorlib.dll!System.Threading.Tasks.Task.Finish(bool bUserDelegateExecuted) Unknown
mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot)    Unknown
mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Unknown
mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Unknown
mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch()    Unknown
mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Unknown
[Async Call]    
TestAsyncRecursion.exe!TestAsyncRecursion.Program.DependsOnPreviousTaskAsync(System.Threading.Tasks.Task<int> previousTask) Line 18 C#
[Async Call]    
TestAsyncRecursion.exe!TestAsyncRecursion.Program.DoSomethingAsync(System.Threading.Tasks.Task<int> previousTask) Line 58   C#
[Async Call]    
TestAsyncRecursion.exe!TestAsyncRecursion.Program.DependsOnPreviousTaskAsync(System.Threading.Tasks.Task<int> previousTask) Line 18 C#
[Async Call]    
TestAsyncRecursion.exe!TestAsyncRecursion.Program.DoSomethingAsync

and so on ...
Run Code Online (Sandbox Code Playgroud)

以下是使用时断点处的示例调用堆栈DependsOnPreviousTaskAsync2:

TestAsyncRecursion.exe!TestAsyncRecursion.Program.DoSomethingAsync(System.Threading.Tasks.Task<int> previousTask) Line 62   C#
[Resuming Async Method] 
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object stateMachine)  Unknown
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run()    Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.OutputAsyncCausalityEvents<System.Runtime.CompilerServices.AsyncTaskMethodBuilder<int>>.AnonymousMethod__0()    Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke()    Unknown
mscorlib.dll!System.Runtime.CompilerServices.TaskAwaiter.OutputWaitEtwEvents.AnonymousMethod__0()   Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke()    Unknown
mscorlib.dll!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(System.Action action, bool allowInlining, ref System.Threading.Tasks.Task currentTask)    Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishContinuations()  Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageThree() Unknown
mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetResult(System.Threading.Tasks.VoidTaskResult result)  Unknown
mscorlib.dll!System.Threading.Tasks.Task.WhenAllPromise.Invoke(System.Threading.Tasks.Task completedTask)   Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishContinuations()  Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageThree() Unknown
mscorlib.dll!System.Threading.Tasks.Task<int>.TrySetResult(int result)  Unknown
mscorlib.dll!System.Threading.Tasks.TaskCompletionSource<int>.TrySetResult(int result)  Unknown
TestAsyncRecursion.exe!TestAsyncRecursion.Program.DependsOnPreviousTaskAsync2.AnonymousMethod__4(System.Threading.Tasks.Task<int> t) Line 44    C#
mscorlib.dll!System.Threading.Tasks.ContinuationTaskFromResultTask<int>.InnerInvoke()   Unknown
mscorlib.dll!System.Threading.Tasks.Task.Execute()  Unknown
mscorlib.dll!System.Threading.Tasks.Task.ExecutionContextCallback(object obj)   Unknown
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot)    Unknown
mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Unknown
mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Unknown
mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch()    Unknown
mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Unknown
Run Code Online (Sandbox Code Playgroud)

Luk*_*oid 7

根据评论,有一些逻辑可以在使用时消除StackOverflowException,.ContinueWith但是引自Stephen Toub:

@Luke Horsley:是的,如果存在一个问题,那么检查堆栈溢出和强制异步延续的逻辑是否存在于ContinueWith而不是等待.添加它是可行的(以牺牲一些性能开销为代价),它只是没有完成.

他之所以没有这样做的理由如下:

这样做的主要原因是探测的性能受到了可测量的影响(即使只有在某个深度之后才进行优化),异步方法的主要用例是实现同步方法的异步版本,其中你在堆栈上最深处的情况与你在同步调用堆栈中的顺序相同.只有当你开始播放异步技巧并使用异步数据结构时才开始冒更大更深层次调用堆栈的风险,并且大多数情况都是由正在使用的数据结构中的逻辑正确处理,而不是强制性能受到影响.等等.结合大多数代码不仅仅存在其中一个问题的事实,导致我们无法启用堆栈探测等待.这并不是说我们将来不能或不会启用它(这是一个相当微不足道的变化).

一种解决方案是使用

await Task.Yield();
Run Code Online (Sandbox Code Playgroud)

DependsOnPreviousTaskAsync返回之前result,这将确保异步执行延续并且实际上重置堆栈跟踪.

这是我为重现您的问题而开发的更简单的示例:

internal class Program
{
    private static void Main()
    {
        bool useAsync = true;

        var tcs = new TaskCompletionSource<object>();
        Task previous = tcs.Task;
        for (var i = 0; i < 100000; ++i)
        {
            previous = useAsync ? DoSomethingUsingAsync(previous) : DoSomethingUsingContinuation(previous);
        }

        tcs.SetResult(null);
        previous.Wait();
    }

    private static Task DoSomethingUsingContinuation(Task previousTask)
    {
        return previousTask.ContinueWith(_ => { }, TaskContinuationOptions.ExecuteSynchronously);
    }

    private static async Task DoSomethingUsingAsync(Task previousTask)
    {
        await previousTask.ConfigureAwait(false);
        // Uncomment the next line to solve!
        // await Task.Yield();
    }
}
Run Code Online (Sandbox Code Playgroud)

此示例还抛出了一个StackOverflowException,取消注释await Task.Yield()解决问题.