In C# i always wondered why they couldn't hide the async/await logic for most cases. I never need to fire off two IO futures at the same time, so just make the thread do other stuff if i'm waiting for IO feedback, don't make me type out async/await in all impacted functions, let the compiler figure out when it can process other stuff
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`:
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.
You 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):
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.
I 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...
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.
I'm not suggesting that C# makes a change, it is probably too late (although you could easily implement both models), I was just describing my preferred semantics (well, I prefer stackfull coroutines, but that's another story). I don't think it deviates much from the existing semantics, the minimum change is adding awaits automatically for any call to awaitable functions and requiring async annotations otherwise. This is a relatively minor change.
I don't think that the async/sync division is a minor thing, but thanks for the Kotlin reference, I'll take a look. Before watching the video I guess that they implement 'stackfull-like' behaviour in otherwise stackless coroutines by force-inlining any HOF so that the coroutine is again flattened. If that's the case, that's great! What happens if inlining can't happen? They reject the code or convert to stackfull coroutines? That's for me as always been the holy grail: stackfull semantics that optimize to stackless (i.e. bounded stack usage) when possible (i.e. when the compler can see all possible yield points).
I have been trying to figure out (in C++) the subset of the language such as the optimization is always guaranteed (at the very least you need first class and explicit continuations so that the compiler can track them and inlining must be possible). Possibly Kotlin has cracked it.
To clarify what I was trying to argue for, if one is using the .net async Task system only for the purpose to help the thread pool (always immediately awaiting an async method) then the responsibility should bear more on the .net framework, instead of spilling async/await Task<> into code bases. I don't know what a solution looks like, but maybe just threads are too expensive in .net
I 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'
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.
Then 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:
I 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).