3 ms·
the use of async and await is a design decision that requires knowledge of the program's logic and desired behavior. It's not just a matter of compiler optimiza
by peheje 3y ago
the use of async and await is a design decision that requires knowledge of the program's logic and desired behavior. It's not just a matter of compiler optimization, and that's why the compiler can't automatically figure out where to use these keywords.
Suppose we have a service where users place orders, and we need to:
1. Save the order.
2. Deduct items from inventory.
3. Send a confirmation email.
If we perform these operations asynchronously but sequentially using `await`:
```csharp
public async Task PlaceOrder(Order order)
{
await SaveOrder(order);
await DeductItems(order);
await SendConfirmationEmail(order);
}
```
They happen in the correct order, which is important.
But, if the compiler removed the `await`, causing all three operations to start at the same time:
```csharp
public async Task PlaceOrder(Order order)
{
SaveOrder(order);
DeductItems(order);
SendConfirmationEmail(order);
}
```
This could lead to issues like sending the confirmation email before the order is saved, showing the importance of `await`. The compiler cannot optimize this without understanding the business logic.
- ikekkdcjkfke 3y agoI was thinking more in terms of making good old 'blocking' calls and that the compiler can 'taskify' those old blocking calls to let the thread do some other task instead of waiting on the 'blocking'
- notamy 3y agoSometimes you want to spawn a [future/task/promise/...] and not immediately await it, but instead pass it around. Suppose something like: should_be_future = async_function() should_execute = async_function() # <--- await goes here handle_future(should_be_future) handle_result(should_execute) While the compiler could figure out where to insert awaits automatically, it adds cognitive overhead for the developer -- suddenly the same way of calling a function can result in either a [future/task/promise/...] or the expected type.
- gpderetta 3y agoThen you should annotate the tricky diverging call site with async (see my other comment), instead of making it implicit and requiring annotation of the common expected case: should_be_future = async async_function() should_execute = async_function() # <--- await goes here handle_future(should_be_future) handle_result(should_execute)
- ikekkdcjkfke 3y agoI just don't run into many cases where I'm not immediately awaiting a task, so instead of littering the code base with async await and their return types all the way up, there could be a lighweight version that free's the thread up when waiting on a blocking call (I'm assuming the main reason we want to use async in C# is to free the thread up to do other tasks). NotAPromiseObject ReadCsv(FileFile file){ string content = waitfor File.ReadAsync(file) ...
- gpderetta 3y agoYou have it backward. The compiler should implicitly add the awaits for waitable objects, unless an operation is explicitly async. So you would write (in pseudocode): Task PlaceOrder(Order order) { SaveOrder(order); DeductItems(order); SendConfirmationEmail(order); } And the compiler will implicitly await all three operations (and ideally infer that your function is async). If you want to overlap computation, you avoid the implicit wait with async: Task PlaceOrders(Order order1, Order order2) { let done1 = async PlaceOrder(order1); // async prevents implicit waiting let done2 = async PlaceOrder(order2); wait_all(done1, done2); // wait_all is also async and implicitly awaited. Ideally this should happen automatically for all unwaited and not returned futures at end of scope } This allows being polymorphic on the async-ness of the function (pardon the pseudo c++): template<Range R, callable<R::value_type> F > void for_each(R range, F f) { for (auto x : range) f(x); // f(x) is awaited if f is async and for_each itself becomes async. } edit: sometimes it is important that no preemption happens in a region [1], so some scoped marker (atomic { ... } for example) would case a compilation error if an await would be introduced automatically. edit2: and of course you should be able to use async even if the called function is a boring old blocking one. The runtime can spawn background task (or better yet use work-stealing) to run it. [1] personally I think that atomicity guarantees should be about data, not code, but whatever.
- peheje 3y agoI appreciate the original comment by ikekkdcjkfke, and your elaboration, gpderetta. However, I perceive a distinction between compiler optimization and proposing a fundamentally different model for handling asynchronicity. It seems to me that what you're suggesting deviates significantly from the existing C# model. Transitioning to this model wouldn't be a simple matter of compiler optimization—it would be a major breaking change that would require rethinking many aspects of the language. Please don't misunderstand me; I'm not dismissing your proposal outright. However, it's essential to consider the potential trade-offs such as control flow management and error handling strategies. These are currently well-addressed by the async mechanism in C#. Regarding polymorphic async-ness, I acknowledge this as a minor limitation within the current C# model. However, a common practice involves returning Task or Task<T>, even when no async calls are involved. Moreover, I find Kotlin's approach to handling this through inlining of suspend functions quite intriguing. You can check it out here: https://kotlinlang.org/docs/kotlin-tips.html#the-suspend-and-inline-mystery https://kotlinlang.org/docs/kotlin-tips.html#the-suspend-and... In closing, it would be beneficial to our discussion if we could clarify whether we're contemplating an optimization within the current C# framework or proposing a fundamentally different approach to asynchronicity.