Correcting Common Async/Await Mistakes in .NET 8 - Brandon Minnick - Copenhagen DevFest 2023

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
okay welcome everybody come on in thanks so much for joining me today my name is Brandon minck and in this session we're talking about how to correct common mistakes in async and await now I am so excited to be here with you today because this is a talk that actually made back in 2018 gave it for the first time at NDC Sydney so it's super cool to come all the way back to Copenhagen and deliver it but there's been a lot more cool stuff add it to net since 2018 so it was an hour talk back then it's about an hour and a half talk now that we're just going to cram into an hour so I'm going to move fast but the good news is I've got a link down here on the slides you'll see this link you'll see this QR code takes you to the same place this is where you can find everything that we're going to talk about today so we've got some open source code all that's up on GitHub you can find it at the website that's where you can find the slides you can even find the recording from I also did this talk at NDC Oslo a couple years ago as well so don't worry about trying to take notes don't worry about trying to keep up don't worry about trying to memorize anything because we got a lot of stuff to cover and it's all there for you so make sure to grab a picture of that QR code and feel free to share it with friends and co-workers who maybe couldn't be here today but let's get rolling so we're going to start by looking at this method this is a async task method called red dat from URL and yes I made this six years ago so it's a little out of date but if you ignore the fact that we're using web client because yes today you should be using HTTP client you should never knew up a new HTTP client inside a method that should be static you should be reusing them we've got HTTP client Factory now but we're still using it in this example just because you'll see in a bit we dive real deep into this code and these are really complex slides that take a lot of work to put together but what we're doing here is we are new up a a web client we're calling download data task async we're returning that as a bite array called result en encoding that into a string data and then passing that to a method called load data now what's really going on here with with async AWA with multi-threading is that the first thread is going to call this chunk of code so let's say thread one calls this code so thread one comes in it news up web client it initializes bite array result and then it hits that await keyword and as soon as our thread hits the awake keyword it goes away it it returns and that's important because thread one is also known as the main thread it's also known as the UI thread it's the most important thread in our apps because it's the only thread that can interact with the UI it's the only thread that can interact with the user when the user Taps a button on our app it's the UI thread that reacts and fires off that uh click Handler uh when the user Scrolls on the screen it's the UI thread that has to redraw everything on the screen while the user scrolling so if thread one was calling download data task async it'd be really bad because if download data task async takes 5 Seconds well then the user can't interact with our app for 5 seconds and our app's basically Frozen and what happens when our app freezes well what do they do they force quit the app they give us a onear review in the app store they tell their friends don't use this app it's crap it freezes all the time when really all that's happening is we accidentally locked up the main thread the calling thread so good news with asyn of we as soon as we hit the await keyword that calling thread in this case thread one returns it's able to now interact with the user the user can tap buttons all the meanwhile thread 2 in this case is running in the background now it's not always going to be thread 2 uh what happens under the hood is do net just goes and grabs any thread from the thre any thread from the thread pool uh we've got this cool thing inet called thread pools where depending on the size of your computer your server your phone whatever your app code is running on uh that'll determine how many threads you have in the thread pool so in this case we got thread 2 it's running download data task async and then when it finishes net goes back to the calling thread in this case thread one and says hey thread one you're back up so thread one jumps back in picks up where it left off and it does the encoding to turn that into a string and then it jumps into load data now we're going to go a little deeper and to go a little deeper we've got to see what the compiler actually generat Ates out of our code so here's that here's that method again and this is actually what it looks like after we compile uh so if you're not familiar umet when you click compile it turns your C code into a dll file and sometimes it does some compiler gener generated stuff for us which is what we're looking at here and if you ever wanted to you can use a decompiler to take that dll and convert it back into C code so we're looking back at C code that was created by in the dll file and then recreated by our decompiler and the first thing you'll notice is we're looking at a private sealed class called the same as our method name read data from URL now I didn't write a class I wrote a method but the compiler took my async method and turned it into a class and this happens anytime we use the async keyword so you might not realize it but every time we use that async keyword and we add it on that little on the method line on the meth method signature the compilers got to work a little bit harder to create this task or create this class and our app size gets just a little bit bigger because it generates some code for us now that's all insignificant for 99.9% of our projects but just FYI it'll add a nanc to your compile time and it'll increase your app size by about a 100 bytes but I don't know if you're like me who cares about 100 byes right like I'm a mobile app developer I make IOS and Android apps using zamarin and net Maui all and c and I I don't care about H 100 byes but maybe maybe you're making stuff for the space shuttle and it's got to be really optimized then you might care what else is in here well we see a couple Fields so a bunch of private fields and these fields are actually the same variables that we created inside our method so not only does it generate a class but it also takes those local variables that we created and turns them into Fields so you can see there's private string data private bite aray result private web client WC uh public string URL because URL was the parameter passed in so it's public and something else you'll notice is these weird angle brackets and underscores uh the net compiler actually does that on purpose because when it generates code it doesn't want to generate anything with a similar variable name that might already exist in your code and so the trick that the team does is it actually creates illegal file names like if you tried to create a field called angle bracket data angle bracket 5or 3 it wouldn't compile so that's how we can kind of get away with creating that or how the net team can get create away with creating that for us and what else do we have in here we've got this method called move next now if I'm being totally honest with you this is what started my journey down the async weight rabbit hole about six seven eight years ago because I pushed my first app to the app store around that time and I kept seeing these weird errors like weird things were happening in my app that I didn't tell it to do and I had no idea what was going on and and a lot of my stack traces was move next and I'm like what is this I I never wrote a method called move next so how is this move next appearing in all my stack traces um well it's because it was autogenerated for me and if we dive into move move next this is what it looks like but let's zoom in move next is basically a state machine so to boil that down into C terms move next is just a giant switch statement and it creates a case every time we use the await keyword so in our instance here we have two cases and that's because in the method I wrote I call the awake keyword once if in My Method I had called the awake keyword twice we would have three cases if I had used the awake keyword three times we would have four cases so every time we use that a keyword we get a new case inside of move next inside of our state machine and if we look at the code case zero this is all the code leading up to that first await so just like we saw earlier it's newing up web client and then it's calling download data task async kind of uh really what it's doing it's getting the this what's it's called an a waiter and not to go into too much detail but that's basically how thread one will know where to come back but then thread one returns so we hit this return statement and this is actually the magic that frees up that calling thread once it hits the awake keyword because thread one as far as it knows it returned it's done it exits the method and now our main thread is able to go back and our users can use our app and not have to worry about it freezing up but before it returns it sets that value of PC to one and it does that because when net comes back to thread one and says hey the background task is done you're back up thread one's going to jump back into case one so it's kind of letting itself know where to pick up where it left off and just like we expect this is the rest of our method where we we get the result from download data task async we encode it and then we call that load data method now the other weird thing inside a move next is this this Tri catch block and I guarantee you everybody in this room if you've written asyn A8 code you've been burned by this Tri catch block myself included and what's what's weird about this is I didn't write a TR catch block right there's no TR catch block and my code this is all autogenerated for us and it's always autogenerated for us and so what this means is if any of my code throws an exception it's going to get caught right here now the good news is if we use the awake keyword the awake keyword will essentially rethrow the exception so when Say thread one comes back to see hey what what happened how'd everything go where do I pick up or I left off it goes oh there's an exception it'll rethrow it so as long as we use the awake keyword we're good but the biggest problem I see and this was actually my problem back in the day when I made my first app was we try to be too clever you know I was I was was making a mobile app and just like we were talking about a minute ago I didn't want to hog the UI thread so I thought I was being real smart saying I'm going to put this on a background thread by saying task.run here's all my code and you know what it's on the background I don't care when it finishes so I'm not going to wait task. run and what was happening what the reason I was getting weird behavior in my app and weird bugs and weird stack traces is that exception or an exception was being thrown in my code but it was being caught here in decid to move next and because I didn't await that task that exception never got rethrown and you might think to yourself isn't that a good thing exceptions crash our apps I try my best to not have exceptions and you're not totally wrong but I would argue exceptions are a good thing because it lets us know as developers that hey something happened that we didn't want something happened unexpected something exceptional happened and it's kind of C's way of letting us know or Net's way of asking us hey what do you want to do with this so for me my app was throwing exceptions I was not awaiting my task I thought I was being smart by running stuff inside the background thread doing test out run and all of a sudden now my app's in this weird state that I never designed it for I never programmed it for and you get really really weird bugs all because of try catch so let's do a quick review before we jump into some code so the async keyword adds about 100 bytes so every time we create a new async method again the compiler creates a class for us and it increases our app size by just a little bit but again what's what's 100 bytes Worth to us in 2023 I don't really mind and then the other thing we want to remember we want to await every task please don't ever do what I did don't ever say task. run and just let it go without awaiting it uh the other really bad pattern I see all the time well not all the time but way more often than we should should is people do underscore equals Tas out run like they're discarding the task and you definitely don't want to do that because then there's no way to get that exception back if one bubbles up so make sure we await every task otherwise these non- awaited tasks are going to hide exceptions all right so let's jump into some code here and like I said this is all this is all open source it's up on GitHub if you didn't get a chance to grab that link I'll share it with you later but what we're going to do here is we're going to take this this app I've created um I don't know if anybody reads Hacker News but it's a cool website I like that people can post Tech news stories to so this is just a mobile app that hits hacker news's apis pulls down the top stories and then you know we can click on any of these and we can read it here in our browser without having to go to the website so what I've got here is this is a AET Maui app obviously all written in C and again don't worry about memorizing anything because I've got this file bad asyn weight practices this is the one we're going to live in today this is the code we're fixing and if you don't remember everything we covered that's okay because it's already fixed inside of good asyn weight practice so let's dive in so this first refactor here we're we're actually we're already getting yelled at and we're already getting yelled at because we're doing exactly what I just told you not to do right uh refresh is an async task method and we're not awaiting it but you know here we are in the Constructor of the class and so you know I just I just stood up in front of everybody and said always await every task but we're in the Constructor Constructors can't use asent KO because Constructors aren't designed for that the whole point of a Constructor is just to initialize your object it's literally assigning a slot in memory to put this object into and then you know we can use it to assign some variables but you're not supposed to do any sort of long run thing in a Constructor because again we're just initializing the object assigning it to a location of memory so we'll never be able to use the awake keyword in the Constructor but so how do we get around this well there is one cool trick here we can use an async void method to get away from to get away from that error now you might be thinking to yourself hold up I was told never to use asyn void and and you're not wrong um but the problem I have with that that method of teaching because I'm sure we've all been in a class or watched a video when somebody says never use async void it's only meant for event handlers and yeah if you subscribe to that then you'll be in pretty good position but this is actually a valid case of acing FOID and the problem I have when people tell us never to use it is they don't tell you why so so why is this code dangerous because if we look at it we're we're covering everything we just talked about in the beginning right because refresh is an acing task method and we're using we're calling the await keyword so that means if an exception is thrown I'll be able to it'll surface it'll be uh visible to me because the awake keyword will rethrow that exception and and honestly this is good code you could push this to production but let's talk about why this is dangerous so uh one of the reasons why this is dangerous is if we go back to that first example where let's say thread one calls refresh so thread one comes into this method and it hits the awake keyword and just like we learned it's going to return and now a different thread let's say thread two is running this method called ref fresh um but what's really happening is thread one's returning to here so thread one's going to return here to continue executing code executing code now why this is dangerous is we've got a couple things going on here so the first thing is this method let's say you know let's say it doesn't exist right here like it's very much in our face here if we were looking at code we would hopefully see this acing void right in our face uh but let's what if it was in a different class let's say this is code I didn't write maybe it's a library right that I don't even have insight into what's going on under the hood uh when I call this method and I look at intellisense and I see intellisense tells me this method returns void as C developers we're rightly Justified into assuming that this refresh method will finish running before it goes to this line and that's totally valid you will never be wrong for assuming that a void method finishes running before it moves to the next next line of code um so one of the reasons this is dangerous is if we look down at refresh you know it's it's getting the top stories we have our top Story collection that it's going to be adding the stories into and in a sorted way because Hacker News likes to sort things by points so you can see people up vote and down vote stories and that's how they bubble up to the top um but what if here I was playing around with top Story collection to and maybe my code here is adding things or maybe I'm clearing it and and now what our code or the problem in our code is that thread one is modifying top Story collection well at the same time in the background on a different thread thread two is also modifying top Story collection so we're going to get weird Behavior here and so this is one of the reasons why async void is dangerous because there's there's no way to await a void method so so we don't we we don't like that um but another reason that async void's super dangerous is let's say this method does throw an exception so we'll just hardcode throw to exception here just to really drive home that issue uh you might think to yourself well okay no big deal you know I'll just I'll wrap this in a try try catch block and I'll just catch it right what's the big deal that's that's how do net works we just catch the exception and we probably do nothing with it cuz that's how we work inet um but the problem with this again is let's say again picture it right thread one comes into here thread one enters the triy catch block thread one calls refresh thread one comes down here hits the awake keyword and now it's returned it's left uh the refresh method and because we're not awaiting refresh thread one's now back here and it's just going to continue on and now thread one's all the way down here out outside of our Tri catch block meanwhile thread 2 in the background is still running this and eventually it's going to throw an exception but we've already exited the tri catch block so another reason why async void is dangerous is because it's kind of almost nearly impossible to catch exceptions that come out of an async void method so what are we going to do now right um so let's actually let's back way way up because we do want to avoid asyn void that's it's good advice it's good advice to avoid using asyn void methods but there are valid use cases as we saw here and so an option we do have and something I've created is this extension method called safe fire and forget and if we dig into the source code here because we're actually in the library for safey and forget we can see how it works and what I'm doing under the hood for you is I'm taking your task I'm awaiting it and then I have some code here to handle exceptions so if we jump back for example we could say if an exception does happen and let's say we'll just tell it to catch every exception then we can handle that exception here um and this is using async void but the nice thing about safe fire and forget is it's really in your face like if you're looking at this code you know just by seeing this method that yes I'm aware that this is a task and no I don't want to await it I just want to send it off to a background task and I know so it's basically we're saying here I know that my thread will continue on well in this case refresh is running so so what you can do uh again all this code's open source but I'll show you it's also up on nugit it's called async AWA best practices so you're more than welcome just to come in here copy paste the code it's just an MIT license it's open source that's what it's there for um but I figured why not noua tize it because I'm lazy too right I didn't want to have to copy this code into all my apps and so you can come download asyn weight best practices slap it into your app we actually just passed over a million downloads which is really really cool uh and so if a million people are using it it's got to be good right and then and then we know again we want to fire and forget this task we want to run it in the background and this is way way way better than what we were talking about earlier where we would do task. run dot dot dot task. run without awaiting it super super dangerous safe fire and forget pretty cool okay so we're going to keep that safe fire and forget and we're done refactoring ing this method here so let's move on to the next one so this is the method called refresh this is the method that executes anytime we do a pull to refresh on the app and you know one of the reasons why I'm calling it here in the Constructor is I want the app to automatically refresh and go get the new stories as soon as the user opens it like how how terrible of that of an experience would it be if you launch my app nothing happens and then you have to manually uh like swipe to refresh that'd be terrible so so that's why we're calling the Constructor um but what I have here is a little trick that we do in mobile apps because sometimes the results come back really fast and users don't don't believe it um so for better or worse I actually inject a task. delay into a lot of my apps so that we get consistency because especially with mobile apps if you do a pulled to refresh and the results come back right away see how it just disappeared it doesn't look like anything happened you're going to go ah what the heck and you're probably going to force quit the app and give me another one star review so as weird as it sounds this is a really common practice where you know it'll always display that activity indicator at the top for two seconds and we do that or I do that by having this minimum refresh time and I'm doing it by calling task. delay and then down here I'll make sure we wait until that task is done so even if all this code completes in 5 milliseconds that'd be insane but even if it does you're still going to see that activity indicator show up for 2 seconds to let you know that I heard you I did it we're good um so so what's wrong with this code well this isn't bad because we're gonna we're going to await this task later so we can we can kick it off here as like a little timer and then we can await it later um but something that we're missing here is I have a cancellation token refresh gives me a cancellation token token and I'm not using it here and that's a bad practice we should always be using cancellation tokens for all of our async task methods so if we're ever writing a async task method we should give the consumer of the method the ability to pass in a cancellation token because otherwise how are they going to stop it if they want to so it's always a best practice to do that and something cool that I want to show you is we can take that cancellation token and we can just slap it on to the end of a task so what we're looking at here is task. delay obviously returns a task and for any method that returns a task and let's say you know those Library creators aren't as smart as we are they don't know about these best practices like we do and they don't give us the option to pass in a cancellation token well there's this cool extension method that lives Inn net now called weight async that allows us to essentially bolt on on a cancellation token to any method that we want that returns a task so this is really cool because sometimes developers don't sometimes developers don't give us that option to pass in a cancellation token and now it doesn't matter we can just basically bolt on this cancellation token If This Were to uh if we were to cancel this token then it would cancel this task that it's attached to now all that to say that was just kind of a convenient excuse to show it because test. delay actually does accept a cancellation token so kudos to the net team on writing good net code all right next refactor so now what we're looking at is this method that calls get top stories so just like we talked about we want to go to The Hacker News API we want to get the latest top stories off of Hacker News and then we want to display them onto the screen so this looks pretty good right like we're calling a weit we're inside of an acing task method what's bad about this well again let's think about what's going on under the hood so let's assume thread one kicks off this method so thread one comes into here thread one spins up task. delay there's no await keyword yet so thread one's still going and we come here we come here and then we hit this awake keyword and now thread one returns so great because we don't want thread one getting the a or hitting the apis for us because if this takes 5 seconds and thread one's locked up for five seconds making API calls our apps Frozen for 5 seconds um but this isn't great because even though a different thread's doing it let's say a background thread thread 32 is is running G top stories well when G top stories is done remember we return back to the calling thread so once get top stories finishes once the task is completed Net's going to go hey thread one you're back up and then thread one jumps in here here and that's what clears our collection and then run to this for each Loop and is that really what we want I mean I don't because I want thread one to be free I want thread one to stay ready to listen to the user interact with them scroll draw do whatever the user wants to do that's what I want thread one focused on but as we just saw thread one's kind of getting hijacked to come back in so one thing we can do here let's do it in line first we can tack on configure a we false so configure we false kind of like uh what we saw just a second ago with DOT weight async this is an extension method that again bolts onto the task so it doesn't affect the task but what it does it tells do net I don't need to return to the calling thread it tells net hey when I'm done when get top stories is completed grab any thread from the thread pool Don't Wait For Thread one and this is really cool because you know what if you know this is a pretty simple app sure but what if I made a game and there's a lot of you know the screen's constantly moving I'm constantly drawing objects on the screen really really working thread one well if I had to return to thread one I got to wait till thread one's ready so if thread one's busy drawing stuff on the screen it might be a little while I mean little while right like micros seconds we're talking but a little while we're just kind of hanging out here our coach is kind of hanging out it's like yep everybody's ready but thread 's still busy so guess we'll just wait so by tacking on configur O8 false we tell net I don't care and for me as as a mobile app developer uhet Maui developer everything we do is in the mvvm architecture so if I have any other mvvm fans out there my rule of them is if I'm not in my view layer so for mvvm that means I'm in my view model layer I'm in my services layer I'm somewhere where this code doesn't touch the UI this is all business logic so if I'm in my view model layer I know none of this code touches the UI then I configure a weight false everywhere um the I'll say the one downside about configure weight false is once you learn about it you're going to start adding it everywhere and yeah Denny does and then once you once you do you kind of wish there was a default but there's no default uh there's no way to tell net hey let's make configurate false to default because I've got a thousand async methods 99,999 of them want configurate false and I only want this one to say configurate true so I'll let you know for that one but now we have to pen configurate false everywhere I've been trying to poke the net team to get them to let us add a default for it but until then we'll just depend configurate false and again this means that we don't return to the calling thread so our code will execute a little bit more quickly and in the case of this this app that it free keeps the UI thread UI thread free to interact with the user all right so check that one off coming down all right now we're in the finally block and we're back to that minimum refresh time task so again this is just a task. delay that I kicked off up here so when I initialized this variable when I called task. delay it started running so this timer this this 2cond timer started running way back here and now I just want to make sure it's been at least 2 seconds before I tell the UI to stop refreshing to remove that little spinny activity indicator at the top um but the problem here is I'm calling dot weight now if there's one thing you take away from this session today it's never never never never never never never never call weightweight is really really dangerous and I'll tell you why so witht weight What's Happening Here is let's say Say thread five we're on thread five now so thread five comes into here and it hits minimum refresh time task and it's still got about a second left before so with DOT weight what happens is you know like we talked about up here normally when a thread sees a weight it returns it either goes back to the thread pool or if it's the main thread it goes back to the UI and to the user uh but with weight weight says no no no no you don't get to go anywhere you're gonna stay right here so weight hijacks that calling thread and says you stay here and it still spends up another thread for that background thread so now with DOT weight we have two things that are really bad the first being if we call that on thread one the main thread well we've just locked our main thread and now our app's Frozen until this is done and we're using two threads when when we only should be using one so even if you don't write any code that has UI maybe you're sitting there going ah Brandon I I make apis I'm a I'm a backend developer who cares about whether the UI gets tho gets frozen well remember your server has a finite number of threads in its thread pool and so if you're calling weight on the server side even though there's no UI to worry about freezing well every time you called out wait you're using two threads instead of one and eventually that server is going to get slammed you're going to hit what's called threadpool exhaustion where now all the threads are used up and your server's basically crippled so not only is weight bad because it'll freeze our UI if that if we call it on the on the UI thread but it also cause our servers to hit that threadpool exhaustion more quickly so the right way to do this is just to await this task and we can even call configurate false here like I said I I put it everywhere in my view model so you'll be seeing that a lot today um but let me let me just show you something real quick because you know it's it's super rare nowadays it's super super rare but there is still a 0.001% chance that you might have to call do weight maybe there's an old library you're using that a developer created before asyn a weight existed before task existed and the only way to do it is to call do weight well if you're in this case and again this should be very rare 99.999% of the time you should not be using dot weight but if we're in this scenario what we actually want to call is get a waiter get result now this is still not great geta waer get result does literally the same thing it's still going to hijack that calling thread it's going to say nope thread one you stay right here well I spin up another thread to execute this in the background so we're still locking the calling thread we're still using two threads when we only should be using one but we get better exception handling if that's the right word for this um so one of the problems with weight and maybe you've seen this is if if this code were to throw an exception like yeah this is just task. delay it's probably not going to throw an exception unless this token gets cancelled but let's pretend this is a a long running method and it does throw an exception weight does rethrow that exception for us so just like the await keyword rethrows the exception that gets caught inside of move next weight will do that but when weight throws an exception it throws what's called a system. agregate exception and those are a little weird and it's an aggregate exception is an exception that can hold exceptions so it makes sense why the net team did this because maybe there was a couple exceptions thrown inside of our uh inside of our task so it wants to Bubble Up all of those to us um but for debugging for reading stack traces it makes things a lot more difficult uh especially for new developers so if you've never seen a system. aggregate exception before you might not know that you have to actually dig another layer into it to find the exceptions that are the collection of exceptions inside the aggregate exception and especially with new developers you know those can be really uh difficult to understand and track down so if we instead use geta wa or get result again it does the same dangerous bad behaviors. weight we don't want to do this but if we have to uh get wa get result get a waiter G result is a little bit better because it'll actually rethrow our exception so whatever exception happened in our code that's the exception we get back from geta wait or got get result so it'll make your life easier but in this case we don't got to do that we're inside of async task method so we're going to do best practices and call just a weight and configure weight false and if you didn't know yes you can use async a weight inide of finally blocks okay next on the list so we've got our GP stories method and if we look at it we're making actually two API calls so The Hacker News API is a little a little hokey I wish it was better but you have to first make a call to say hey give me all the IDS for the top stories and then they give you the IDS and then you make another API call for each story that you have to iterate over to get each of them back so it's not great you know why not just give it to me all at once I just want the top stories like what else is Hacker News for other than the top stories but this is the way they designed it um and oh something cool before I forget this is a a new little toy inside ofet 8 uh this has nothing to do with asyn weight but I wanted to show it off here because I am a huge huge fan of immutability so when when I make API calls in my apps and I get data back from another source SCE I don't want that data to change I've been using I readon list the interface I readon list instead of list because that actually will store things inside of a list and you can't change it well in net 8 they came out with this new namespace called system. collections. Frozen and one of them is called Frozen set and there's also Frozen dictionary and kind of similar to a readon list or an mutable list uh this allow this will make sure nothing changes inside you know one of the problems with I readon list is you could still change something if you worked hard enough you could still you know make a reference to something inside of it and swap them out you know with reference types. net can get a little hokey like that and we can kind of hack our own code accidentally a little bit but Frozen set it'll never change so that's why I've got this here because I want to get these top stories back up here and I always want that pure data that came back from the API because who knows maybe you gotta you got to double check it later like hey what what data actually did come back and if we already modified that data that'd be bad so welcome to Frozen set but back to asyn stuff so we're looking at this code here and what are we doing we're getting those top story IDs so we got them and then once we get those IDs we have to iterate over them like I said this this API kind of sucks because now I've got to one by one get make 50 API calls to get 50 stories so I got to make this API call get a new story add it to the list I got to make another API call get the story add to the list and you know if each API call takes 10 seconds and I'm making 50 50 of them serially well my users aren't going to like that so so what can we do here well instead of rewriting this I'm going to jump over here to the good async way practices to show you something that's really really cool inside of net now that we can utilize and it's called I async inumerable so with i async inumerable before we look at the code before we look at that rewrite let's look at how we use it so in the good code here this is our refresh method again but you know we're passing in cancellation tokens best practices um and then here now we see this await for each Loop and this is really really cool because if we look at the bad code side by side the bad code when it calls get stories it just has to wait till all those stories have been retrieved so again it's just sitting there for let's say 500 seconds while we make 50 API calls and then it can display them on the screen with IAS sync innumerable we have this await for each Loop let's make this full screen where we can basically pass in an async method get top stories is an async method um it's a little weird because it's returning async inumerable instead of task but we can still use the async keyword we can still use the awake keyword um but what's cool is once one story is retrieved I can take action on it and if I if I relaunch the app here this is running with the good view model so the good practices you'll see the app updates in real time it doesn't wait for each to come back I'm able to kick off 50 background tasks and then as they finish I can Surface them to the user so even if and you know mobile phones are fickle maybe you're on the bus maybe the you go through a tunnel your internet connection sucks we're at a conference where there's terrible Wi-Fi although I think the Wi-Fi has been pretty good here so far at least for me uh we we don't want the user just sitting there looking at like just little spinning indicator it's so much better if we can start giving them the data first or as as we're waiting for all of it to finish rather so so how does this work this is and this is a little weird if you've never implemented in iyn inumerable before um it's going to feel a little icky but man the results are so good that it's totally worth it um so the first thing I want to point out is these parameters so like I mentioned earlier we always want to be able to pass in a cancellation token if we're making an async task method or even an async i async enumerable method um give me the option to give you a cancellation token because if it's taken more than 10 seconds maybe I just want to call it off so we can still do that but I think nurmes got some extra logic in it that we can take advantage of so there's this enumerator cancellation attribute and if we assign that here as our cancellation token parameter to our cancellation token parameter now now this I async inumerable knows that if this token is ever cancelled stop so and again we shouldn't but let's say none of our methods in here accepted cancellation token so we're not we're not doing anything with this cancellation token it's not being used anywhere um that's okay because thewait for each Loop will automatically break its iteration once it sees that this cancellation token has completed as long as we put this attribute in it now should we ignore it no we should always pass in those cancellation tokens so let's put them back here um so keep that in mind I think you do get yelled at I think I'll get a do I get a squiggle oh wait where'd it go if I get rid of just that part yeah we get a little bit of a squiggle there so it's letting me know that you've got a what's exactly it say you've got a there we go we should decorate with the enumerator cancellation Togo and attribute blah blah blah blah blah so if you forget hopefully that's enough to remind you as well um but yeah if we look at this code the way we take a method like this where we are in the bad way doing a four each Loop and iterating over one story at a time and turn that into an isync inumerable is we basically use a list of tasks so I I created this new list of task of type story Model because that's that's what my API returns back so the API is going to give me a task of type story Model and what I do I say hey for every top Story ID basically kick off that task so kick it off in the background like this get story right here this returns a task but I'm not awaiting it I'm just putting all those tasks into a list because then what I do is I put it in a while loop and I say get top story task list. any if that's still true keep iterating through here and what we get is or what we can do is we can take advantage of task. when any so task. when any totally totally cool um what it does is you pass into task. when a collection so you pass in an inumerable and anytime or rather to be more specific you pass in an inumerable of type t so if you have a list of tasks you can pass those into task. when any and as soon as one of those tasks is completed it'll return it to me so I say a wait t. when any of course configurate false and I get this completed get story task here so the first thing I do is I remove that from my list because I know it's done I don't need to I don't need to await it anymore and then I get the result from that story and I call yield return now if you're familiar with I inumerable you've maybe done a yield return before um but maybe you're not like who who uses yield return really anymore for I innumerables I don't but for I async innumerable what yield return does and the reason it's so beneficial is as soon as we hit this yield return our code jumps into this for each block so now the yield return happens and we can iterate on what just that one result we got back and then the code returns back here to resume the while loop so in this manner we're able to kick off all the background tasks we need and then we can show them to the user as they complete so super super useful like I said creating a list of tasks is a little weird and then a weight dot task. whenn is a little weird but man the the benefits of isnc and neurable huge huge huge so highly highly recommend that for all of our apps just so we don't have to leave the user sitting there waiting for something to happen all right so let's pretend like we refactored that here we're a little low on time so I'm not going to rewrite this whole method for us all right so next one we've got a method called get story This Is Calling The Hacker News API it's getting the story cancellation tokens good um we're not doing configur weight fall so let's slap that in there that'd be good but what else could we do because we're using a weight we're using async configure false well something that's kind of cool here is that if we look at the return type of get story it returns a type of task story Model and if I look at my method My Method signature it also returns a type of task story Model and the only place in this method where I'm using the awake keyword is in the return statement so something I can do I can get get rid of async I can get rid of a weight and I can just return that task now why is this good why would I want to do this well let's put those back and think about what's going on again so again let's pretend thread one kicks off this method so thread one jumps into get story and immediately hits the awake keyword so thread one leaves we grab a background thread now thread 52 is running get story in the background when thread 52 is done it's going to well this case we have configurate false so it won't return to the calling thread it'll let net know hey I'm all done so with configurate false net just goes to the thread pool grabs whatever threads free and let's say thread seven now returns this method so what just happened was with this one line of code we had to switch threads twice and if we can avoid that we should because switching threads switching context is expensive in net so if we get rid of async get get rid of a weight and just return that task essentially what we're doing is deferring that context switch that thread change up to whoever calls this method so wherever I got in the code where are we calling it up here so up here where we call awake get story it's returning that task that we just returned so so by just returning the task we can save a little bit on async of weight our codee's going to run a little bit faster and we got rid of the asyn keyword so our code gets a little bit smaller I mean 100 btes smaller but still a little bit smaller now I've got one more down here um and this one's kind of similar right I mean the only real difference is we've got this if statement to you know instead of getting the top story IDs every time like do they really change that much and if we've already retrieved them and they've done a pull the user does another pull to refresh well I said if it's only been an hour just use the ones we already got we don't need to make another API call so that's a little different we've got a try catch block here but like I was just saying if the only place we're using the awake keyword is in the return statement we can get rid of it and this up here will yell at us because we have to say from result just to pass that into a task since we do have to return a task and this is good right no this is actually really bad so this is the exception um so this is fine up here but down here we're inside of a tri catch block so if we think about what happens is let's say our code comes in the thread gets to here and then the thread hits this return statement it returns we've we've exited this method which means we've exited this TR catch block so now if this method throws an exception we're never going to catch it here because we've already left we've already returned so so we can't do that actually so in this case because we're inside of a tri catch block and we want to catch that exception be able to handle it we do actually want to return a weight so most of the times if the only place in your method where you use the awake keyword is in the return statement you can just return the task but in cases like this where we're inside of a TR catch block or maybe we're inside of a using block like we're disposing of an object make sure to keep the return await because speaking from experience you're going to get real weird bugs in your app and you're like why didn't this why didn't this bug get caught I catch I got a catch right here so we're going to keep this AWA in here we're going to keep configure a weight false um but what we can do to refactor this method is we can use something called a value task now a value task is kind of similar to a task on the surface it feels super super super super similar like if I look at the code that calls this method I still await it I still can basically treat it like a task um but with a value task it's a value type so in net we have value types and we have reference types value types get put onto a stack reference types get put onto a heap and if you remember from your data structures days adding something to a stack super quick super simple you just popping it or push it into the top putting something on a heap more expensive because a heap has to be indexed so adding something to a heap is a little bit more expensive and the reason we can use value task here is because of this part of the code right here so if we look at the code the first time we call it we're not going to have anything our data is not going to be recent so we're not going to return here we're going to come down here and call the API but then the user does a pull to refresh and the second time this code runs we're going to return right away and the third time this code runs we're going to return right away and for the whole next hour the next time this code runs we're going to return right away and if you have a scenario like this where you have a method where the hot path where something like this where nine times out of 10 you're going to return without ever using the awake keyword you can return a value task and you get a nice little performance bump so you don't want a value task everywhere if if your method always calls the awake keyword keep using a task that's what it's there for but with value task we get a little bit of a performance bump because we don't have to go through all the overhead of creating a task and putting out the Heap and they're a little bit more complex than value tasks anyway so we can take advantage of that here and again our app now works a little a little bit faster okay so let's let's do a quick review like I said this is years and years this is like what 15 years of async weight content smash into an hour so what do we talk about well never use weight never use do result we didn't show do result in the examples but it does the same thing that weight does it's going to lock the calling thread it's going to keep it it's going to hold it hostage while the other background threads going we're going to be using two threads we should only be using one so instead we should just use the awake keyword but if in that really really rare instance where we can't use a weight we should use get a weit or get result and get a wait or get result actually replaces both it has the same behavior as that weight and it has the same behavior as result so we can go through all of our code and hopefully replace replace weight with a weight but again in the rare instance where you have to get a wait or get result fired forget task so if you want to run a task on a background thread that's totally cool I do it all the time myself feel free to grab my n package ASN await best practices and then you can use that fire and forget extension method or if your company doesn't like you adding new get packages you can just copy paste the code that's totally cool too avoid return a weit so like we saw earlier if we're if the only place in our method where we use the awake keyword is in the return statement we could just return the task except if you're in a TR catch block or if you're in a using block again this is totally from experience I I return out of a using block once and all of a sudden I'm getting object disposed errors because what I was trying to do in the background that object got disposed so learn from my pain learn from my examples if you're using a TR catch block if you're inside of a using block keep the return await utilize configurate false so if you don't need to return to that calling thread configurate false will help your code run a little bit faster there is a caveat to this net has something called a synchronization context in most Frameworks uh so I've listed out a couple the popular ones or at least the ones I could think of off the top of my head um and kind of a rule of thumb is if it's got a UI it probably has synchronization context because the whole point of a synchronization context is to be able to help Net return to the UI thread so if you have a UI there's a likely likely chance that you have a synchronization context and if that's the case configure weight false will never return back to that calling thread um if there is no config if there is no synchronization context like for example asp.net core doesn't have a conf configuration context synchronization context so in asp.net core you can still use configure weight false I actually do it's partly out of habit and partly kind of best practices because I copy paste code from a lot of places a lot so I just want to follow best practices I still use it in asp core but because asp.net core does not have a synchronization context calling configur rate false is the same thing as calling configur rate true they they don't make a difference because that whole configure weight is actually telling the synchronization context whether or not to returned back to the calling thread so little caveat there uh value task so again if the hot path of your method does not use the await keyword if nine times out of 10 that code in your method will never call await have it return a value task instead of a task isnc inumerable like we saw this is our await for each Loop so for streaming data if we want to update the UI as uh data returns back uh this will give a much much better user experience just keep in mind we do still want to use cancellation tokens and make sure to tell that for each Loop that await for each Loop this enumerator cancellation attribute to let it know that hey this is your cancellation token so anytime this cancels you're done you don't need to keep looping and we async so if you ever have a method where the developer just didn't allow you to include a cancellation token or pass in a cancellation token you can just bolt on a cancellation token with this extension method do weight async and it works just as if they did so it's kind of a little workaround for us for super super helpful for backwards compatibility last one and we actually didn't cover this in the example code mostly because I couldn't really come up with a good example in this app to make this work but if you've ever heard of I disposable there's now an i async disposable and it works super super similar so we have our using block and just like with I disposable when the end of the when we reach the end of the using block that object will be disposed so in this case we're neing up file stream we're going to write all our code we saving data to a file and at the end of this using block the file stream will be disposed so we're being you know Good Shepherd of our memory and good net developers well kind of the same idea with AWA using so there's certain libraries like file stream that are kind of heavy to initialize heavy to tear down and we don't really want to do that on the main thread so they've given us now I async disposable and to use that all we have to do is say await using treat it exactly the same we can even you can see at the end there at the very very end it says do configurate false so we can still tell it configurate false and then the way this works at the end of that block that's when it'll actually await it so it's a little the syntax makes sense and the fact that it looks good um but it's a little weird because that await doesn't really happen till the end but it'll still happen so if you haven't had a chance yet this is this is the time make sure to take out your phones grab a picture of the QR code grab this link because this is where you can find all the resources from today so I've uploaded all of the slides from this presentation I've uploaded a video from a previous time I've given this talk so you don't have to worry about memorizing anything you can re-watch the video you can share it with your co-workers um I've also included a bunch of helpful links in here so if you want to dive deeper into value task you want to dive deeper into isnc disposable you want to dive deeper into isnc innumerable all those cool things we talked about today are on that website for you as well thank you [Applause]
Info
Channel: NDC Conferences
Views: 73,392
Rating: undefined out of 5
Keywords: Brandon Minnick, NDC, Conferences, 2023, Live, Fun, Copenhagen, .NET, Async, .NET 8, C#, C#12
Id: zhCRX3B7qwY
Channel Id: undefined
Length: 60min 2sec (3602 seconds)
Published: Thu Oct 26 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.