Using an object in a continuation, ensuring it isn't disposed before you need it

Viewed 147

In a Xamarin.iOS app, for various reasons we can't use async/await. Therefore, we are using Task.ContinueWith. The problem is that objects we rely on in the continuation are disposed before the continuation gets called.

using (UIImage image = // Somehow get the image)
{
    DoSomethingWith(image);
}

void DoSomethingWithImage(UIImage image)
{
    UploadImageAsync(image).ContinueWith(t =>
    {
        // Image has been disposed by this point.
        DisplayResult(t.Result, image);
    });
}

I'm interested to get ideas on how to manage the above case. Options I've considered:

  • Remove the using block, and let image go out of scope. This would be a bad idea, since UIImage could be quite large.
  • Remove the using, and call Dispose() in the continuation. This feels error-prone, as whereever code like this exists, you're disposing the object far from where it was created.
  • Wrap the call to DoSomethingWithImage() in a Task.Run(() => DoSomethingWithImage(image)).ContinueWith(t => image.Dispose()). So you have a continuation within a continuation. This feels over-complicated for this scenario.
  • Any other options I'm overlooking?
1 Answers

First of all this a good question. Based on the mentioned options I think you have to change angel.

In your current implementation the scope of the UIImage is closely related to the DoSomethingWith method. In other words the retrieved image is only used inside that method. I can see two separate way, how this can evolve:

  1. Introduce DoSomethingElseWith method
  2. Get rid of the DoSomethingWith and "inline" the UploadImage and DisplayResult

In either way you would end up to have more than one methods inside in your using block. Which will indicate that the cleanup of the UIImage is none of the methods responsibility. In normal cases you would do this by awaiting the last one and let the using block do its job, right? If you can't await it then the most obvious choice for continuation is make use of ContinueWith. (Others: TaskFactory.ContinueWhenAll, TaskFactory.ContinueWhenAny, Task.WhenAll, Task.WhenAny, for further info).

UIImage image = RetrieveImage();
var uploadJob = UploadImage(image);
var displayJob = uploadJob.ContinueWith((t => DisplayResult(t.Result, image)).Unwrap();
var disposeJob = displayJob.ContinueWith(_ => image.Dispose());

Alternatively you can use some short of signalling mechanism (sync primitive) to indicate when the last async operation has finished, like AutoResetEvent, CountdownEvent, TaskComplitionSource.

Your third option could be to use TPL Dataflow. With that you can pipe together the small pieces and use Completion to initiate the dispose as your last action.

Related