简体   繁体   中英

Should DisposeAsync throw background task exceptions, or leave it to the client to observe explicitly?

I don't think this question is a duplicate of "Proper way to deal with exceptions in DisposeAsync" .

Let's say my class that implements IAsynsDisposable because it has a long-running background task, and DisposeAsync terminates that task. A familiar pattern might be the Completion property, eg ChannelReader<T>.Completion (despite ChannelReader doesn't implement IAsynsDisposable ).

Is it considered a good practice to propagate the Completion task's exceptions outside DisposeAsync ?

Here is a complete example that can be copied/pasted into a dotnet new console project. Note await this.Completion inside DisposeAsync :

try
{
    await using var service = new BackgroundService(TimeSpan.FromSeconds(2));
    await Task.Delay(TimeSpan.FromSeconds(3));
}
catch (Exception ex)
{
    Console.WriteLine(ex);
    Console.ReadLine();
}

class BackgroundService: IAsyncDisposable
{
    public Task Completion { get; }

    private CancellationTokenSource _diposalCts = new();

    public BackgroundService(TimeSpan timeSpan)
    {
        this.Completion = Run(timeSpan);
    }

    public async ValueTask DisposeAsync()
    {
        _diposalCts.Cancel();
        try
        {
            await this.Completion;
        }
        finally
        {
            _diposalCts.Dispose();
        }
    }

    private async Task Run(TimeSpan timeSpan)
    {
        try
        {
            await Task.Delay(timeSpan, _diposalCts.Token);
            throw new InvalidOperationException("Boo!");
        }
        catch (OperationCanceledException)
        {
        }
    }
}

Alternatively, I can observe service.Completion explicitly in the client code (and ignore its exceptions inside DiposeAsync to avoid them being potentially thrown twice), like below:

try
{
    await using var service = new BackgroundService(TimeSpan.FromSeconds(2));
    await Task.Delay(TimeSpan.FromSeconds(3));
    await service.Completion;
}
catch (Exception ex)
{
    Console.WriteLine(ex);
    Console.ReadLine();
}

class BackgroundService: IAsyncDisposable
{
    public Task Completion { get; }

    private CancellationTokenSource _diposalCts = new();

    public BackgroundService(TimeSpan timeSpan)
    {
        this.Completion = Run(timeSpan);
    }

    public async ValueTask DisposeAsync()
    {
        _diposalCts.Cancel();
        try
        {
            await this.Completion;
        }
        catch
        {
            // the client should observe this.Completion
        }
        finally
        {
            _diposalCts.Dispose();
        }
    }

    private async Task Run(TimeSpan timeSpan)
    {
        try
        {
            await Task.Delay(timeSpan, _diposalCts.Token);
            throw new InvalidOperationException("Boo!");
        }
        catch (OperationCanceledException)
        {
        }
    }
}

Is there a concensus about which option is better?

For now, I've settled on a reusable helper class LongRunningAsyncDisposable ( here's a gist ), which allows:

  • to start a background task;
  • stop this task (via a cancellation token) by calling IAsyncDisposable.DisposeAsync at any time, in a thread-safe, concurrency-friendly way;
  • configure whether DisposeAsync should re-throw the task's exceptions ( DisposeAsync will await the task's completion either way, before doing a cleanup);
  • the task's status, result and exceptions can be observed at any time via LongRunningAsyncDisposable.Completion property;

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