Is it safe to use ContinueWith as a “finally” oper

2019-06-22 09:58发布

问题:

Consider a piece of code like:

private Task<string> Download()
{
    var wc = new WebClient();
    Task<string> backgroundDownload = wc.DownloadStringTaskAsync(this.Uri);
    // Make sure the WebClient is disposed no matter what.
    backgroundDownload.ContinueWith((downloadTask) => { wc.Dispose(); });
    return backgroundDownload;
}

Can I be certain that the WebClient.Dispose() call occurs and that any exception produced is rethrown to the caller as if there was no call to ContinueWith?

Can clients observe this ContinueWith? (e.g. will later calls to ContinueWith remove the Dispose call?)

回答1:

With the code that you have you can be certain that the continuation will be fired regardless of whether or not the code completed successfully, was cancelled, or throws an exception.

The one potential problem with the solution that you have is that other continuations can potentially run, before, during, or after the web client is disposed. If you don't have a problem with other continuations running before this cleanup runs then what you have is fine. If that's a problem then you'll need to return the continuation, not the original task, but you'll also need to propagate the result (and exceptions/cancellation) correctly. The use of async makes all of this way easier:

private async Task<string> Download()
{
    using(var wc = new WebClient())
      return await wc.DownloadStringTaskAsync(this.Uri);
}


回答2:

First of all, the continuation will be performed even if a regular exception occurred. However it is less likely to run than a regular finally block in the eventuality of exceptional conditions such as an OutOfMemoryException.

Now I would not try to dispose the webclient. Remember that disposing is an optimization, because native resources will be disposed by the finalizer anyway. The only reason we are disposing is that a finalizer is expensive because it triggers a second GC pass.

But in the order to perform your optimization the system may have to create new threads. Besides you may be prolongating the lifetime of your webclient a lot if the threadpool is filled with long running tasks.

Basically, you have to choose the lesser of two evils and I am not convinced that one less GC run is worth what you're doing. You should consider this decision in the context of your application.