Thank u for starting explaining the most "unexplained" charters of the await/async in a very concise way. Waiting for the next parts. It will be great that u show how to handle correctly the concurrency use cases (when we try try to update the same "list/array/etc" from different tasks.
Just use any of the collections in the System.Collecions.Concurrent namespace (ie ConcurentQueue, ConcurrentStack, ConcurrentDictionary, ConcurrentBag etc) they are extremely robust and thread safe so you can update them from as many concurrent Tasks as you like. You can also use them for tracking and controlling the flow of Tasks. Very handy when you have hundreds or thousands of Tasks that you need to process as efficiently as possible.
It seems messy, but the motivation for MS building it this way is clear. If everything was going smoothly until the cancellation token was triggered, then what you want is a "one cancellation exception to rule them all" and that's what is thrown. If *any* of those tasks threw a "genuine" exception, you absolutely do not want that to be overlooked by virtue of you having caught an OperationCanceled exception. This is why having two separate catch blocks (one for when the CTS was triggered, the other for real errors) is a good pattern. You could argue that the real WTF here is that triggering a CTS results in an exception being thrown, but given that the cancellation could be initiated by an external source (OS shutdown in progress, for example) and that pushing an exception onto a thread is the quickest way to stop it, I can see why it was engineered this way.
I tend to agree, other than I would expect the exception property in the when all task to contain all the exceptions, including the operation canceled ones.
Messy indeed. OperationCancelledException is still an exception and why it is omitted from the aggregate just befuddles me and is cause for concern about design decisions elsewhere. My first question was what other secret exceptions are not included and where will this bite me in the @ss later?
I'm curious when is it safe to access task.Result? And how do we work with Task.WhenAlll if all of the tasks return a result of different type? Just some cases which I found interesting while learning about Task.WhenAll, that I think your viewers may find interesting.
So the AggregateException thrown is absolutely useless because the actual "source of truth" lies in each of the original tasks. AggregateException does not contain all of the exceptions, just some of them depending on some still unclear criteria. While this is fantastic short video to explain some of this really confusing behavior deeply embedded within the async/await implementation, it does beg the question "why is it done this way?". This is a complaint about the implementation itself and not meant to detract from this video which does a great job of explaining the high level overview of this problematic behavior. Either the AggregateException class should contain an aggregate list of ALL exceptions or it should be renamed to "PartialAggregeteOfSomeExceptionsButNotOthers" because this is really just another way to make our already difficult jobs much harder for no apparent gain.
That's some interesting behavior I didn't know about. But I arrive at a different solution. If you do a Catch(OperationCancelledExcepion) first, it'll hit only when everything has been cancelled, without any additional errors. Followed by a Catch(Exception) block where one can handle the cases where a "real error" occurred and the whenAllTasks.Excepion will not be empty. In this scenario, I don't feel I need the OperationCancelledExceptions. Making two Catch-Blocks like that, I don't see a need for the the list of *all* Exceptions.
This is a very good example of why you should consider making all of your functions Try functions where the exception is handled inside the function itself and any details are logged because when you throw exceptions out of each function it gets very messy very quickly and there is no guarantee that the exception will be handled properly by the caller. Also if you really must know the reason why the function failed at the time of calling, you can always return a tuple containing a bool indicating whether the function succeeded or failed and the exception itself or a text description of what went wrong.
That ! My only thought during the video was "I would not have had problem in Go." Love libs like ErrorOr (even if I don't like the naming) for those kind of things.
I was not aware of it, but im actually doing it correct xD. I have a for each loop on my users and i have a method which needs much processing so i add them as a task for each of the user so they can process in parallel, and then i await the list. My question to you is, if you know how can i start all the task at the same second? var tasks = new List(); foreach (var user in Users) { tasks.Add(Process(user, Filter.Intense)); tasks.Add(Process(user, Filter.Medium)); } await Task.WhenAll(tasks); the moment i Add them they start instantly, is there a way to add them and only after start in parallel?
Im surprised that the syntax and the way C# (or dotnet) handle exceptions in async tasks are similar with Python. I wonder if there is anything similar to the Pythons exception group. and how can C# handle exceptions within multiple tasks where you should keep the other "no exception" tasks running? e.g) WhenAll(goodTask1, errTask1, goodTask2) and I want goodTask1 to keep running whether errTask1 raise exceptions
As he says it in the video, when you "await Task.WhenAll()", it waits for all the tasks to finish. So if errTask1 raises an exception, it won't stop goodTask1. This means if you wanted task #3 to stop if task #2 fails, you'd need to use a cancellation token to cancel it.
9:24 I think you still care about the Exception e in the catch. In realistic scenarios you will have probably some more code before the await Task.WhenAll call and an exception could happen to that non task related code. I think just ignoring the catch Exception is a bit too scary for me
Uhm when i have done this i put the try catch inside the method that is being called, which is inside a service with its own logger. Is that bad? Im not doing libraries for others to use at the end im the only one using that code. But if it's a method that i call many times, instead of always writing the try catch outside i just do it inside. (This before i ve found about oneof and your amazin errorOr) But hey i'm a dumb junior dev what do i know xD
First of all it is worth noting that if you have tasks started like this you are better off simply awaiting them one after the other instead of going for Task.WaitAll like this task1 = SomeOperationAsync(); task2 = SomeOtherOperationAsync(); result1 = await task1; result2 = await task2; Where you might need WaitAll is if you create tasks with a loop. Because I can never remember these exception rules I simply wrap the async call and whatever logic I can stick before and after the call in another async method and wrap the whole thing in a try/catch and do whatever I would do with the exception to begin with. This way I am sure no exception gets through WaitAll
What if you need to load data from 3 different repositories? Makes no sense to await each, just call them in parallel. Say they each take 3 seconds, then why wait 9 seconds?
@@jfpinero look at the order of his statements. He starts the tasks, then awaits each after the other. The tasks will be running in parallel, so the time taken will still be around 3s. WhenAny/WhenAll basically does the same thing internally, but using task continuations in a semaphore like pattern.
@@billy65bob it will not run i parallel. He has just written tasks declarations before and than called it one after another. The second await call will wait for the first one to complete and than will call its method.
@@vladix25 You're right in that it's not strictly parallel; the async keyword is kind of magic and the execution of it is actually synchronous until there is something it can wait for asynchronously. At which point the current call is put on a back burner, to continue as soon as its wait has completed. While it's on the back burner, functions further up the stack continue their execution. Once the wait is completed, the calls marshal back to the executing thread (unless ConfigureAwait(false) was used) to continue their execution. For op's example, he calls SomeOperationAsync(), this runs synchronously until it does some sort of await internally, then the original method will continue running to then kick off SomeOtherOperationAsync() until it too awaits something internally. Op then awaits the results of those tasks, which would both ideally be in the middle of waiting for something. Consider this code, per your suggestion, the total wait here should be around 14s; but reality and expectation is fairly close to 7.8s. var wait1 = Task.Delay(5000); var wait2 = Task.Delay(1337); var wait3 = Task.Delay(7777); await wait1; await wait2; await wait3;
Hmm, this really highlights how inefficient Task.WhenAll is, when in most cases when one of the tasks fails, you should probably cancel the remainder to recover the resources that they're taking up.
Thank u for starting explaining the most "unexplained" charters of the await/async in a very concise way. Waiting for the next parts. It will be great that u show how to handle correctly the concurrency use cases (when we try try to update the same "list/array/etc" from different tasks.
Thanks, Alex! I have an interesting video planned around concurrency and .NET’s memory cache. Let me see if I can broaden it
Just use any of the collections in the System.Collecions.Concurrent namespace (ie ConcurentQueue, ConcurrentStack, ConcurrentDictionary, ConcurrentBag etc) they are extremely robust and thread safe so you can update them from as many concurrent Tasks as you like. You can also use them for tracking and controlling the flow of Tasks. Very handy when you have hundreds or thousands of Tasks that you need to process as efficiently as possible.
It seems messy, but the motivation for MS building it this way is clear. If everything was going smoothly until the cancellation token was triggered, then what you want is a "one cancellation exception to rule them all" and that's what is thrown. If *any* of those tasks threw a "genuine" exception, you absolutely do not want that to be overlooked by virtue of you having caught an OperationCanceled exception. This is why having two separate catch blocks (one for when the CTS was triggered, the other for real errors) is a good pattern.
You could argue that the real WTF here is that triggering a CTS results in an exception being thrown, but given that the cancellation could be initiated by an external source (OS shutdown in progress, for example) and that pushing an exception onto a thread is the quickest way to stop it, I can see why it was engineered this way.
I tend to agree, other than I would expect the exception property in the when all task to contain all the exceptions, including the operation canceled ones.
Messy indeed. OperationCancelledException is still an exception and why it is omitted from the aggregate just befuddles me and is cause for concern about design decisions elsewhere. My first question was what other secret exceptions are not included and where will this bite me in the @ss later?
This is a perfect example why using a framework for more complex designs/scenarios is never as big a time saver as one might think.
👆🏼
Especially when implemented in a confusing and inconsistent way such as this.
I'm curious when is it safe to access task.Result?
And how do we work with Task.WhenAlll if all of the tasks return a result of different type?
Just some cases which I found interesting while learning about Task.WhenAll, that I think your viewers may find interesting.
Please can you talk about this in your up coming videos?
So the AggregateException thrown is absolutely useless because the actual "source of truth" lies in each of the original tasks. AggregateException does not contain all of the exceptions, just some of them depending on some still unclear criteria. While this is fantastic short video to explain some of this really confusing behavior deeply embedded within the async/await implementation, it does beg the question "why is it done this way?". This is a complaint about the implementation itself and not meant to detract from this video which does a great job of explaining the high level overview of this problematic behavior. Either the AggregateException class should contain an aggregate list of ALL exceptions or it should be renamed to "PartialAggregeteOfSomeExceptionsButNotOthers" because this is really just another way to make our already difficult jobs much harder for no apparent gain.
Hi Amichai, can you please make a video on how you set your visual code and which extensions you used in it.
Best teacher i accidently found.
That's some interesting behavior I didn't know about. But I arrive at a different solution.
If you do a Catch(OperationCancelledExcepion) first, it'll hit only when everything has been cancelled, without any additional errors.
Followed by a Catch(Exception) block where one can handle the cases where a "real error" occurred and the whenAllTasks.Excepion will not be empty. In this scenario, I don't feel I need the OperationCancelledExceptions.
Making two Catch-Blocks like that, I don't see a need for the the list of *all* Exceptions.
I like that approach as well. Cut it from the final version of the video to keep it short 🤓
I was not aware of it. Great video. Thanks!
This is a very good example of why you should consider making all of your functions Try functions where the exception is handled inside the function itself and any details are logged because when you throw exceptions out of each function it gets very messy very quickly and there is no guarantee that the exception will be handled properly by the caller.
Also if you really must know the reason why the function failed at the time of calling, you can always return a tuple containing a bool indicating whether the function succeeded or failed and the exception itself or a text description of what went wrong.
That !
My only thought during the video was "I would not have had problem in Go."
Love libs like ErrorOr (even if I don't like the naming) for those kind of things.
Never had problem with it. It seems intuitive for me.
Hi Amichai,
Thanks for the video. Could you please share the name of the tool that you use to draw the arrows on the screen?
Thank you so much
So I should manually catch exceptions around every task.whenAll or is there an option to write it once in middleware?
or write it as a generic helper static method
What a mess!!!! This stinks and should be redesigned in the framework! Thanks for the explanation :)
Yeah it’s an odd design 🫣
I was not aware of it, but im actually doing it correct xD.
I have a for each loop on my users and i have a method which needs much processing so i add them as a task for each of the user so they can process in parallel, and then i await the list.
My question to you is, if you know how can i start all the task at the same second?
var tasks = new List();
foreach (var user in Users)
{
tasks.Add(Process(user, Filter.Intense));
tasks.Add(Process(user, Filter.Medium));
}
await Task.WhenAll(tasks);
the moment i Add them they start instantly, is there a way to add them and only after start in parallel?
they do run in parallel bc you havent awaited them yet
Im surprised that the syntax and the way C# (or dotnet) handle exceptions in async tasks are similar with Python.
I wonder if there is anything similar to the Pythons exception group. and how can C# handle exceptions within multiple tasks where you should keep the other "no exception" tasks running?
e.g) WhenAll(goodTask1, errTask1, goodTask2) and I want goodTask1 to keep running whether errTask1 raise exceptions
As he says it in the video, when you "await Task.WhenAll()", it waits for all the tasks to finish. So if errTask1 raises an exception, it won't stop goodTask1. This means if you wanted task #3 to stop if task #2 fails, you'd need to use a cancellation token to cancel it.
You need to zoom that like 5x so it ia readable on a phone
Unrelated question: What software is used to draw on screen?
Good work on explaining Task. WhenAll! Keep up the good work!
It’s called ZoomIt
Yup 👍🏼
@@MyFuzzyAfterlife Thanks! 👍👍
Nice, thank you
Hi! I like your videos, esp. the clean/DD series but, do you have any plans about when to show up on mastodon (or at least crosspost)?
Thanks! I think I’m heading over to mastodon as well. Too much ye & musk on my feed 😆
9:24 I think you still care about the Exception e in the catch. In realistic scenarios you will have probably some more code before the await Task.WhenAll call and an exception could happen to that non task related code. I think just ignoring the catch Exception is a bit too scary for me
You can have access to the individual exception directly in the task array.
Uhm when i have done this i put the try catch inside the method that is being called, which is inside a service with its own logger.
Is that bad? Im not doing libraries for others to use at the end im the only one using that code. But if it's a method that i call many times, instead of always writing the try catch outside i just do it inside.
(This before i ve found about oneof and your amazin errorOr)
But hey i'm a dumb junior dev what do i know xD
Haha no such thing as a dumb junior dev 😠 I would add error handling logic for the when all task regardless of what happens inside the called method
@@amantinband oh i see!
Learning something new every day👍🏽
Thanks for sharing the knowledge🙏🏼
Nice explain, When will the RESTAPI persistant storage epsodes coming?
Thanks, persistence is the next section in the DDD series
@@amantinband
Which dev env have you used in this video? Can you please share details of how to use the same?
Thanks for knowledge sharing
Bad description of the problem.
Task.WhenAll is a method that returns TASK, so use await same as for any task.
The auto code generator is frustrating
Dear gawd
It has more info but too fact to catch and jumping so fast to follow...
First of all it is worth noting that if you have tasks started like this you are better off simply awaiting them one after the other instead of going for Task.WaitAll
like this
task1 = SomeOperationAsync();
task2 = SomeOtherOperationAsync();
result1 = await task1;
result2 = await task2;
Where you might need WaitAll is if you create tasks with a loop. Because I can never remember these exception rules I simply wrap the async call and whatever logic I can stick before and after the call in another async method and wrap the whole thing in a try/catch and do whatever I would do with the exception to begin with. This way I am sure no exception gets through WaitAll
What if you need to load data from 3 different repositories? Makes no sense to await each, just call them in parallel. Say they each take 3 seconds, then why wait 9 seconds?
@@jfpinero look at the order of his statements. He starts the tasks, then awaits each after the other.
The tasks will be running in parallel, so the time taken will still be around 3s.
WhenAny/WhenAll basically does the same thing internally, but using task continuations in a semaphore like pattern.
@@billy65bob it will not run i parallel. He has just written tasks declarations before and than called it one after another. The second await call will wait for the first one to complete and than will call its method.
@@vladix25 wanna bet?
@@vladix25 You're right in that it's not strictly parallel; the async keyword is kind of magic and the execution of it is actually synchronous until there is something it can wait for asynchronously.
At which point the current call is put on a back burner, to continue as soon as its wait has completed. While it's on the back burner, functions further up the stack continue their execution. Once the wait is completed, the calls marshal back to the executing thread (unless ConfigureAwait(false) was used) to continue their execution.
For op's example, he calls SomeOperationAsync(), this runs synchronously until it does some sort of await internally, then the original method will continue running to then kick off SomeOtherOperationAsync() until it too awaits something internally.
Op then awaits the results of those tasks, which would both ideally be in the middle of waiting for something.
Consider this code, per your suggestion, the total wait here should be around 14s; but reality and expectation is fairly close to 7.8s.
var wait1 = Task.Delay(5000);
var wait2 = Task.Delay(1337);
var wait3 = Task.Delay(7777);
await wait1;
await wait2;
await wait3;
So I was doing it accidentally the correct way 😎
I think even your final solution has a problem if any of the tasks throws before its first `await`
Why are you saying it that way?
Saying _what_ what way?
Ahsync vs aysync? 😂
Hmm, this really highlights how inefficient Task.WhenAll is, when in most cases when one of the tasks fails, you should probably cancel the remainder to recover the resources that they're taking up.
Crikey. Perfect reason to use Result types with errors as values. Stop throwing all together. It’s control flow madness.