Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Dispose to IAsyncStateMachine #48482

Closed
timcassell opened this issue Feb 18, 2021 · 5 comments
Closed

Add Dispose to IAsyncStateMachine #48482

timcassell opened this issue Feb 18, 2021 · 5 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Threading.Tasks

Comments

@timcassell
Copy link

timcassell commented Feb 18, 2021

Background and Motivation

Unlike most exceptions, cancelations should be considered to be normal program flow. It is considered bad practice to use exceptions to control program flow, yet that is exactly how the async Task API was architected.

Not only that, but throwing an OperationCanceledException is expensive. UniTask provides a means of suppressing the exception when awaiting a task to increase performance, but because of the limitations of AsyncMethodBuilder, it is impossible to cancel the async UniTask from within the async function without throwing the exception. The only workaround is to rejigger the return type to something like async UniTask<(int result, bool canceled)> which makes it cumbersome to work with, both inside the async function and outside.

So what's the solution?

Proposed API

Add void Dispose() to IAsyncStateMachine.

public interface IAsyncStateMachine : IDisposable
{
    void MoveNext();
    void SetStateMachine(IAsyncStateMachine stateMachine);
}

Doing so would allow a custom AsyncMethodBuilder to short-circuit the statemachine, perform any necessary cleanup, and cancel the task without throwing an exception.

Usage Example

Async function

public async Promise<int> Func(CancellationToken token)
{
    try
    {
        if (token.IsCancellationRequested)
        {
            await new CancelAsyncPromise(); // Cancel the promise without throwing.
            throw new Exception("This should never be reached");
        }
        return 100;
    }
    finally
    {
        Console.WriteLine("Finally was executed.");
    }
}

Short-circuit awaitable

public struct CancelAsyncPromise : INotifyCompletion
{
    public CancelAsyncPromise GetAwaiter() => this;
    public bool IsCompleted => false;
    public void OnCompleted(Action continuation) => throw new NotImplementedException();
    public void GetResult() => throw new NotImplementedException();
}

Custom AsyncMethodBuilder implementation

public struct PromiseMethodBuilder<T>
{
    private AsyncPromiseRef _promise;

    public Promise<T> Task
    {
        get { return new Promise<T>(_promise, _promise.Id); }
    }

    public static PromiseMethodBuilder<T> Create()
    {
        return new PromiseMethodBuilder<T>()
        {
            _promise = AsyncPromiseRef.GetOrCreate()
        };
    }

    public void SetException(Exception exception)
    {
        if (exception is OperationCanceledException)
        {
            _promise.SetCanceled();
        }
        else
        {
            _promise.SetException(exception);
        }
    }

    public void SetResult(T result)
    {
        _promise.SetResult(result);
    }

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {
        if (awaiter is CancelAsyncPromise)
        {
            try
            {
                stateMachine.Dispose();
                _promise.SetCanceled();
            }
            catch (Exception e)
            {
                SetException(e);
            }
        }
        else
        {
            awaiter.OnCompleted(_promise.GetContinuation(ref stateMachine));
        }
    }
}

Alternative API

Or more like an additional nice-to-have, instead of the awaitable implementing INotifyCompletion and IsCompleted => false; and the MethodBuilder checking the type in AwaitOnCompleted, add a new optional method to AsyncMethodBuilders to accept a cancelation type:

public void Cancel<TCanceler>(ref TCanceler canceler, ref TStateMachine stateMachine)
    where TCanceler : IAsyncCanceler
    where TStateMachine : IAsyncStateMachine
{
    try
    {
        stateMachine.Dispose();
        _promise.SetCanceled();
    }
    catch (Exception e)
    {
        SetException(e);
    }
}
public struct CancelAsyncPromise : IAsyncCanceler { }

Although, that might not be feasible if a type can implement IAsyncCanceler and INotifyCompletion, unless a new keyword is introduced.

@timcassell timcassell added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Feb 18, 2021
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Feb 18, 2021
@ghost
Copy link

ghost commented Feb 18, 2021

Tagging subscribers to this area:
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and Motivation

Unlike most exceptions, cancelations should be considered to be normal program flow. It is considered bad practice to use exceptions to control program flow, yet that is exactly how the async Task API was architected.

Not only that, but throwing an OperationCanceledException is expensive. UniTask provides a means of suppressing the exception when awaiting a task to increase performance, but because of the limitations of AsyncMethodBuilder, it is impossible to cancel the async UniTask from within the async function without throwing the exception. The only workaround is to rejigger the return type to something like async UniTask<(int result, bool canceled)> which makes it cumbersome to work with, both inside the async function and outside.

So what's the solution?

Proposed API

Add void Dispose() to IAsyncStateMachine.

public interface IAsyncStateMachine : IDisposable
{
    void MoveNext();
    void SetStateMachine(IAsyncStateMachine stateMachine);
}

Doing so would allow a custom AsyncMethodBuilder to short-circuit the statemachine, perform any necessary cleanup, and cancel the task without throwing an exception.

Usage Example

Async function

public async Promise<int> Func(CancellationToken token)
{
    try
    {
        if (token.IsCancellationRequested)
        {
            await new CancelAsyncPromise(); // Cancel the promise without throwing.
            throw new Exception("This should never be reached");
        }
        return 100;
    }
    finally
    {
        Console.WriteLine("Finally was executed.");
    }
}

Short-circuit awaitable

public struct CancelAsyncPromise : INotifyCompletion
{
    public CancelAsyncPromise GetAwaiter() => this;
    public bool IsCompleted => false;
    public void OnCompleted(Action continuation) => throw new NotImplementedException();
    public void GetResult() => throw new NotImplementedException();
}

Custom AsyncMethodBuilder implementation

public struct PromiseMethodBuilder<T>
{
    private AsyncPromiseRef _promise;

    public Promise<T> Task
    {
        get { return new Promise<T>(_promise, _promise.Id); }
    }

    public static PromiseMethodBuilder<T> Create()
    {
        return new PromiseMethodBuilder<T>()
        {
            _promise = AsyncPromiseRef.GetOrCreate()
        };
    }

    public void SetException(Exception exception)
    {
        if (exception is OperationCanceledException)
        {
            _promise.SetCanceled();
        }
        else
        {
            _promise.SetException(exception);
        }
    }

    public void SetResult(T result)
    {
        _promise.SetResult(result);
    }

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {
        if (awaiter is CancelAsyncPromise)
        {
            try
            {
                stateMachine.Dispose();
                _promise.SetCanceled();
            }
            catch (Exception e)
            {
                SetException(e);
            }
        }
        else
        {
            awaiter.OnCompleted(_promise.GetContinuation(ref stateMachine));
        }
    }
}

Alternative API

Or more like an additional nice-to-have, instead of the awaitable implementing INotifyCompletion and IsCompleted => false; and the MethodBuilder checking the type in AwaitOnCompleted, add a new optional method to AsyncMethodBuilders to accept a cancelation type:

public void Cancel<TCanceler>(ref TCanceler canceler, ref TStateMachine stateMachine)
    where TCanceler : IAsyncCanceler
    where TStateMachine : IAsyncStateMachine
{
    try
    {
        stateMachine.Dispose();
        _promise.SetCanceled();
    }
    catch (Exception e)
    {
        SetException(e);
    }
}
public struct CancelAsyncPromise : IAsyncCanceler { }

Although, that might not be feasible if a type can implement IAsyncCanceler and INotifyCompletion.

Author: timcassell
Assignees: -
Labels:

api-suggestion, area-System.Threading, area-System.Threading.Tasks, untriaged

Milestone: -

@stephentoub
Copy link
Member

The C# compiler is providing the implementation of IAsyncStateMachine... what would its generated Dispose method do? Are you suggesting Dispose would execute any finally blocks as if an exception had been thrown but without one actually being thrown?

@timcassell
Copy link
Author

@stephentoub Yes, that's exactly right. Currently, if I try to short-circuit the async method this way, any finally blocks would not be executed.

@timcassell
Copy link
Author

I actually didn't consider awaiting something within the finally block, so I'll close this issue and start a discussion in csharplang.

@ghost ghost locked as resolved and limited conversation to collaborators Mar 21, 2021
@tannergooding tannergooding removed the untriaged New issue has not been triaged by the area owner label Jun 24, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Threading.Tasks
Projects
None yet
Development

No branches or pull requests

4 participants