May 1st, 2019

Async-Async: Consequences for parameters to parallel asynchronous calls

Last time, we learned about a feature known as Async-Async which makes asynchronous operations even more asynchronous by pretending that they started before they actually did. The introduction of Async-Async is intended to be transparent to both the client and the server, provided they were following the rules to begin with. Of course, if you weren’t following the rules, then you may notice some side effects.

From the client side, it means that you cannot mutate the parameters of an asynchronous operation until the operation completes. This was never permitted to begin with, but people sometimes got away with it because they “knew” that certain parameters were consumed before the initial method returned an asynchronous operation.

// Code in italics is wrong.

// Create three widgets in parallel.
var options = new WidgetOptions();

// Create a blue widget.
options.Color = Colors.Blue;
var task1 = Widget.CreateAsync(options);

// Create another blue widget.
var task2 = Widget.CreateAsync(options);

// Create a red widget.
options.Color = Colors.Red;
var task3 = Widget.CreateAsync(options);

// Wait for all the widgets to be created.
await Task.WhenAll(task1, task2, task3);

// Get the widgets.
var widget1 = task1.Result;
var widget2 = task2.Result;
var widget3 = task3.Result;

This code “knows” that the Widget.Create­Async method looks at the options.Color before it returns with an IAsync­Operation. It therefore “knows” that any changes to the options after Widget.Create­Async returns will not have any effect on the widget being created, so it goes ahead and reconfigures the options object so it can be used for the third widget.

This code does not work when Async-Async is enabled. The calls to Widget.Create­Async will return immediately with fake IAsync­Operations, while the real calls to Widget.Create­Async are still in progress. As we saw earlier, the result of the real call to Widget.Create­Async will be connected to the fake IAsync­Operation so that you get the result you want, but the timing has changed to improve performance. If the above code manages to change the options.Color to red before one of the first two real calls to Widget.Create­Async reads the options, then one or both of the first two widgets will end up red rather than blue.

This is basically a case of violating one of the basic ground rules for programming: You cannot change a parameter while the function call is in progress. It’s just that for asynchronous operations, the “in progress” extends all the way through to the completion of the asynchronous operation.

It’s fine to kick off multiple asynchronous operations. Just make sure they don’t interfere with each other.

// Create three widgets in parallel.
var options = new WidgetOptions();

// Create a blue widget.
options.Color = Colors.Blue;
var task1 = Widget.CreateAsync(options);

// Create another blue widget.
var task2 = Widget.CreateAsync(options);

// Create a red widget.
options = new WidgetOptions();
options.Color = Colors.Red;
var task3 = Widget.CreateAsync(options);

// Wait for all the widgets to be created.
await Task.WhenAll(task1, task2, task3);

// Get the widgets.
var widget1 = task1.Result;
var widget2 = task2.Result;
var widget3 = task3.Result;

This time, we create a new Widget­Options object for the final call to Widget.­Create­Async. That way, each call to Widget.Create­Async gets an options object that is stable for the duration of the call. It’s okay to share the options object among multiple calls (like we did for the first two blue widgets), but don’t change them while there is still an asynchronous operation that is using them.

Of course, once the operation completes, then you are welcome to do whatever you like to the options, since the operation isn’t using them any more.

// Create three widgets in series.
var options = new WidgetOptions();

// Create a blue widget.
options.Color = Colors.Blue;
var widget1 = await Widget.CreateAsync(options);

// Create another blue widget.
var widget2 = await Widget.CreateAsync(options);

// Create a red widget.
options.Color = Colors.Red;
var widget3 = await Widget.CreateAsync(options);

In this case, we created the widgets in series. We changed the options after awaiting the result of the operation, so we know that the operation is finished and it is safe to modify the options for a new call.

Next time, we’ll look at another consequence of Async-Async.

 

Topics
Other

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

5 comments

Discussion is closed. Login to edit/delete existing comments.

  • Yiannis Papadopoulos

    I faced the same problem in a C++-based runtime system. I ended up using annotation based on ownership of the object passed to the operation.
    The runtime was doing aggregation of async requests transparently (since some ended up being serialized and sent across the wire), so I had to manage very carefully and unambiguously what happens to objects passed to an operation.
    I went down the path of Uniqueness and Reference Immutability for Safe Parallelism....

    Read more
  • Tautvydas Žilys

    Whoa, this is surprising! Does that mean you can’t pass in place constructed HSTRING references, like so?

    ComPtr<IAsyncOperation<StorageFolder*>> operation;hr = storageFolderStatics->GetFolderFromPathAsync(HStringReference(“D:\\path\\to\\my\\\file.txt”).Get(), &operation);
    ….

    • Raymond ChenMicrosoft employee Author

      That’s okay, because the async operation will do a WindowsDuplicateString to extend the lifetime of the HSTRING, the same way it calls AddRef to extend the lifetime of any COM objects.

  • Henrik Andersson

    Huh. I would’ve expected the opposite issue. The parameter being changed after perfoming the async call and the async method being able to notice the change becuase it was slower than the caller. But with async-async you’d be capturing a serialized copy, not a reference, to the argument object.

    • Raymond ChenMicrosoft employee Author

      In many cases, you really do want a reference. For example, serializing a random access stream would be wrong (writes would be lost). Or the parameter could have references to unserializable objects, like a network connection. Or it could be part of a complex web of objects, possibly with circular references, like an audio graph.