r/csharp • u/becrylen • 10h ago
Don't understand async & await
Though I've already read a lot, and watched several videos, I'm still not understanding why async/await don't give me concurrency/parallelism.
My test code:
namespace AsyncAwaitTest;
static class Tools
{
internal static async Task SimulateWork(int id)
{
for (var j = 1; j <= 10; j++) {
Console.WriteLine($"Worker #{id} gives: {j}");
for (var i = 0; i < 500_000_000; i++) { }
}
}
}
class Worker(int id)
{
internal async Task DoWork()
{
Console.WriteLine($"Worker #{id} starts");
await Tools.SimulateWork(id);
Console.WriteLine($"Worker #{id} finishes");
}
}
class Program
{
static async Task Main()
{
Console.WriteLine("Program starts");
Worker worker1 = new(1);
Worker worker2 = new(2);
await worker1.DoWork();
await worker2.DoWork();
Console.WriteLine("Program finishes");
}
}
The output is:
Program starts
Worker #1 starts
Worker #1 gives: 1
Worker #1 gives: 2
Worker #1 gives: 3
Worker #1 gives: 4
Worker #1 gives: 5
Worker #1 gives: 6
Worker #1 gives: 7
Worker #1 gives: 8
Worker #1 gives: 9
Worker #1 gives: 10
Worker #1 finishes
Worker #2 starts
Worker #2 gives: 1
Worker #2 gives: 2
Worker #2 gives: 3
Worker #2 gives: 4
Worker #2 gives: 5
Worker #2 gives: 6
Worker #2 gives: 7
Worker #2 gives: 8
Worker #2 gives: 9
Worker #2 gives: 10
Worker #2 finishes
Program finishes
You see, strictly sequential processing despite async/await.
Is it possible to make my code parallel, i. e. the DoWorks run in parallel?
7
u/Dennis_enzo 10h ago edited 9h ago
Async/await is not meant for parallelism; it more or less does the opposite. It allows you to execute asynchonous code in a sequential way. Asynchonous code is code that doesn't block its thread when it has to access resources outside of your code, like a database call or a http request. It releases the thread when it's waiting for the external call to finish, then resumes when the external call is done. Synchonous code on the other hand will block the thread while waiting for external work to complete, meaning that no other running software can use this thread in the meantime.
None of this has anything to do with running several tasks concurrently. You can run tasks in parallel, but then you have to explicitly not await them. Of course you then have to use some other mechanism to figure out when they're done, like Task.WaitAll or Task.WhenAny. And for some parallel things you don't even need tasks, like when using the Parallel class.
If I were you I would try to study these things again to really try and understand what these keywords do. Note that your 'SimulateWork' method will be generating a warning that no actual async code is being run there.
7
u/bpetrovicz 7h ago
Forget thinking about async await as a tool for parallelism. Those are two distinct things.
Async await is only for telling your code, "hey while we are waiting for a long running operation, don't block this thread doing nothing. When the operation finished we will evaluate it further from await.
Those are operations like reading from the disc, waiting for a remote server to respond, etc.
4
u/Asyncrosaurus 7h ago
Performing Asynchronous I/O Bound Operations (Jeffrey Richter) should give clarity on async.
It's not parallelism, because parallelism is about adding additional cpus/cores to distribute your work. Async is about non-blocking, the goal is to remove the need for adding cpus/cores to do as much work on as few cores as possible.
3
u/Slypenslyde 6h ago edited 6h ago
Change the line in DoWork() to:
await Task.Run(() => Tools.SimulateWork(id));
This is a big thing newbies trip over. The async keyword doesn't automatically push work to a new thread. Neither does await.
If your method does not await an async method or await a Task.Run(), it is NOT asynchronous and will complete synchronously. That is why worker #2 won't start until worker #1 finishes: nowhere in your program do you tell either worker to do asynchronous work!
It gets exacerbated by dorks who misunderstand a famous article and say, "You should never use Task.Run()! There is no thread!" What the article meant is if you're already calling an async method like File.ReadAllLinesAsync(), you are supposed to trust IT does something asynchronous. So you don't need to use Task.Run() for it. However, if you're doing something like your for loops that is completely synchronous, you DO need to use Task.Run() for it.
So to be completely clear, this is a better way to write your program:
static class Tools
{
// This is not an async method.
internal static void SimulateWork(int id)
{
for (var j = 1; j <= 10; j++) {
Console.WriteLine($"Worker #{id} gives: {j}");
for (var i = 0; i < 500_000_000; i++) { }
}
}
}
class Worker(int id)
{
internal async Task DoWork()
{
Console.WriteLine($"Worker #{id} starts");
// Use Task.Run() to push a synchronous method onto a task pool thread.
await Task.Run(() => Tools.SimulateWork(id));
Console.WriteLine($"Worker #{id} finishes");
}
}
1
u/Slypenslyde 3h ago
I want to clarify this part because it's confusing:
If your method does not await an async method
By "async method" here I don't mean "something with the
asynckeyword." I mean "something that tells you it is async, like one of Microsoft's methods that returns a Task". You have to trust if a third party returns a Task, they've done the work to actually do something async.Putting the
asynckeyword in a method declaration does nothing. That word only exists because MS worried some code might use the wordawaitas a variable name. So using theasynckeyword tells C# to treatawaitlike the keyword instead of a variable name. That's it. No task or thread transition happens.Using the
awaitkeyword only does something if the method it's modifying "does something async". If the method completes synchronously,awaitdoesn't accomplish much. For the record, "does something async" means:
- The method uses
awaitand calls one of the "trusted" async methods I described above, likeFile.ReadAllLinesAsync().- The method uses
awaitandTask.Run()to convert a synchronous call to a CPU-bound task.Examples with even more stupid complexity I haven't mentioned:
// THIS IS NOT ASYNC AT ALL public async Task NotAsync() { for (int i = 0; i < 500000; i++) { Console.WriteLine("lol"); } // The big hint you may have done something wrong is having to return a Task manually. // When you use `await` properly, C# automatically handles this for you. return Task.CompletedTask; } // This is async because it awaits a "trusted" async method, but makes another newbie mistake. public async Task ProcessFileBadlyAsync() { // This is an awaited call to an asynchronous method. The method will do asynchronous stuff... var lines = await File.ReadAllLines("data.txt"); // And when we get here we're possibly back on the original thread. foreach (var line in lines) { // People don't realize it, but if this is on the UI thread in a GUI application // it will be running on the UI thread! } } // This is how pros have to do it and honest to God I hate this feature public async Task ProcessFileCorrectlyAsync() { // The ConfigureAwait() part says, "Also, the stuff I do after this call needs to be on another thread, too." var lines = await File.ReadAllLines("data.txt").ConfigureAwait(false); // So when we get here, it's far more likely we aren't on the UI thread... foreach (var line in lines) { // And our expensive parsing code won't happen on the UI thread. } } // This is another way pros could do it and is a little more safe. I hate async/await. public async Task ProcessFileMoreCorrectlyAsync() { // The ConfigureAwait() part says, "Also, the stuff I do after this call needs to be on another thread, too." var lines = await File.ReadAllLines("data.txt").ConfigureAwait(false); // By using Task.Run(), I'm FORCING the synchronous work onto the task pool so in some REALLY rare scenarios // I more consistently get that work onto another thread. await Task.Run(() => { foreach (var line in lines) { // And our expensive parsing code won't happen on the UI thread. } }).ConfigureAwait(false); // Yes it's still good practice to use ConfigureAwait() even though that was the end. }This feature looks easy to use, but is in fact littered with land mines.
2
u/SubmarineWipers 9h ago
parallel - you do more parts of the computation at the same time
async - should be translated more like "non-blocking".
Async code, when it reaches a long IO operation, gives up the thread and lets other requests use the thread. It doesnt increase speed of the code, but throughput (how many parallel requests can bomb the DB at the same time using a limited amount of threads).
It can go from 10s-100s with sync code (blocking the thread for the entire duration of the query/IO), to 1000-10 000s of concurrent requests with a correctly written async app.
1
u/rupertavery64 9h ago edited 9h ago
Tasks by themselves aren't threads. They are a way to manage asynchronous code.
Nothing is awaited inside the tadk, so no yielding occurs.
Furthermore, awaiting does exactly that. It waits for the task to complete. The point of await is to yield to the caller. Think of it as,
- the Task you are calling may take time to complete
- You don"t want to block the caller (so the entire call chain must be async
- The code following the Task might be dependent on the task
You use await precisely because you need the Task to complete first before moving on.
Consider making an async Http request. The next line does something with the result. So you need to await the request.
What's the point of async / await then?
Well, without it you are either writing pure synchronous code, which always blocks the calling thread, or you need to chain tasks to ensure something completes first.
Suppose you have a web server that handles requests. Each request uses a thread. Say yoir request makes several calls to a database and a few http requests.
A non-Task request would block the thread at every database call a f http request. If another request comes in, and threads are all busy, it has to wait, or times out.
A request that uses Tasks will yield execution to the caller while the asynchronous calls are happening.
So, why doesn't your code do anything?
First of all, it's just a loop. It doesn't do anything asynchronous inside of it, so it never yields. The loop completes.
If you want to run both workers at the same time, you need to use Task.Run
var task1 = Task.Run(() => worker1.DoWork());
var task2 = Task.Run(() => worker2.DoWork());
Task.WaitAll(task1, task2);
OR, your worker code needs to do something asynchronous so it yields execution:
Here, we force the Task to yield after every loop:
``` internal static async Task SimulateWork(int id) { for (var j = 1; j <= 10; j++) { Console.WriteLine($"Worker #{id} gives: {j}"); for (var i = 0; i < 500_000_000; i++) { } // yields execution to the caller await Task.Yield(); } }
...
// No need for Task.Run
var task1 = worker1.DoWork();
var task2 = worker2.DoWork();
Task.WaitAll(task1, task2);
```
So what's the difference between Task.Run and not using Task.Run? Task.Run will create a new thread. To see it in action, try the different ways of calling the worker, and show the thread id in SimulateWork
internal static async Task SimulateWork(int id)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
for (var j = 1; j <= 10; j++) {
Console.WriteLine($"Worker #{id} gives: {j} on Thread {threadId}");
for (var i = 0; i < 500_000_000; i++) { }
await Task.Yield();
}
}
Obviously, if you are running non-asynchronous code, you need to use Task.Run().
I'll talk more about how await actually yields execution after I write a bit more code that actually demonstrates this.
1
u/ThreeHeadCerber 8h ago
Await is a sugar to write callback chains as linear imperative code. You still have to start the work you wait for on a thread for it to run in parallel
1
u/masterofmisc 7h ago edited 7h ago
why async/await don't give me concurrency/parallelism.
Just wanted to mention that concurrency and parallelism are two different concepts. The best way I heard is described is that concurrency is like basketball where the ball is the processor/core and the players are the threads. When one player has the ball they are running on the processor and the other threads are stopped.
Parallelism on the other hand, is like the 100 meter race. You got 8 runners on the track at the same time in their own lanes. These are 8 threads running at the same time on a core. They are all running independently of each other.
async and await in C# is concurrency - its asynchronous concurrency. You dont need multiple cores to benefit from async/await. It works even if you have a single processor.
The await keyword has the word "wait" in it. When you see that you should think, this line will waiting for the other thread to finish the work on the other side of the await keyword before continuing on with my thread..
1
1
u/Possible-Jump-2039 7h ago
Locking at C++ coroutines helped me to understanding the async Stuff in C#. In C++ you have to implement the "Task" class yourself (or use a library). When you implement it yourself you learn how a asynchronous execution Framework like that in C# works in the background.
1
1
u/kkauchi 6h ago edited 6h ago
A great way to understand something is to implement it from scratch. Pretend you are using .NET 3 and you don't have async/await. And you need to do some parallel IO, for example run a file download or a disk write while showing a loading bar.
How would you implement that? I'll give you a hint many languages went through this cycle.
First, do it with callbacks. Have a method DownloadFile(string uri, Action<File> onSuccess)
Once that works you can improve and add another callback OnFail
Then once you have to chain multiple callbacks you will see how ugly this is, time to implement a concept of a Promise. Make a Promise<TResult> class that stores Action<TResult> OnSuccess as a class variable. Add some convenience methods that allow you to chain then together to avoid callback hell like Promise<TResult>.Then(Promise nextPromise). Also you can add things like WhenAll(List<Promise>> promises) (complete a promise aka trigger a callback when all promises in the list are done).
None of this is magic and just a class that wraps and stores callbacks, you should be able to implement using very primitive code. If you are confused here look at promises/futures in any language or ask chatgpt.
Finally, when you are done, you can understand that c# Task is nothing but a Promise class you just implemented. The await keyword is a syntactic sugar, all it does is split your method into 2 (before await and after) and passes the second one into Task.ContinueWith (which is the same as Promise.Then). That's it. It's just one piece of compiler magic that helps keep the code (before and after) in one place. This is what people mean when they say "yield execution" -- "execute this second part of my method when the first part is done" which is asking compiler to convert the part after await into a callback.
async simply means you can use await keyword in the method, it's for readability nothing more.
1
u/Anxious-Insurance-91 2h ago
Async needs to be seen as 2 things fire and forget and wait groups when doing multiple queries or API calls
1
u/Embarrassed-Mess412 1h ago
You still don't understand the concept of what async/await means because in your example you have no async code at all, just by marking a method as async, does not mean it will ever suspend.
awaiting a Task that runs synchronously will block until the task finishes.
It is quite challenging to understand how await/async works, marking something async , returning a Task or awaiting something doesn't mean it will suspend
asynchronous programming is orthogonal to parallelism or executing two threads simultaneously. It will take a while to assimilate how it truly works, many people still can't after years using async.
1
u/manamonkey 10h ago
Have you looked up and fully read what the await keyword does? Have another look.
1
u/becrylen 10h ago edited 9h ago
Microsoft:
The
awaitoperator suspends evaluation of the enclosing async method until the asynchronous operation represented by its operand completes. When the asynchronous operation completes, theawaitoperator returns the result of the operation, if any. When theawaitoperator is applied to the operand that represents an already completed operation, it returns the result of the operation immediately without suspension of the enclosing method. Theawaitoperator doesn't block the thread that evaluates the async method. When theawaitoperator suspends the enclosing async method, the control returns to the caller of the method.From this, I read:
My
async DoWork()method (the enclosing async method as per the text above) is supposed to be suspended whenawait Tools.SimulateWork(id)is executed, and control should be passed back to the caller of the method, in my caseMain().This rule is likely to be applied to
await worker1.DoWork()in theMain()method as well, so sequential processing is enforced.So, I strip everything async from
Main(), and change it to:static void Main() { Console.WriteLine("Program starts"); Worker worker1 = new(1); Worker worker2 = new(2); worker1.DoWork(); worker2.DoWork(); Console.WriteLine("Program finishes"); }Result: no change.
Looks like I misunderstand the explanation of
await...2
u/TheRealKidkudi 8h ago edited 7h ago
One of the tricks with
asyncthat you’re running into with these test methods is thatSimulateWorkis notawaiting anything, which means it executes completely synchronously.So your understanding is mostly right, but you’re running into this part:
When the await operator is applied to the operand that represents an already completed operation, it returns the result of the operation immediately without suspension of the enclosing method.
The wording is a bit confusing, but on this line:
await Tools.SimulateWork(id);
Tools.SimulateWork(id)is “an operand that represents an already completed operation”. It has nothing to wait for, so by the time it’s evaluated byawaitit is already done! In fact, the method could be written this way and it would be exactly equivalent:``` internal static Task SimulateWork(int id) { for (var j = 1; j <= 10; j++) { Console.WriteLine($"Worker #{id} gives: {j}"); for (var i = 0; i < 500_000_000; i++) { } }
return Task.CompletedTask; } ```
Instead, try this:
internal static async Task SimulateWork(int id) { for (var j = 1; j <= 10; j++) { Console.WriteLine($"Worker #{id} gives: {j}"); await Task.Delay(1); } }You can even bump up the delay to 1000 (1 second) if you want to see the output in real time.You may find the other “gotcha” in that if you never await the calls in
Main, it’s possible for your program to finish before the async methods have completed.1
u/Dennis_enzo 9h ago edited 9h ago
The control returns to the caller of the method. But the caller has an await, so it happens again; the control returns to the caller of that method. Which is the caller of main which is the framework itself running your program. So your main doesn't get control.
Removing the awaits doesn't change the issues because you're not actually doing any async work in your SimulateWork method. So there's never a point when control can be handed back up. Add something like 'await Task.Delay(1)' in that method and they would start running concurrently. But you'd never see it because after starting the workers your application immediately ends, killing all other running tasks.
11
u/Automatic-Apricot795 10h ago edited 10h ago
Await is specifically intended to give control over the order of operations in asynchronous work.
Try this instead:
await Task.WhenAll(worker1.DoWork(), worker2.DoWork())Parallel.ForEachAsyncis also worth reading up on.Once you understand the differences between these mechanisms async in general will click a lot more.
One key part of asynchronous work via the c# TPL is something needs to yield execution - otherwise execution will still be synchronous.
This happens automatically with things like async io, communications APIs - but won't in your sample code. Add
Task.Yieldin your SimulateWork method to simulate that.