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ÂOperation
s, 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.
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....
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);
….
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.
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.
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.