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
CreateAsync method looks at the
options.Color before it returns with an
IAsyncOperation. It therefore “knows” that any changes to the
CreateAsync 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
CreateAsync will return immediately with fake
IAsyncOperations, while the real calls to
CreateAsync are still in progress. As we saw earlier, the result of the real call to
CreateAsync will be connected to the fake
IAsyncOperation so that you get the result you want, but the timing has changed to improve performance. If the above code manages to change the
Color to red before one of the first two real calls to
CreateAsync 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
WidgetOptions object for the final call to
Widget.CreateAsync. That way, each call to
CreateAsync 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.
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.
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.
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. If you express ownership through the type system, then you can avoid the kinks of async operations rather painlessly.
It was roughly like this:
– The caller controls the ownership of an object passed as an argument to an operation.
– For any regular object, the caller is guaranteed that all operations have no side-effects on the objects passed to the operation.
– The callee controls the constness its parameters; an argument can be mutated if it is mutable (i.e., non-const), but the caller shall not see any mutation on the argument.
– If the operation is sync: if the callee accepts an immutable object (i.e., const reference), then a copy is not needed. If the callee accepts a mutable object (i.e., reference, or by value) then a copy needs to be made.
– If the operation is async: a copy is always made, unless the caller relinquishes ownership (i.e., moves the object) or temporarily says that the object will be immutable until the next synchronization point.
– Async-async operations (i.e., aggregated async operations that are invoked later than expected): they’re just regular async operations.
Reference: STAPL-RTS: An Application Driven Runtime System