-1

Maybe I'm missing something obvious here. So in C# there are async/await features which we use on IO operations in order to free the thread while waiting for IO to finish.

The question is why do we have to do this and why framework can't do this automatically?

EDIT To be short what would be cool is we call

var a = File.ReadAllText

and it works exactly as if we called

var a = await File.ReadAllTextAsync

because .net framework would be responsible for calling all IO operations (which it receives from user code as synchronous code) asynchronously.

ren
  • 3,843
  • 9
  • 50
  • 95
  • Please show some concrete code to help us follow your thoughts, or add more details, it's pretty unclear what you're asking. – Corentin Pane Nov 06 '19 at 11:09
  • 1
    you might not want to do things asynchronously. – Tim Rutter Nov 06 '19 at 11:12
  • @Jeroen Mostert Why do I have to put async/await? – ren Nov 06 '19 at 11:14
  • @Tim Rutter What is the scenario when I don't want to free an unused thread? – ren Nov 06 '19 at 11:14
  • 1
    Because synchronous code is much simpler to write, compile and understand, which is why, by default, everything you write executes synchronously. It would have been possible to construct a language and a framework where everything was pervasively asynchronous, threads didn't exist and you didn't need keywords -- but .NET is not such a framework and C# is not such a language. – Jeroen Mostert Nov 06 '19 at 11:16
  • 1
    You don't have to await every asynchronous call. – André Sanson Nov 06 '19 at 11:17
  • Synchronous code is also more efficient most of the time. The overhead for managing threads outweighs the benefits unless your doing I/O or something equally as expensive. – Johnathan Barclay Nov 06 '19 at 11:22
  • @André Sanson Ah that one, ok. But it's kinda rare case, could have special keyword in this case only – ren Nov 06 '19 at 11:22
  • So for example you wonder why `Thread.Sleep(1000)` causes the thread to be blocked for 1000 msec, instead of been returned to the thread pool? What should happen after 1000 msec if the initial thread is occupied with calculating something else? Are you OK with the code that follows `Thread.Sleep(1000)` running in another thread? – Theodor Zoulias Nov 06 '19 at 11:24
  • Well this is considered a bad practise and the compiler throws a warning but it's up to you to decide and yes it's a rare case. – André Sanson Nov 06 '19 at 11:24
  • @Theodor Zoulias Thread.Sleep isn't IO, I'm asking specifically why IO operations can't be automatically async under the hood without us worrying about async/await. – ren Nov 06 '19 at 11:30
  • https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs4014 just for reference about calling async without await – André Sanson Nov 06 '19 at 11:32
  • Are you asking about IO async methods, or about IO blocking methods? In other words are you asking why you must add `await` before [`ReadAllTextAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.readalltextasync), or why the compiler doesn't automatically replace all calls to [`ReadAllText`](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.readalltext) with calls to `ReadAllTextAsync`? – Theodor Zoulias Nov 06 '19 at 11:37
  • @Theodor Zoulias About why the compiler doesn't automatically replace all calls to ReadAllText with calls to ReadAllTextAsync. I understand the case when you call async and forget, but most likely you will wait for the result anyway so the async part could be hidden away entirely. – ren Nov 06 '19 at 11:51
  • So you suggest that me as a library author of a method `ReadAllText` I should be able to release a newer version of my library having a `ReadAllText` method with a different signature (returns `Task` instead of `string`). And when the users of my library update the reference to the newer version with the incompatible API, the C# compiler should silently create an async state machine on every place that a call to `ReadAllText` is made inside the client code, and happily compile their code. Is this your suggestion? – Theodor Zoulias Nov 06 '19 at 12:01
  • Because if you need your code to be executed asynchronous you need to specify the keyword async and await on your method. Else other way around. – Indunil Withana Nov 06 '19 at 12:09
  • @Theodor Zoulias No, I'd rather suggest ReadAllText would stay as is, but it would be async nevertheless. The user would use it as before but somewhere in ReadAllText where it makes IO call the call would be async (framework would take care of that). So as users of a framework we wouldn't care about this issue at all. – ren Nov 06 '19 at 12:27
  • ok, probably I wouldn't call ReadAllText async anymore, because it wouldn't be, but it would achieve the same as async method would: free the IO waiting thread without the async/await stuff – ren Nov 06 '19 at 12:30
  • perhaps a better question would be: why do we need async methods? To free IO waiting thread, right? – ren Nov 06 '19 at 12:32
  • You cannot "free the thread waiting for I/O" unless you go async all the way. Note that the underlying framework code will *already* use asynchronous I/O calls on the OS level even in many cases where you are calling it synchronously (i.e. it will use a `ReadFile` call with an `LPOVERLAPPED` parameter) so everything's async except for the calling thread blocking, and the final bit of processing where it unblocks -- but of course that's *still* a blocking thread, and that's unavoidable unless the call site changes. What *exactly* the thread blocks on is not relevant, it still hurts scalability. – Jeroen Mostert Nov 06 '19 at 12:34
  • @Jeroen Mostert so what's the difference between var a= File.ReadAllText and var a = await File.ReadAllTextAsync ? – ren Nov 06 '19 at 12:40
  • I think that you should update your question by including your suggestion in details. As it is now it is intriguing but cannot be answered. A well-presented suggestion may be quite interesting actually. – Theodor Zoulias Nov 06 '19 at 12:41
  • The first blocks the calling thread, the second does not -- instead it schedules a continuation to be called when the operation finishes, and releases the calling thread to go do something else. That's the whole essential difference between async and sync code. Note that that actually has little to do with how the underlying operation is implemented, though for best results we obviously do want an OS mechanism where no threads are dedicated to individual operations either (as is the case with asynchronous I/O implemented through completion ports). – Jeroen Mostert Nov 06 '19 at 12:42
  • You may also wish to read Stephen Cleary's ["there is no thread"](https://blog.stephencleary.com/2013/11/there-is-no-thread.html), which explains this magic in more detail. Bottom line: you can't end up in the desired situation where there truly is no thread, unless you enable the caller to completely let go of its notions of thread-ness, which you can't do without rewriting the calling code (i.e. what `async` / `await` does under the covers). – Jeroen Mostert Nov 06 '19 at 12:50

1 Answers1

2

If these two lines were equivalent:

string s = await File.ReadAllTextAsync(); // Current syntax

string s = File.ReadAllText(); // Proposed syntax

...then we would no longer be able to create a Task without awaiting it immediately. So we couldn't do all these cool staff with Task.WhenAll, Task.WhenAny etc that allow concurrency. We would then need a different keyword to signify non-awaiting, like this:

var s = defer File.ReadAllText(); // s is Task<string>

Or infer it by the type of the return variable:

Task<string> s = File.ReadAllText(); // defer is inferred

Any method containing the line string s = File.ReadAllText(); would be now implicitly async, so it should return a Task. For consistency, and to make it easier for the developers, it should probably allowed to preserve the unwrapped types in the signature. Example of equivalent current and proposed syntax:

public async Task<string> GetData() => await File.ReadAllTextAsync(); // Current syntax

public string GetData() => File.ReadAllText(); // Proposed syntax

I am not sure how far you can go with this experiment. I guess that if async/await had been introduced along with the TPL library, you could go some miles before hitting an obstacle. But since TPL (2010) predates async/await (2012), there were already lots of APIs exposing the Task type, and lots of code using these APIs already that would be broken with the new syntax.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • The major problem with this experiment would be that it would simply render all existing code incompatible on the binary level. All your new code would suddenly become `async`, which no existing code could call into without being recompiled itself. Breaking the ecosystem on such a massive level would be a non-starter for any sensible language designer, no matter how cool asynchronous support is -- and working around this by having the runtime "silently" inject synchronous waiting at the boundaries opens up the can of worms that is sync over async. This would be a new language period. – Jeroen Mostert Nov 06 '19 at 13:43
  • I quess @Jeroen Mostert is right it would simply be a breaking change and so they decided what they decided – ren Nov 06 '19 at 14:24
  • 1
    @ren The reason wasn't that it would be a breaking change. It's that *there are use cases for both*. Sometimes you might actually want something to run synchronously and need to rely on that behaviour. – Gabriel Luci Nov 06 '19 at 14:42
  • @GabrielLuci can you give an example where synchronous code is mandatory? In other words an example where having the option between calling a sync and an async version of the same method, you must call the sync version by all means. – Theodor Zoulias Nov 06 '19 at 14:49
  • @JeroenMostert it would be cool that "async all the way" would be free, a gift by the compiler. The existing code would not be affected, since the libraries would continue exposing the classic synchronous APIs, enhanced with async versions whenever this made sense. Compiled old code could switch to the new libraries without problem. Old versions of the C# compiler could use the new libraries without problem. The new C# compiler would recognize the possibility of calling the async version of sync methods, create async state machines anywhere needed, and compile async-all-the-way programs! – Theodor Zoulias Nov 06 '19 at 14:58
  • Event handlers for one (i.e. `async void`), since awaiting async code actually returns from the method and you may not want to return until you know something has happened. I just [answered a question](https://stackoverflow.com/q/58718453/1202807) about that yesterday. – Gabriel Luci Nov 06 '19 at 15:00
  • I'm thinking specifically of the case where *you* write library code used by others. If your code automagically became async, existing callers could no longer use it -- remember, *that* code is not necessarily going to be recompiled (or at least you don't want to force that). To make this work, the compiler could compile your code both ways always (i.e. rename your methods to `Async` and provide a sync alternative), but now you end up with sync wrappers over async methods... – Jeroen Mostert Nov 06 '19 at 15:03
  • ...and you ideally want to leave the choice of what strategies apply there [to the caller, not the callee](https://devblogs.microsoft.com/pfxteam/should-i-expose-synchronous-wrappers-for-asynchronous-methods/). (The reverse strategy of exposing async over sync wrappers is not that great either, linked in the same article.) I'd say that as a gift, it would be more like a Trojan horse than anything else. I'd prefer languages that are pervasively async to save surprises. :-) – Jeroen Mostert Nov 06 '19 at 15:04
  • 1
    @GabrielLuci that's a good argument. Almost a show stopper. I can't think of a way around this, that doesn't break old code. – Theodor Zoulias Nov 06 '19 at 15:10
  • @JeroenMostert sync wrappers over async methods should be safe, if autogenerated by the compiler. The smart guys of the TPL team would ensure that no nasty deadlocks or other surprises would ever occur, unless it is mathematically impossible for this guarantee to be made. – Theodor Zoulias Nov 06 '19 at 15:17
  • 1
    Those smart guys haven't yet figured out how to automatically determine if `.ConfigureAwait(false)` or `.ConfigureAwait(true)` is the appropriate thing to do in all cases (since it depends on whether your synchronous code is thread-affine or not) so I wouldn't be so optimistic. :-P Yes, it is sort of an interesting thought experiment to see how much effort it would take for a working proof of concept of this, but at the same time it's hopefully obvious why they didn't go with this as the initial implementation. There's enough magic in `async` / `await` as it is. – Jeroen Mostert Nov 06 '19 at 15:20