Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid the possible stack overflow in this async/await program?

Below is an example of a program that can exhibit a stack overflow when using the method DependsOnPreviousTaskAsync, which is surprising because I don't believe there is any explicit synchronous recursion.

You can see an example of the stack before the overflow occurs at the breakpoint coded into the method DoSomething. There will be a big long chain of underlying dependent async state machine calls.

The logical chain exists because of the dependency between a task and its predecessor, but I am quite surprised that this chain of async calls manifests itself recursively on the call stack!

To work around the problem, I've coded up the method as DependsOnPreviousTaskAsync2, which uses old-style ContinueWith continuation handling instead of async/await. In this case, the call stack observed at the breakpoint is never very deep.

My question is, is there something I'm missing with respect to the use of async/await that would prevent the stack overflow? Or have I just hit an edge case that requires the use of the workaround to break the surprising recursion inherent in the async/await state machine?

EDIT: I've added TaskContinuationOptions.ExecuteSynchronously to the workaround version, and although the observed call stack can be deeper, I don't see any StackOverflowExceptions using this method. Whatever stack overflow detection logic is successfully being applied to ContinueWith and ExecuteSynchronous is not being applied in the async/await version.

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);
    }
}

Here is an example call stack at the breakpoint when using 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 ...

Here is an example call stack at the breakpoint when using 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
like image 294
Monroe Thomas Avatar asked Mar 03 '14 16:03

Monroe Thomas


People also ask

Does async await block execution?

Because await is only valid inside async functions and modules, which themselves are asynchronous and return promises, the await expression never blocks the main thread and only defers execution of code that actually depends on the result, i.e. anything after the await expression.

Does async await block the thread?

An await expression in an async method doesn't block the current thread while the awaited task is running. Instead, the expression signs up the rest of the method as a continuation and returns control to the caller of the async method.

Does async await block main thread C#?

The await operator doesn't block the thread that evaluates the async method. When the await operator suspends the enclosing async method, the control returns to the caller of the method.


1 Answers

As per the comments, there is logic to eliminate StackOverflowException when using .ContinueWith however as quoted from Stephen Toub:

@Luke Horsley: Yes, the logic for checking for a stack overflow and forcing asynchronous continuations if it's a concern exists for ContinueWith but not for await. It's feasible to add it (at the expense of some performance overhead), it just wasn't done.

His justification for the reason it was not done is as follows:

The primary reason for this is that there's a measurable performance hit to the probing (even with the optimizations in place to only do it after a certain depth) and the primary use case for async methods is for implementing asynchronous versions of synchronous methods, in which case the deepest you'd get on the stack is on the same order as you would have gotten in your synchronous call stack. It's only when you start playing asynchronous tricks and using asynchronous data structures that you start running the risk of much deeper call stacks, and the majority of those situations are appropriately handled by logic in the data structures being used, rather than forcing the performance hit upon all awaits. That, combined with the fact that most code doesn't just have one of the issues, led us to not enable the stack probing for awaits. That's not to say we couldn't or wouldn't enable it in the future (it's a fairly trivial change).

One solution is to use

await Task.Yield();

inside of DependsOnPreviousTaskAsync before you return result, this will ensure continuations are executed asynchronously and the stack trace is virtually reset.

Here is a simpler sample I developed to reproduce your problem:

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();
    }
}

This sample also throws a StackOverflowException, uncommenting the await Task.Yield() solves the issue.

like image 144
Lukazoid Avatar answered Sep 28 '22 18:09

Lukazoid