简体   繁体   中英

Cancel and wait on async Task methods

Trying to figure this out but can't find anything definitive.

Suppose I have a class that does some work async and can be started or stopped on the fly:

class ModelObject
{
    private CancellationTokenSource cts;
    private CancellationToken ct;
    private Progress<int> progress;
    
    private async Task DoSomething(IProgress<int> progress)
    {
        ...
        
        while (DoingSomething)
        {
            if (ct.IsCancellationRequested)
            {
                await Task.Delay(10000); // Take our sweet time to cancel
                // Cancel work and return
            }
            
            ...
        }       
    }
    
    public async void StartAsync()
    {
        progress = new Progress<int>(...);
        cts = new CancellationTokenSource();
        ct = cts.Token;
        
        await DoSomething(progress);
    }
    
    public void Stop()
    {
        ct.Cancel();
        
        // HELP: await while DoSomething cancels
        
        // perform cleanup
    }
}

I am not sure how to await while the existing instance of DoSomething cancels.

Typical use would be like this:

ModelObject modelObject = new ModelObject();
modelObject.StartAsync();
modelObject.Stop();

Any help would be highly appreciated.

While you could await the task returned from DoSomething , you can also use a TaskCompletionSource ;

        private TaskCompletionSource<object> tcs;

        private async Task DoSomething(IProgress<int> progress){
          ...
           while (DoingSomething)
           {
               if (ct.IsCancellationRequested)
               {
                   // Cancel work and return
               } 
            ...
           }       
           tcs.SetResult(null);
        }

        public async Task Stop () {
           cts.Cancel();
           await tcs.Task;
        }

My preference would be to add a Completion property to the ModelObject class. The caller could choose to await or not the Completion after canceling an operation. Also I would probably place guards against an improper use of the API. Attempting to start a new operation while an existing one is pending, would cause an exception.

public Task Completion { get; private set; } = Task.CompletedTask;

public async void Start()
{
    if (_cts != null) throw new InvalidOperationException("Operation is in progress.");
    using (_cts = new CancellationTokenSource())
    {
        _ct = _cts.Token;
        _progress = new Progress<int>(/*...*/);
        this.Completion = DoSomethingAsync();
        try
        {
            await this.Completion;
        }
        catch { } // Ignore
        finally
        {
            _cts = null;
            _ct = default;
            _progress = null;
        }
    }
}

public void Stop()
{
    _cts?.Cancel();
}

Another piece of advice: in case of cancellation ensure that your DoSomethingAsync throws an OperationCanceledException . This is the expected behavior of a cancelable asynchronous operation. For example if you pass the token to any built-in API, you'll notice that the returned task will complete as Canceled immediately after calling the _cts.Cancel() method. To mimic this behavior you could call _ct.ThrowIfCancellationRequested() inside the loop, and pass the token to any API that accepts it (for example the Task.Delay method). In fact it would be probably better to catch internally this exception, and do the clean up inside the catch block:

private async Task DoSomethingAsync()
{
    try
    {
        while (doingSomething)
        {
            // Do some work
            _ct.ThrowIfCancellationRequested();
        }
    }
    catch (OperationCanceledException)
    {
        await Task.Delay(10000); // Take our sweet time to cancel
        throw; // Rethrow the exception
    }
    finally
    {
        // perform cleanup, whether successful, failed or canceled
    }
}

I'm not sure why StartAsync method is async since it indefinitely executes unless the Stop is Invoked. I mean it will not return at some point in time without another action. I feel there is no need for async in this class But if it necessary, I've drafted code that might help

    class ModelObject {
        private CancellationTokenSource cts;
        private CancellationToken ct;
        private Progress<int> progress;
        private Task doSomethingTask;
        private async Task DoSomething(IProgress<int> progress){
          ...
           while (DoingSomething)
           {
               if (ct.IsCancellationRequested)
               {
                   // Cancel work and return
               } 
            ...
           }       
        }

        public async void StartAsync () {
            progress = new Progress<int>(...);
            cts = new CancellationTokenSource ();
            ct = cts.Token;
            doSomethingTask = DoSomething (progress);
            await doSomethingTask;
        }

        public void Stop () {
            cts.Cancel();
            doSomethingTask.Wait();
        }
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM