Reputation: 5779
I've found myself growing the habit of keeping the Task
objects well beyond their completion as result containers.
So far I haven't identified any drawbacks and I find the code clearer than having separate variables to store the result after the tasks are completed.
A couple usage examples follow. Although I don't think it is really relevant, they have been though as part of View Models in MVVM applications. (Please note that this is not actual working code, I only try to outline the pattern.)
Early initialization
A SlowClient
is some class requiring a few seconds to connect to a WCF or REST
service. Thus, I initialize it as soon as possible through a task. When the client is required, the task is awaited, yielding the initialized SlowClient
(either immediately if the task is done or after waiting for its completion).
So I'd do something like this:
public class SomeViewModel
{
private Task<SlowClient> _slowClientInitializerTask;
public SomeViewModel()
{
_slowClientInitializerTask = Task.Run(() => CreateAndInitSlowClient());
}
private SlowClient CreateAndInitSlowClient()
{
// SlowClient instantiation and initialization, taking a few seconds...
}
// Task result consumer example
void OnSomeCommandExecuted(object parameter)
{
try
{
var client = await _slowClientInitializerTask;
// do something with the client
} catch {
// may re-create the task if the client ceases to be valid
}
}
}
As exemplified by OnSomeCommandExecuted
, every method using SlowClient
will simply do:
var client = await _slowClientInitializerTask;
If for some reason the result ceases to be valid (the
connection to the service being dropped or what not), I'd just run a new
task and assign it to _slowClientInitializerTask
-- just like in the
code shown in the constructor.
The alternative to this pattern would be to create some extra _slowClient
variable that is updated once the task is completed, thus requiring a check every time it is going to be used. For instance:
if (_slowClient == null)
_slowClient = await _slowClientInitializerTask;
I see no benefit, only increased complexity.
Background workers
A more complex example uses tasks to process images, creating a new file containing a re-sized image. A report including these images must be generated; it accesses the image files through their paths and it must use the re-sized versions whenever possible -- if it is not, then the original images are used instead. Thus, I need to be able map the path of the original images to their re-sized versions.
// Key: original image path; Value: task returning the re-sized image path
private Dictionary<string, Task<string>> _resizeTasks;
// Costly operation => Want to execute it asynchronously
private string ResizeImage(string originalImagePath)
{
// returns the path of a temporary resized image file
}
// Command execution handler for instance => Launches image resize on background
void OnAddImageExecuted(object parameter)
{
string path = parameter as string;
if (!_resizeTasks.Keys.Contains(path))
_resizeTasks[path] = Task.Run(() => ResizeImage(path));
}
// Generates a report consuming the images => Requires the result of the tasks
void OnGenerateReportExecuted(object parameter)
{
try {
foreach (var faulted in from entry in _resizeTasks
where entry.Value.IsFaulted select entry.Key)
_resizeTasks[path] = Task.Run(() => ResizeImage(path)); // Retry
await Task.WhenAll(_resizeTasks.Values); // Wait for completion
} catch { } // Ignore exceptions thrown by tasks (such as I/O exceptions)
var imagePaths = _resizeTasks[path].Select(entry =>
entry.Value.Status == TaskStatus.RanToCompletion ?
entry.Value.Result : entry.Key);
// generate the report requiring the image paths
}
The actual implementation uses a ConcurrentDictionary
since the addition on images is executed asynchronously. Moreover, images can be removed and added again so there is a separate list for the current added images, and the _resizeTasks
also serves as a cache of previously re-sized images.
Task disposal is not the topic here, since I could dispose them later and anyways, it seems not necessary in these cases, as stated in the Do I need to dispose of Tasks? post from Parallel Programming with .NET MSDN Blog:
No. Don’t bother disposing of your tasks, not unless performance or scalability testing reveals that you need to dispose of them based on your usage patterns in order to meet your performance goals. If you do find a need to dispose of them, only do so when it’s easy to do so, namely when you already have a point in your code where you’re 100% sure that they’re completed and that no one else is using them.
My concerns are the following:
Upvotes: 4
Views: 112
Reputation: 116558
* Another drawback is that if you await
a faulted task multiple times you would get the exception thrown each time. This may be problematic, but it depends on your specific exception handling.
Upvotes: 3