C
C#14mo ago
joren

❔ Clarification on Async and Task.Run

So I've been trying out the Async and Task.Run, now I need some clarification about the two and their differences. Now Async is basically one worker, that shifts its attention from Task to Task while another task is being processed (without needing workers attention). Now I read that C# achieve this by using the Task.Scheduler, which has a thread pool and what not. Now Task.Run would be parallelism in C#, it creates a thread with the function you pass to it. Now, granted the above information is correct, Async using a thread pool wouldnt that be considered utilizing parallelism?
130 Replies
joren
jorenOP14mo ago
or does async Task<T> typically only utilize one thread and just juggle the different tasks as effieciently as possible in one thread?
Pobiega
Pobiega14mo ago
The above is partially correct. Task.Run uses the same threadpool as the rest of the TPL (Task Parallelization Library) the difference is that Task.Run always uses a thread from the pool, while await x has no such guarantees, it might just run on the same thread that scheduled it.
joren
jorenOP14mo ago
is Task.Run the only way to create a thread, without calling the platforms API directly of course?
Pobiega
Pobiega14mo ago
I can very heavily recommend https://www.youtube.com/watch?v=8lUs9ukVrFY by our very own mtreit
C# Community Discord
YouTube
Solution1: Threads, Tasks and more with Mike Treit
This presentation recaps some of the basics of writing code that makes use of threads, tasks and asynchronous features in C#. It is a modified version of a talk the author has given to his team at Microsoft. This is an intermediate level talk aimed at developers still learning how to write parallel and async code in C#.
Pobiega
Pobiega14mo ago
Thread.Start
joren
jorenOP14mo ago
Makes sense, good to know.
Pobiega
Pobiega14mo ago
if you want a full blown thread
Exeteres
Exeteres14mo ago
this is true for single-threaded languages/runtimes like Node.JS/Dart VM but in C# it is slight more complex when you await something the compiler "passes the continuation" to something called Syncronization Context which is bound to the current thread and this context decide where this continuation will be run: in the same thread or in some other place or will not run at all
joren
jorenOP14mo ago
what I am getting at based on you two is that TAP may utilize parallelism whenever it sees fit, to ensure continuation of the program? That's what I'd need if I were ever to write a DLL I suppose
Pobiega
Pobiega14mo ago
there are situations where you want to use that. a listener for your server, for example but tasks is generally what we use for anything short lived but really, if you have an hour, check the video I linked. its very good
joren
jorenOP14mo ago
Makes sense, but when using Task.Run I am guarenteed to have my own thread that isnt shared amongst other tasks, for instance TAP using my thread? I guess thats the point, TAP might decide to utilize my thread if it seems fit(?)
IsNotNull
IsNotNull14mo ago
You are getting a thread from a thread pool then.
joren
jorenOP14mo ago
yeah, but would that thread still be used by TAP for other tasks
Pobiega
Pobiega14mo ago
its a thread from the pool, iirc it cant be "co-opted" by something else, unless you hit an await in there
joren
jorenOP14mo ago
ah yeah, so when its not being used it might be used by TAP
Pobiega
Pobiega14mo ago
if you hit an await, you hit an await and its up to the sync context again
joren
jorenOP14mo ago
whereas Thread.Start would never have that, now the Listener example makes sense.
IsNotNull
IsNotNull14mo ago
If you use Thread.Start, that creates an OS thread and doesn't use one from a pool. In some cases that can be more expensive.
joren
jorenOP14mo ago
yeah the threads in the thread pool, are they smaller?
IsNotNull
IsNotNull14mo ago
No, they are reused It doesn't have to create an OS thread, because one already exists and is reused
joren
jorenOP14mo ago
so still an OS thread, just put in a pool and reused and utilized for tasks ye
IsNotNull
IsNotNull14mo ago
Yeah, creating an OS thread has an overhead...sometimes it can be fairly substantial
joren
jorenOP14mo ago
makes sense, so generally speaking using the thread pool (TAP) instead of Thread.Start unless you have a very specific reason for it
IsNotNull
IsNotNull14mo ago
Correct
joren
jorenOP14mo ago
if you inject a DLL into a program, it wouldnt have a pool would it lets say
IsNotNull
IsNotNull14mo ago
Really long lived background jobs that you don't want to impact the thread pool are sometimes started with Thread.Start
joren
jorenOP14mo ago
you have a C# application, that runs, that has a thread pool. If I inject my DLL, can I use the thread pool from the application I injected into
IsNotNull
IsNotNull14mo ago
What do you mean by injecting a dll?
joren
jorenOP14mo ago
injected, as in loaded in, assuming its a feature and allowed
Exeteres
Exeteres14mo ago
them will use the single thread pool
IsNotNull
IsNotNull14mo ago
Apps can load a DLL dynamically using reflection. Do you mean that?
joren
jorenOP14mo ago
I mean I dont know about reflection, I come from C++ and I never used reflection really basically you manual map or use load libary to inject a DLL into a process and you create a thread inside the application you inject into
IsNotNull
IsNotNull14mo ago
If someone is referencing your library and using it, they will have access to the same thread pool
joren
jorenOP14mo ago
that's where your "program" lives, could they use the same thread pool?
IsNotNull
IsNotNull14mo ago
That isn't really a thing in C#, not as described There are library methods you can call to load an assembly, but they don't use that terminology
Exeteres
Exeteres14mo ago
there is only one thread pool per process
joren
jorenOP14mo ago
Makes sense, so a DLL on its own cannot have a thread pool? unless its loaded in by an actual process
Exeteres
Exeteres14mo ago
yep DLL is just a bunch of classes
IsNotNull
IsNotNull14mo ago
If you are offering a library for use, its expected it will be referenced as a nuget package, or in some cases as a dll. Dynamic assembly loading is an edge case used by systems that have a plugin architecture
joren
jorenOP14mo ago
Yeah Okay makes sense yes, but once loaded in they cannot create their own threadpool only one thread pool per process is the rule so granted its loaded in without an issue, it should be able to leverage the thread pool of the process it loaded into
IsNotNull
IsNotNull14mo ago
They 'could', but the DLL would have to contain code implementing a custom thread pool or scheuduler It wouldn't be expected
joren
jorenOP14mo ago
Ah, ye - wouldn't make much sense in most cases to do such
Exeteres
Exeteres14mo ago
i think you can create your own thread pool or some another scheduler the default thread pool mechanism exists to make it "just work"
IsNotNull
IsNotNull14mo ago
If you have a library and it has methods that block on IO, devs will expect your library to expose async methods that will be scheduled on the default threadpool. You can deviate from that if you have a good reason.
joren
jorenOP14mo ago
makes sense, nice thanks you two very insightful. To go back to my initial question, async programming in C# does utilize parallelism to some extend when needed that is
IsNotNull
IsNotNull14mo ago
You can deviate from that if you have a good reason.
A good reason would be that you know your library will create many tasks or long lived threads and you don't want to disrupt the system that is using the library. For example, if you hold onto all of the threads from the pool for a long time, it can cause stalls in an ASP.NET Core web app running in the same process as it struggles to ramp up the number of actual OS threads in the pool Most libraries won't bother though
joren
jorenOP14mo ago
but if possible, it uses only one thread to jiggle the tasks efficiently
IsNotNull
IsNotNull14mo ago
Usually the consuming system just deals with what the library does and they modify how they call the library as needed
Exeteres
Exeteres14mo ago
yes it does "it just works" 🙂 and works well in most cases
joren
jorenOP14mo ago
Makes sense I would agrue that at that point it should be a seperate application if the size is substantial or equal to the application you wish to load a DLL into, and just make them communicate some way makes sense, I suppose the terms parallelism and asynchronous programming have a thin line in C#
IsNotNull
IsNotNull14mo ago
An async method runs on the calling thread until it hits an 'await' statement that blocks. Then the calling thread stops executing (or goes back to the thread pool if it started there). When the await ends, the async method returns...generally as another thread from the thread pool taken at that time.
joren
jorenOP14mo ago
they have some overlap
IsNotNull
IsNotNull14mo ago
There is some complication related to application models that have 'syncrhonization contexts', but I won't get into that (usually GUI apps that have a single rendering thread)
Exeteres
Exeteres14mo ago
they have the same meanings as in other languages and they're different but they're integrated so well that most programmers can really not think about it just put await where the Task returns and make the method async 🙂 and it works
IsNotNull
IsNotNull14mo ago
There is an article with "there is no thread" in the title that is a good read about async/await. While your app is awaiting an OS IO operation, there might be no app thread active (related to that method)...there might only be a completion scheduled by the OS that will schedule a callback that rehydrates your stack/captured async context
IsNotNull
IsNotNull14mo ago
There Is No Thread
This is an essential truth of async in its purest form: There is no thread.
IsNotNull
IsNotNull14mo ago
Some of that information might be out of date by now. I just wanted to point out that async/await is implemented by using a state machine to tuck your method away while its waiting for a result from some OS or other IO continuation
joren
jorenOP14mo ago
https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/#final-version Lets take this example for instance, in short, could you tell me the process that it goes through in slight detail. As far as im aware, once we get to the:
var eggsTask = FryEggsAsync(2);
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var eggsTask = FryEggsAsync(2);
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
It creates the tasks, does it already create a thread for each, or juggles them in one thread? or does it just create them, and once we Task finishedTask = await Task.WhenAny(breakfastTasks); it'll execute them, in whatever way TAP decides (i think the latter)
IsNotNull
IsNotNull14mo ago
It will run FryEggsAsync synchronously (on the current thread) until it hits the first await statement inside FryEggsAsync or one of the methods that FryEggsAsync calls Then it return a Task to the caller that could be awaited It will continue executing on the original calling thread at that point and do the same for the FryBaconAsync
joren
jorenOP14mo ago
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");

return new Egg();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");

return new Egg();
}
So in this case, it will run until the await Task.Delay() here, the first one first of course.
IsNotNull
IsNotNull14mo ago
When the Task for FryEggsAsync is returned, the Task that is returned is the one that was scheduled at that time on a task scheduler It might not start executing immediately. It might only be scheduled at that point
joren
jorenOP14mo ago
Then it return a Task to the caller that could be awaited
so it returns, and says like this is being await'ed, you can do something else in the meantime?
IsNotNull
IsNotNull14mo ago
At some point it will be given a thread pool thread to run on
joren
jorenOP14mo ago
thats when it looks for the next call, the FryBaconAsync and executes it
IsNotNull
IsNotNull14mo ago
Kind of Its in an indeterminite state of scheduling, executing, finished, or failed until you await it or check its status
Exeteres
Exeteres14mo ago
yes it literally returns Task and passes the execution back to the caller like any other method
IsNotNull
IsNotNull14mo ago
The task returned from the first async method On the first await Task.Delay, that is when the Task is returned and execution will continue on the outer method You should see the Warming the egg pan... in the console at that point
joren
jorenOP14mo ago
it returns Task and its status is added to the Task.Scheduler saying "this isnt finished, can be continued after ..." and then moves on and so for each time I await a status of a Task is added to the scheduler and it continues
IsNotNull
IsNotNull14mo ago
Its a bit more than that
joren
jorenOP14mo ago
and then at some point it'll wrap all the tasks up
IsNotNull
IsNotNull14mo ago
It creates a snapshot of the inner method's state (usually causing an allocation) capturing locals and things like that and schedules a continuation to run the remainder of the FryEggsAsync
joren
jorenOP14mo ago
ah and then proceeds this way its always doing something at times it would normally be waiting at some point u might have like 4 Tasks, with different statuses that are scheduled in a certain order
IsNotNull
IsNotNull14mo ago
But while its waiting for the continuation inside of Task.Delay inside FryEggsAsync the method goes away, it only exists as a snapshot in memory
joren
jorenOP14mo ago
ye, and it moves to the next
IsNotNull
IsNotNull14mo ago
If you don't call await, it doesn't do the method snapshot
joren
jorenOP14mo ago
it executes it, sees a await delay and in that time it might continue the snapshot (status ig) that we created of the first method what happens then, a new thread from the threadpool?
IsNotNull
IsNotNull14mo ago
Yeah. But with await you can't continue execution until the current awaitable finishes
Exeteres
Exeteres14mo ago
it worth to mention that await has rather "push" semantics it does not "await" nothing by occupying some thread it just subscribes to this task and task will trigger the continuation after it completes (it may not complete) and this continuation will be put to the thread pool and executed in the next available thread by default
IsNotNull
IsNotNull14mo ago
If you want multiple Tasks executing from a method at once, you usally won't use await
joren
jorenOP14mo ago
so how does it move past the await and start with another Task
IsNotNull
IsNotNull14mo ago
You can put the Tasks into a Task collection / array, then await a Task.WhenAll call and pass them all in (or pass them as a params array) Async / await is mostly designed for the case where you want to do one blocking thing at a time and dont' want to tie up the calling thread or threadpool while doing so.
joren
jorenOP14mo ago
can you show an example of that, a simple one how it solves this
IsNotNull
IsNotNull14mo ago
var taskA = DoSomethingAsyncA();
var taskB = DoSomethingAsyncB();
await Task.WhenAll(taskA, taskB);
var taskA = DoSomethingAsyncA();
var taskB = DoSomethingAsyncB();
await Task.WhenAll(taskA, taskB);
Uhh, you'll have to check that, I just wrote it off the cuff
joren
jorenOP14mo ago
thats the solution, but how await/async solves not blocking the thread it still waits for it to finish, and doesnt continue no?
IsNotNull
IsNotNull14mo ago
The calling thread can be released from blocking on the method while the method is waiting for the Task to complete Really important for UI rendering threads, for example They can continue to render other things and continue handling user input, etc...
joren
jorenOP14mo ago
without creating another thread of each thing that its supposed to render and handle at the same time you mean
IsNotNull
IsNotNull14mo ago
I'd have to give a bit longer example If I'm in a page, and a user clicked a 'submit' action, for example
joren
jorenOP14mo ago
I get why its useful though, I dont see how await/async solves that if thats the only thing you use
IsNotNull
IsNotNull14mo ago
The UI thread activates the 'clicked' method Its an async method I await a call to my repo.SaveThing method
joren
jorenOP14mo ago
mhm ye
IsNotNull
IsNotNull14mo ago
The UI thread becomes available again while the await is going on
joren
jorenOP14mo ago
it waits to execute until it has time to process it?
IsNotNull
IsNotNull14mo ago
Because the thread that called that method is no longer blocked waiting You could think of the method as completed with a continuation scheduled to continue the method, with a snapshot of the methods local variables
joren
jorenOP14mo ago
uhu yes, but when does it handle it - when nothing else has to be done?
IsNotNull
IsNotNull14mo ago
Like if the method has nothing inside of it after the repo.SaveThing was awaited? It would still continue to complete the method...but I guess it wouldn't do anything
Exeteres
Exeteres14mo ago
it transforms your async method to state machine class with single method MoveNext something like
c#
void MoveNext() {
__counter++;
switch (__counter) {
case 0: {
// code before first await
SomeMethodReturningTask().GetAwaiter().OnCompleted(MoveNext);
}
case 1: {
// code after first await
SomeAnotherMethodReturningTask().GetAwaiter().OnCompleted(MoveNext);
}
case 2: {
// code after second await
SomeAnotherAnotherMethodReturningTask().GetAwaiter().OnCompleted(MoveNext);
}
// and so on
}
c#
void MoveNext() {
__counter++;
switch (__counter) {
case 0: {
// code before first await
SomeMethodReturningTask().GetAwaiter().OnCompleted(MoveNext);
}
case 1: {
// code after first await
SomeAnotherMethodReturningTask().GetAwaiter().OnCompleted(MoveNext);
}
case 2: {
// code after second await
SomeAnotherAnotherMethodReturningTask().GetAwaiter().OnCompleted(MoveNext);
}
// and so on
}
so your async method just transforms to regular sync method split by awaits and each part ends by calling the async method, accessing task awaiter and providing MoveNext method as a completion that's very simplified, it's more complicated in real state machine
IsNotNull
IsNotNull14mo ago
You'll see those MoveNext calls come up sometimes in stack traces while you are debugging an app that uses async / await
joren
jorenOP14mo ago
More like, if I await a method and create the snapshot when it the snapshot actually used it needs to be done at some point
IsNotNull
IsNotNull14mo ago
Generally you'll use await when you need to do something after the awaitable thing has finished The snapshot are the variables in the method that you want to continue after the await
private async Task SaveThing(Thing thing)
{
var result = await repo.SaveThing(thing);
if (result.Failed) throw new Exception(...
}
private async Task SaveThing(Thing thing)
{
var result = await repo.SaveThing(thing);
if (result.Failed) throw new Exception(...
}
The 'thing' variable might be stored in the snapshot / state machine
joren
jorenOP14mo ago
Okay so lets say I use await, we basically say we want to save the state etc for later and we continue on?
Exeteres
Exeteres14mo ago
all local variables of async method are just transformed to fields of the state machine class so there is no need to save them they are already persisted we just create a new instance of state machine
joren
jorenOP14mo ago
so await, would be like: I put the bacon in the pan, now I save that I need to flip it and which bacon to flip, and I continue with something else
Exeteres
Exeteres14mo ago
and call MoveNext
IsNotNull
IsNotNull14mo ago
Await means, put this method (and its execution) away and continue it later after this Task completes
so await, would be like: I put the bacon in the pan, now I save that I need to flip it and which bacon to flip, and I continue with something else
If "I continue with something else" means the caller of that method continues
joren
jorenOP14mo ago
no like it might start another Task is what I meant
IsNotNull
IsNotNull14mo ago
The caller of that method could Not the method itself
joren
jorenOP14mo ago
yeah
IsNotNull
IsNotNull13mo ago
That method doesn't exist until the bacon fries, in that metaphor
joren
jorenOP13mo ago
ye, no I mean the caller of the async method
IsNotNull
IsNotNull13mo ago
Its a state machine / snapshot Yeah The caller will get a Task back at that point Which they can await, or move on and do something else Their choice
joren
jorenOP13mo ago
with that snapshot essentially
IsNotNull
IsNotNull13mo ago
As far as the caller is concerned, its just a Task They don't need to know about the state machine
joren
jorenOP13mo ago
I dont know there's smth in my way of thinking wrong I guess it doesnt click like its a tad frustration I wont lie
IsNotNull
IsNotNull13mo ago
That is not surprising. Its not intuitive
joren
jorenOP13mo ago
I have pieces but I cant piece it together
IsNotNull
IsNotNull13mo ago
Concurrency in general is frustrating
Exeteres
Exeteres13mo ago
you put the bacon in the pan and walk away saying "do something about it" someone else comes along and flips it over, spices it up and cooks it the way they want and then calls you again or doesn't call you at all and your bacon gets picked up by a garbage collector, dead serious
IsNotNull
IsNotNull13mo ago
You know whats silly. While its taken for granted, there is no guarantee that Tasks will run on other threads even when scheduled I think Blazor WASM uses tasks and async methods, but its all just executed by a single thread
joren
jorenOP13mo ago
wouldnt rendering become an issue then? I suppose you'd need some type of priority setting
IsNotNull
IsNotNull13mo ago
No, because using async means you don't block the render thread for longer operations. It can still react to user input and other events
joren
jorenOP13mo ago
in a single thread
IsNotNull
IsNotNull13mo ago
Yup Because again... "There is no thread" (with lots of caveats) (no thread being used by that application method at that callsite, etc...) Blazor is scheduling a continuation of the async method waiting for a JS interop call to do a fetch to finish, or something like that (in most cases)
joren
jorenOP13mo ago
I dont understand how one thread can handle multiple things at the same time its like one cook being able to do two things at the same time
joren
jorenOP13mo ago
our brain cant do it, we can only switch our focus quickly alright, ill eat and watch that
IsNotNull
IsNotNull13mo ago
He is a contentious figure, but its not a bad watch.
joren
jorenOP13mo ago
he looks funny, thats for sure
IsNotNull
IsNotNull13mo ago
Pretty close to the chef metaphor @Exeteres was using, I think I meant he gets flak from the dev community for his language design stances He is a key figure in google's 'Go' language The stability of most modern high performance web servers and systems depend on the a clear distinction between concurrency and threads, so its worth learning even if unintuitive
joren
jorenOP13mo ago
yeah I understood, I'll give it a watch while I eat some food! Thanks for now, I'll be back and hopefully I've put the pieces together at that point
Accord
Accord13mo ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server