❔ 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
or does
async Task<T>
typically only utilize one thread and just juggle the different tasks as effieciently as possible in one thread?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.is
Task.Run
the only way to create a thread, without calling the platforms API directly of course?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#.
Thread.Start
Makes sense, good to know.
if you want a full blown thread
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 allwhat 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
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
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(?)You are getting a thread from a thread pool then.
yeah, but would that thread still be used by TAP for other tasks
its a thread from the pool, iirc it cant be "co-opted" by something else, unless you hit an await in there
ah yeah, so when its not being used it might be used by TAP
if you hit an await, you hit an await and its up to the sync context again
whereas
Thread.Start
would never have that, now the Listener example makes sense.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.
yeah
the threads in the thread pool, are they smaller?
No, they are reused
It doesn't have to create an OS thread, because one already exists and is reused
so still an OS thread, just put in a pool and reused and utilized for tasks
ye
Yeah, creating an OS thread has an overhead...sometimes it can be fairly substantial
makes sense, so generally speaking using the thread pool (TAP) instead of
Thread.Start
unless you have a very specific reason for itCorrect
if you inject a DLL into a program, it wouldnt have a pool would it
lets say
Really long lived background jobs that you don't want to impact the thread pool are sometimes started with Thread.Start
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
What do you mean by injecting a dll?
injected, as in loaded in, assuming its a feature and allowed
them will use the single thread pool
Apps can load a DLL dynamically using reflection. Do you mean that?
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
If someone is referencing your library and using it, they will have access to the same thread pool
that's where your "program" lives, could they use the same thread pool?
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
there is only one thread pool per process
Makes sense, so a DLL on its own
cannot have a thread pool?
unless its loaded in by an actual process
yep
DLL is just a bunch of classes
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
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
They 'could', but the DLL would have to contain code implementing a custom thread pool or scheuduler
It wouldn't be expected
Ah, ye - wouldn't make much sense in most cases to do such
i think you can create your own thread pool or some another scheduler
the default thread pool mechanism exists to make it "just work"
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.
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
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
but if possible, it uses only one thread to jiggle the tasks efficiently
Usually the consuming system just deals with what the library does and they modify how they call the library as needed
yes it does
"it just works" 🙂
and works well in most cases
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#
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.
they have some overlap
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)
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 worksThere 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
There Is No Thread
This is an essential truth of async in its purest form: There is no thread.
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
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:
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)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
So in this case, it will run until the
await Task.Delay()
here, the first one first of course.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
Then it return a Task to the caller that could be awaitedso it returns, and says like this is being
await
'ed, you can do something else in the meantime?At some point it will be given a thread pool thread to run on
thats when it looks for the next call, the
FryBaconAsync
and executes itKind of
Its in an indeterminite state of scheduling, executing, finished, or failed until you await it or check its status
yes
it literally returns
Task
and passes the execution back to the caller like any other methodThe 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
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 continuesIts a bit more than that
and then at some point it'll wrap all the tasks up
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
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
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
ye, and it moves to the next
If you don't call await, it doesn't do the method snapshot
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?Yeah. But with await you can't continue execution until the current awaitable finishes
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 defaultIf you want multiple Tasks executing from a method at once, you usally won't use await
so how does it move past the
await
and start with another TaskYou 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.
can you show an example of that, a simple one
how it solves this
Uhh, you'll have to check that, I just wrote it off the cuff
thats the solution, but how
await/async
solves not blocking the thread
it still waits for it to finish, and doesnt continue no?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...
without creating another thread of each thing that its supposed to render and handle at the same time
you mean
I'd have to give a bit longer example
If I'm in a page, and a user clicked a 'submit' action, for example
I get why its useful though, I dont see how
await/async
solves that
if thats the only thing you useThe UI thread activates the 'clicked' method
Its an async method
I await a call to my repo.SaveThing method
mhm ye
The UI thread becomes available again while the await is going on
it waits to execute until it has time to process it?
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
uhu yes, but when does it handle it - when nothing else has to be done?
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
it transforms your async method to state machine class with single method
MoveNext
something like
so your async method just transforms to regular sync method split by await
s
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 machineYou'll see those MoveNext calls come up sometimes in stack traces while you are debugging an app that uses async / await
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
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
The 'thing' variable might be stored in the snapshot / state machine
Okay so lets say I use await, we basically say we want to save the state etc for later and we continue on?
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
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
and call MoveNext
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 elseIf "I continue with something else" means the caller of that method continues
no like it might start another Task
is what I meant
The caller of that method could
Not the method itself
yeah
That method doesn't exist until the bacon fries, in that metaphor
ye, no I mean the caller of the async method
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
with that snapshot
essentially
As far as the caller is concerned, its just a Task
They don't need to know about the state machine
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
That is not surprising. Its not intuitive
I have pieces but I cant piece it together
Concurrency in general is frustrating
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
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
wouldnt rendering become an issue then?
I suppose you'd need some type of priority setting
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
in a single thread
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)
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
Give this a watch, if you have time
https://www.youtube.com/watch?v=oV9rvDllKEg
our brain cant do it, we can only switch our focus quickly
alright, ill eat and watch that
He is a contentious figure, but its not a bad watch.
he looks funny, thats for sure
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
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
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.