Correcting Common Async/Await Mistakes in .NET 8 - Brandon Minnick - NDC Porto 2023

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
well welcome everybody wow okay welcome everybody thanks so much for joining me today my name is Brandon mik and in this session we'll be talking about correcting common async await mistakes in.net now I'm going to be moving fast because async await came out a long time ago and every new release ofn net has come out with more async aake goodies so there's more had to add into this presentation so it's about an hour and a half worth of material we're going to cram into an hour but don't worry because if you go to this website that I made for you today Cod traveler. async best practices you can find everything recovering today you can find a recording of this talk from a previous NDC session you can find the slides we're going to be looking at open source samples you can find those on GitHub they're all linked there as long along with a bunch of resources if you want to dive further down this Rabbit Hole um this is a a special topic for me because I'm I'm actually a mobile app developer I make apps for IOS and Android using zamarin and net Maui and everything we're talking about today are mistakes that I've made and it was until I kind of went down the rabbit hole did I really understand what was going on so let's jump into it because we got a lot to cover now we're going to break down this method and see what's really going on uh yes this is old code still using web client no you should not be using web client this is not part of the best practices this is old code like I said I've been given this talk for a few years now slides are a little complicated as you'll see in a second and I haven't updated them yet but what we're looking at here is an async task method called read data from URL we pass in a string URL then we new up a web client web client calls download data task async and we await it which is good we want to we want to await that task because if download data task async takes a couple seconds well we want that to be happening on a background thread we don't want our UI to be tied or UI thread to be tied up and processing stuff in the background so what's actually going on here well let's say thread one kicks off this method so thread one jumps into read data from URL it news up web client and in the signs bite array B bite array result um but then thread one hits that awake keyword and it returns it goes back to interact with the user and this is really important because again thread one's our calling thread thread one's our UI thread thread one's our main thread and thread one is the only thread that can interact with the user so if we have our app on the screen and the user starts scrolling thread one's the only one that can redraw the UI or if the user Taps a button thread one's the only thread that can respond to that button tap so if thread one was is running download data task async well let's say it takes 5 Seconds thread one's then Frozen for 5 seconds it can't respond any button Taps it can't respond to any swipes and our app's basically Frozen and what happens next our users you know they force quit the app they give us a onear review on the App Store and life's not good but luckily we have async await so as soon as we call the await keyword a background thread will run that task so a background thread runs download data task async uh in this case we call it thread 2 but it could be any thread from the thread pool uh there's a we don't have time to dive into it today but net has a thread pool that it manages for us so every other thread other than thread one is a background thread and depending on the size of your server your phone whatever is running the code determines the size of the thread pool so if you have more CPU power more RAM you'll have a bigger thread pool than say like a little Raspberry Pi but when thread 2 is done when download t t when when download data task async is finished thread 2 shouts back to thread one and goes hey I'm all done you're back up and thread one jumps back in and thread one continues running our code so thread one uh creates that string data by uh passing in the bite array and then kicks off that load data method so to really understand how all this works because the threads really talk to each other no they're not people they're just computer programs we've got to look at the compiler gener ated code so what you might not know is anytime you create an async task method the compiler creates all of this code for you now the first thing you'll notice is the compiler generated a class I didn't write a class I made a method but when this code compiles the compiler turns our async task method into a class that inherits from I async State machine now if you look closely these variable names look very very similar like this class is called angle bracket read data from URL angle bracket dore1 you know it's got a little bit of a funny name but that's basically the same uh same as my method name and the compiler does this on purpose because we can't we can't use angle brackets for our our class names that's an invalid class name if we were to write that code but the compiler can do it and it does it on purpose so that it avoids comp avoids any conflicts so this way it can generate a class it knows there's no other class in our code called angle bracket read data from URL angle bracket because again that'd be illegal for us to write but compiler knows the rules so it can break its own rules so it generates all this code for us it generates also all these fields so every private or rather every local variable that we created in our code becomes a field so we have private string data we've got private bite array result we have private web client WC and we have public string URL now that's public because if you remember URL was the parameter we passed into the method now the biggest thing in here and this is where we're going to focus a lot on today is this move next method and this is what used to baffle me because I I started publishing apps to the App Store uh about eight years ago and I I thought understood async A8 I didn't uh but uh I would get these weird bug reports these weird stack traces would come back in like I would see move next all over these stack traces and I'm like what is this method you know I I never wrote a method in my code called move next and well it turns out it's because it was autogenerated from the compiler so let's actually look at what move next looks like so this is the code that the compiler generated for move next and essentially what this is is is a giant State machine so you can see it's essentially a giant switch statement with multiple cases in it and the way this works every time we use the await keyword the compiler will generate another case so in this example we use the awake keyword once so we have two cases if we were to use the awake keyword twice we would have three cases if we use the awake keyword four times we would have five cases so for every awake keyword it generates another case in our switch statement and if we look at case Zero we can see this is basically our code leading up to that first await so we we initialize that web client we kick off download data task async s it to a variable called a waiter zero and then we return so this is this is the magic this is how thread one knows to leave the method and it can just return to continue interacting with the user continue handling those button Taps scrolling drawing on the screen the because it knows download data task async is going to be run in the background on a different thread now before it returns though it sets that value of PC to one and this is important because remember when thread 2 is done it shouts back to thread one and goes hey you're back up and thread one's got to jump back into our code and the way thread one knows where to pick up where it left off is from that variable so it set the new state that PC variable to one so now it knows it just jumps back into case one where it gets the result from download data task async encodes that bite array into a string and runs that method called load data now here is the gotcha this is the biggest problem that causes the most headaches that in uh C code that I see today and it's because everything in move next is wrapped in a tri catch block now now I didn't write a tri catch block but what the compiler did it took all my code and put it inside of this Tri catch block so this means that if any of my code throws an exception it's going to be caught by this exception Handler and now that's usually okay because as long as we use the await keyword when we call the task as long as we say await task that exception gets rethrown but if you don't the task that exception disappears and it's gone forever and this was the biggest problem of mine uh when I first started out when I started making mobile apps using zamin publishing to the App Store and the weird bugs I was seeing was because I thought I was being smart I knew about background threads and I knew about a single weight or at least I thought I did uh so I would say things like task. run and put a bunch of Co in there cuz I knew I wanted the UI thread to be free I wanted this code to run on a background task but I didn't care when this code finished it was just a background task so I didn't a wait task.run so I would just say task. run boom go ahead and what I didn't know was that code was throwing an exception but because I wasn't awaiting the task that exception was caught in this exception Handler and it was never bubbled up to me the developer so I never caught it in any of my testing and it would happen you know in weird scenarios that didn't expect just like exceptions usually do uh now you might thinking well wait a minute we got rid of exceptions that's a good thing right no exceptions are good we want exceptions to be thrown because exceptions happen when there's an exceptional case when there's something that we didn't write code for that the system doesn't know how to handle occurs that's when exceptions get thrown so we want exceptions to be bubbled up to us so that we can handle them gracefully whereas with my code when I said task. run put a bunch of code in it didn't a wait it those exceptions were getting caught never resurfaced to me so that I could handle them and then what happened to my apps was my apps now in this weird state that I never planned for you know it's like if somebody's on a bus using my app and they try to press the submit button and it fails because maybe the bus goes through a tunnel well what happens well I don't know because I'm assuming that it finished successfully I never realized that my exceptions were being swallowed up okay so let's do a quick review the async keyword adds about 100 bytes nowadays this is more of a fun fact than anything else cuz does anybody really care about 100 bytes I don't you know we're talking 100 bytes not 100 kilobytes not 100 megabytes not 100 gigabytes this just 100 bytes so this is more of a fun fact that yes the compiler has to write some extra code for us so that extra code makes our app size a little larger but in reality I don't really care about this but if if you are working on a an embedded system or Raspberry Pi maybe this matters but just know that every async class or every async keyword becomes a class and that adds about a 100 bytes to our app size and we want to await every task because like we said non awaited tasks hide exceptions and that's a bad thing we want exceptions to be rethrown bubbled back up to us as the developer so that we can handle them gracefully and we can tell our app what to do if our app this is internet connection for example okay so let's actually jump into some code here I've I've got a sample app and again this is all open source the links available on on the website and I'll give you a chance to uh grab that link before we finish again today but this sample app is it's just an app that I made to pull down the top stories from Hacker News if you've never heard of Hacker News it's just a it's a cool website where people can share um cool text stories so if you're into that stuff you can check it out I like to flip through it every now and again um and so I made this app that goes out pulls the top stories from The Hacker News API and displays them here in the mobile app so that way I don't have to click through them and I can just tap on any of these and I can read the article right here so let's jump into this code now again don't worry about memorizing anything because you'll see we're working in this class called news view model bad asyn practices but the good good news is I've already completed this for you so there's another view model called good I in away practices where everything we're talking about today is already done so let's jump into it and the first one we have here we have to refactor this method called refresh um now what we're looking at is this is the Constructor of my class and I'm calling this refresh method which is what triggers when the user does uh pull to refresh on my app so this is what's going to go go out to The Hacker News API it's going to get the top stories and I put this in my Constructor because it's kind of a bad experience if a user launches my app and then they have to manually pull to refresh like if you're just greeted with a blank screen you're not going to know what to do you're going to think the app's broken so I want that data to be there so I want to call refresh as soon as this app launches but here in the Constructor you know we can't use async A8 and this is actually a good thing we because Constructors aren't made for async A8 Constructors are literally just to initialize a class as it gets assigned into memory we're just supposed to set some variables set some properties uh we're not supposed to do any long running tasks in the Constructor we're literally just creating an object so that c can put that object in memory for us so we'll never be able to use asyn weight in the Constructor so okay uh what do we do because I just said we need to await every task and now we can't well there's one cool thing we can do I can create a method here called refresh where all a wait that method and then here I'll just call my new method refresh my computer just froze one second guys I have no idea what's going on let's see yep now we're back sorry hopefully that didn't mess up anything back there all right so so now in our Constructor we're calling this method refresh which is void so I don't get those squiggles anymore and inside of this refresh method I'm calling our async task method refresh and I'm able to await it now this is good code it might not feel like good code because it's async void right and what are we told as C developers never use async void async void's only meant for uh vent handlers we should never write an async void method uh well it turns out this is actually a valid use case for async void but my problem with that advice when people say don't ever use it ever use async void is nobody ever tells you why you shouldn't use asyn FOID so let's let's look at why real quick so one one thing that uh bad thing about async void is if we scroll down a little bit we'll see this method here you know it gets the top stories it clears the collection and then it inserts all the new stories into that collection so what if I did something like this where I start adding stories to the collection up here and this is not good because what's what's going to happen is again let's say thread one kicks off this method refresh so thread one comes into here thread one hits this awake key word and as we just learned it's going to return so thread one's going to exit the method and it's going to continue on so now thread one's running this line of code meanwhile a background thread let's say thread two is running our async task refresh method and now both of these both of these threads are interacting with the top Story collection so what's going to happen is we just created a race condition where sometimes thread one's going to win and it's going to be able to add this top story before it gets cleared down here by thread 2 but sometimes thread 2 is going to win maybe you have a really fast internet connection and all those top stories are cashed and it just comes back right away so so this is one of the dangers with async void because if if I didn't see this method right here you know it's this one's apparent it's in my face I call it here it's written two lines of code or few lines of code down below um but what if it wasn't right like what if this method was written by another developer on my team it lives in a different class so I don't see right away that this is async void so I'm just writing my code you know as a good C developer and I'm just using intellisense to feel my way around this code base and I see that refresh returns void and as a c developer it's a totally valid assumption that we can assume because this method returns void that this method will finish running before this next line of code runs but in reality refresh is still running out a background task so we've basically just inserted the future bomb to everybody else on our team by running an async void method so that's one of the big dangers is they might not know it's async void they might not understand that we've just created a race condition so so why else is why else is this bad well let's say this method throws an exception so so now we know every time this method runs it's going to throw an exception and and you might think okay no big deal right I'll just do a little try catch block here I'll catch every single exception that pops up and I'll put the refresh method inside of that problem solved right well not really because again remember when thread one calls this Constructor then thread one goes into refresh and thread one keeps running this code until it hits the awake keyword well thread one returns so thread one continues on and thread one's now down here and it's exited the tri catch block meanwhile that background thread thread two is still running this code so this exception here is about to be thrown and we've already exited the tri catch block so another reason why async void is bad or why it's dangerous is it's almost impossible to catch an exception so okay now now what can we do right because this is this is bad code we don't want to do this we don't want to uh you know set these landmines for future developers on our team well what we can do let's bring this code up and let's blow away async void because I don't like it either even though it is valid um so what I've done for you is I've actually written a library called safe fire and forget uh this so this is an extension method that you can add on to the end of any task and what it does is basically what we just talked about so if I go to the code here because this is actually the code for that Library this is just the sample app that lives in it and we scroll all the way down here to see how it handles safe fire and forget you can see what I'm doing for you is I'm wrapping your task in a TR catch block I'm still calling a wait but this is async void so it's fired forget meaning as soon as let's say Say thread one sees that away keyword it's going to exit it's going to continue on your task is still going to run in the background thread so no worries there um but if your task does throw an exception I also give you the opportunity to handle it so what we can do is we can put a little function in here and say maybe say trace. right line and pass in our exception there's probably a better way to do it but I can tell it safe fire and forget means I'm going to run this in the background and I'm just going to move on so I know that this code will execute right away we're not going to await this and I know it's safe because we're still awaiting that task so we're still able to get that exception if the exception is thrown so uh so highly recommend this like I said all this codes open source so this is this is that website if you haven't pulled it up on your phone yet already uh and you can head to the n get package here called asyn weight best practices uh I've been uh publishing this for a while we've got over a million download so I think other people like it too or because this is all open source you're totally welcome just to copy and paste the code if you don't want to add another dependency to your library but the thing I really really like about sayy and forget is it it's forcing us to be very explicit so if another developer comes along they're going to see safe fire and forget they might not recognize that right away but but once they understand this is us saying we're fire and forgetting we're we're going to tell this task to go off and run and the next line of code's going to run right away it's very much inyour face it's very obvious that what our attentions are okay so first refactor done now scrolling down here we're inside this refresh method again this is what triggers anytime the user does a pull to refresh on the app and the first refactor I have here is for this very iable so what I'm doing here is I'm creating a task as a variable where basically I'm kicking off task. delay and I'm saying wait two seconds and this variable name is called minimum refresh time task uh because I want every time you do a pull to refresh on my app I want you to see that spinning indicator for at least two seconds no matter what even if you have the fastest internet connection in the world I want to make sure you see that animation and so later on this method you'll see we wait for this task to finish before we end the animation and this might feel a little weird or seem a little strange but it's actually really common um I know in the in the mobile World probably in the web world too because we want our users to have a consistent experience you know if they pull a refresh and they get all the data and then let's say all that data gets cached so the next pull to refresh takes half a second half a second might not even be enough time for the animation to happen so they might assume that the app didn't trigger the pull to refresh and so they'll just keep pulling to refresh and next thing you know we're blowing up The Hacker News API because they don't realize that they actually have the latest data so so I want the app to take at least two seconds every time to show the little animation and the reason I want to refactor this is because we have a cancellation token here now as good C developers anytime we kick off a task we should always pass in a cancellation token but sometimes we have use libraries that we don't own so I can't always go and change a method to add a new parameter because maybe I'm using somebody else's nit package right so there's a cool thing we can do and it's called weight async and inside of weight async we can pass in that cancellation token and what this extension method basically does is it bolts that cancellation token onto the original task so now if my original cancellation token is cancelled so we'll test. DeLay So it's a way to essentially bolt on a cancellation token if for whatever reason the the async task method you're calling doesn't allow it but you know again we're good C developers so we're always going to have a cancellation token parameter and as as luck has it so is the net team they also allow us to pass in cancellation tokens so we're just going to pass it in here instead of using weight async since it's already baked in for us okay the next refactor we're calling get top stories so what we're doing we're we're getting the top stories from Hacker News then we're going to clear out the previous collection and then we're going to add them into that collection in a sorted order um and this looks pretty good right like we're using we're using the away keyword async task method um so why do we want to refactor this well if we think about what's going on let's say let's say thread one kicks off refresh right so so thread one creates runs this code thread one runs this code thread one comes all the way up to here until it hits the awake keyword and then thread one returns and so get top stories is going to run on a background thread great that's exactly what we want but remember when get top stories is done it's going to pull thread one back in So thread one now has to jump in to clear my clear my top Story collection and run through this for each Loop and again thread one that's our UI thread the only thing that's can interact with the user so if that's doing something else that means it can't handle that button tap it can't refresh the UI and so you know if this four each Loop takes 5 Seconds well thread one is now locked up for 5 seconds our I is Frozen for 5 seconds so what we can do here is we can add in configur we false now this looks a little weird but it's an it's an extension method on every task that tells net to say I don't care what thread I return on so so again with configurate false thread one will run all this code it'll hit the awake keyword thread one go returns it goes back it's now interacting with our user our apps working great everybody's Happy And now when thread two is done running get top stories it doesn't go call back to thread one it just goes to the thread pool and says hey any available tasks anybody and it just grabs whatever task is or whatever thread is available to continue and running on our code so highly recommend configure weight false uh for a couple reasons one we've now ensured we're not going back to thread one so you know what if what if this was a game right like what if what if this was a a game that had to run at 60 frames a second 120 frames a second so thread one's going to be really busy drawing the UI and if we didn't have configur weight false and by the way there is configure weight true but that's just the default so configure weight true is the same as not doing anything um but if we did call back to thread one and thread one's really busy well our code just sitting here it's just paused until thread one is available to jump back in but with configur weight false we can say hey I don't care what thread this continues on threadpool just give me whatever whatever thread is free and I'm happy so uh my rule of thumb again I'm a mobile developer and we use the mvvm architecture a lot so my realle thumb is in my view models in my Surface layer any parts of my code that never touch the UI I configure away fals everything uh if I am in my UI layer so maybe I'm handling a button tap and I need to redraw something on the page um then obviously don't use configurate false because you want to return to the UI thread to uh update the UI but configurate false I add it everywhere where I'm not interacting with the UI all right another refactor done so let's keep going down here and here it is so now we see this is that task we created earlier so we have our minimum refresh time task and let's bump this up a little bit um and I'm calling dot weight and this is bad um I I have a rule of thumb with DOT weight and that's never never never never use dweight dweight is really really bad um the reason it's really bad is when this code gets called so now we know we're we're not on thread one anymore we know we're on a background thread thanks to configurate false so let's say thread five is running this code now it's whatever random thread that the thread pool gave us um so let's say thread five hits this code and it hits weight well what weight does it doesn't release the calling threat so weight says uhuh thread five you stay right here you're not able to return you stay here I'm still going to run this on a background task so that uh minimum refresh time task is still running on a different thread but meanwhile we've locked up thread five so now without weight we're locking up two threads when we only really should be using one now that can be really really bad if we didn't put in configur weight false here like if if we were still returning to the calling thread if thread one was down here well now we've just Frozen our app so really really bad stuff and you know even if even if you don't make apps that have UI like maybe you're sitting there Going H well I uh I only make apis for the back end I don't have to worry about this stuff it matters for you too because every time we call thread weight or dot every time you call Dot weight you're using two tasks when you only should be using one and eventually your server is going to hit what's called threadpool exhaustion because there's a limited amount of threads in that thread pool which again the beefer beefier your server the more threads you'll have access to um but again with weight you're going to use two threads every time you only need to be using one and eventually you're going to run out of it and that's called threadpool exhaustion so so what we should be doing down here is just awaiting it easy um but the reason I added this in here is it's it's rare nowadays but there might still be times where you can't call async A8 I see it in Old interfaces like I've had old interfaces that return a Bo not a task of type buol that just return a Bool and I have to implement this method because it's part of the interface but that doesn't let me use async a weight so in those rare cases what do we do because you know Brandon just told you to never use weight well there's a better way we can call get a waiter get resol now get waiter get resol is still bad because it's still still blocking it does the same behavior as weight so if thread five is calling this code down here thread five is still going to be blocked it's still going to be locked get waiter get res doesn't release that thread it's still going to run uh minimum refresh time task on a background thread so we're still using two threads and we should only be using one but the benefit of geta waiter get result is if our code throws an exception get a waer g result throws our exception um the one good side of weight is it also rethrows the exception just like uh the await keyword does but weight and result they both throw what's called a system. aggregate exception which if you've never seen those it's an aggregate exception is an exception that holds multiple exceptions so in my experience get a waiter get results way better because it'll actually throw my exception with my code in it whereas with the aggregate exception that's just another step for me when I'm debugging code trying to figure out what's wrong looking at stack traces because I have to dig into the system. agregate exception with do weight but with geta we or get result it'll throw our code with our exception so it's a little better than do weight but again if you can if you can avoid it avoid it and instead use the aw keyword okay so next thing to refactor we've got a method called get top stories uh turns a task of type Frozen set and this is actually pretty cool Frozen set is something brand new in net 8 this actually has nothing to do with async A8 but I'm really excited about it so I want to share it with you here um a frozen set you can think of it like a list that can never change uh maybe you're familiar with uh immutable collections like there's already a thing called immutable list and a mutable array and there's I read only list and so it's like these things already exist in net why is the net team adding this in in net 8 um with this they've created this new namespace called system. collections. Frozen so there's also a frozen dictionary you can use well the reason this is being added in is because yes you might have an immutable list or you might be using a type of IAD only list but it's not really true because you know C we've got reference types and anybody can always kind of come in and swap out a reference to an object in your list and all of a sudden your list is different now so with Frozen set your list will never change so I'm really excited for this because the way I like to write my code is anytime I get results back from an API or pull anything out of a database I like to keep that immutable I don't ever want that to change in case I need to reference it later so what I love about Frozen set is it'll never change there's no way for another developer on my team to accidentally swap something out in the list and huge performance benefit because now net knows there's never ever going to be anything right writing to this set it's actually not a list because it's not indexed but there's never ever going to be anything written we're only going to create it once so it can do a bunch of uh performance optimization so your reads are going to be super fast on Frozen set so like I said nothing to do with asyn weight just highly recommend it I'm a big fan of Frozen uh the new Frozen collections name space and like I said there's also a frozen dictionary too which also highly recommend so we're looking at this code here right we we're supposed to refactor this method but you know I'm seeing we're using async of weight we're calling configur weight false and this looks pretty good I mean you know it it does kind of suck that we have to call configure weight false every time um but that's kind of is what it is you know I worked at Microsoft for about seven years and worked closely with a net team and I would always poke him I was like Hey guys give me give me a flag give me a global flag that I can say configurate false is the default cuz I'm tired of writing it so much but they haven't done it yet maybe someday we'll get that uh it doesn't exist today it doesn't exist in upcoming version of net 8 so for now we just got to call configure weight false every time uh but that's okay and so this looks pretty good but one thing we can do better is we could turn this into an IAS sync innumerable now we don't have a ton of time today so I'm going to cheat a little bit and I'm going to go over to my good async O8 practices class and show you what this looks like um so if you haven't heard of IAS sync inumerable IAS sync inumerable is what allows us to call a wait for each so maybe you've used a wait for each in your code uh if you haven't what it's doing is it's awaiting this task um but this task is innumerable meaning you can enumerate through it you can move to the next one so what happens is we call a weight and again we still want to call configure weight false we always want to pass in our cancellation token because again we're good C developers um but basically it's just a for each Loop that every time there's a new value provided back in this async method it'll run this code so what's really cool about this and here I'll jump back to the bad one um to show you for reference you know this bad one is I don't like it because we have to first get the top story IDs The Hacker News API kind of sucks because I can't just say give me the top stories instead I have to call the first API and say give me the IDS of all the top stories and then once I have the ID numbers I have to make subsequent calls for every single story to say hey for this ID give me the story so I don't know who came up with that idea who wrote that API I hate it but that's what this code is doing right it's doing everything in serial so one at a time it's a waiting getting the next story and this is really slow because what if I have 50 top stories what if I have a 100 top stories well now I'm going to have 100 API calls one after each other that sucks so with isync inumerable if we jump into here what I'm doing is I'm creating a list of tasks and what I'm doing is saying for every story that I have an ID for kick off this task so this task the API hackernews API service. gstory will start running so now I have 50 API calls all running in parallel and then the way I handle this is I just have a while loop so in my while loop I have I just say do I still have any tasks left and I also give the user the ability to limit it so maybe they only want 10 stories um they can do that so as long as we haven't hit their limit and as long as we still have tasks left uh we can use what's called task. when any so task pass. win any we pass in a list and anytime well specifically a list of tasks and anytime a task in that list has completed it'll give it to us so when I say await task. when any the first API call to return and complete it gives me that task of type story Model so what I do is because it's done now I can remove it from my list and then I grab the story out of it and we call yield return to return that story with which again triggers our for each Loop so then this code runs it's added to the list and our user is able to see these stories rolling in in real time and it's way faster because we're making all those API calls at the same time instead of waiting for them one at a time to come back um if you've never heard of yield return um this doesn't actually exit the method it's it's an ie numerable thing where it can return partial results back so that's how it's getting those partial results back to our a wait for each Loop um one other cool thing with isync enumerable because again good C developers always allow us to pass into cancellation tokens so we've got that here but iyn enumerable has this cool thing built in called this enumerator cancellation where if I just apply this attribute to let net know that yes this is the token that I want to cancel this method then in my code I never have to write anything like this never have to check that cancellation token I never have to see if it's been cancelled I never have to worry about throwing it because as soon as this cancellation token is cancelled because I've given it this enumerator cancellation attribute net knows okay break this method we got to move on we're done so really really cool highly recommend I think innumerable you know the um the Json do the net Json team system. text. Json there we go the system. tex. Json team uh they've added in a bunch of features to support isnc innumerable so a lot of it's baked into HTTP client for us too so all of this is really really cool you can even use it in your apis that you build in asp.net core okay so we'll pretend like we refactored that even though even though we didn't but we looked at it we talked about it that still counts okay the next thing to refactor um I have this method called get story and all it's doing is calling my Hacker News API service and calling that g story API and if we look at this the git story method returns a type of task story Model and my git story method also returns a task of type story Model so there's something really cool we can do here we can actually get rid of the async keyword and we can get rid of the awake keyword and we can just return that task now this probably feels a little weird because we're not awaiting it um but this is really cool because we just got a little performance benefit because if I go back and put that code back in and we think about what's going on here if we say thread one calls this method well thread one comes in here immediately hits the await keyword and returns this code runs we get the top story and then I mean if we had configurate false it'd be a little bit better here but without configur weight false now it's going to call thread one back in here and thread one's just coming in here to return the value so we just switched threads twice which is not cheap I mean it's it's relatively fast you know we're talking Nan but we don't need to do that because we can just return the task and then we avoid an additional unnecessary thread switch so we can get a little performance boost by doing this so one of my rules of thumb is if you have a method where the only place you use the await keyword is in the return statement instead of saying return a wait just return that task check okay last thing to refactor here we've got another method called get top story IDs and you know I I just said it I just said anytime we have a method where we only use the awake keyword in the return statement we should return the task so is that should be what we're doing here right you now we can return the task and we've got another return statement up here but that's okay because we can say task. from result and we can pass in there we go so all this looks fine this is going to compile but this is actually a trick question this is this is one of the exceptions to that to that rule that I just introduced you to because in this case we're inside of a tri catch block and so if we if we think about what's happening in this code uh let's say again thread one calls this method it comes into here if statements false so we get to here and we go to get the top story IDs but thread one just return so we just hit this return statement so we've exited this method we're no longer inside of get top story IDs anymore which means we're also no longer inside of this Tri catch block so caveat to the rule that I just shared with you is if you do have a return await inside of a tri catch block keep it and again that's that's from a lot of experience where I've made that mistake one too many times um another another place where we want to keep that is if we have a using block so think about I disposable right we can say using blah blah blah we've got our brackets and at the end of that using block that object's going to be disposed and I've also added bugs to my app where I didn't return a wait because I said I don't need it I want to save the thread switch and again what happened was we returned we left the meth method which means we left the using block which means that object that I needed just got disposed and weird errors happened so if you're inside of a TR catch block or if you're inside of a using block keep return a weight uh if you're not you don't need it now what can we do here you know I mean I see right away we don't have configurate false shame on me so let's put that in there um but this all looks pretty good however we have this line of code here we have we have a spot in our method where we don't use the awake keyword so what I've done here is again to prevent from my users from just hammering The Hacker News API I say if you've if you've already gotten the data within the last hour you know the top stories don't change that much you know I don't want somebody sitting there and just calling the API all day um so to try and limit that I say if the data is recent within an hour then just grab the IDS we've already got you know we can refresh the links and we can still do everything but we don't need to make another API call and now we're in an async method where we're not calling the await keyword and if you think about this this is the hot path of our method this path here will be called more than this path so the first time the user launches our app the data is not going to be recent so it's got to go get those top story IDs but then the second time they refresh the app we're going to go down this part of the method and the third time we're g to go down this part of the method so the majority of the time this method gets called we don't use the await keyword there's something cool we can do instead of returning a task we can return a value task now value task is kind of the same thing as a task it's it's not exactly the same and again I've got some documentation on the website for you where if you want to dig down deep into this you can see the exactly all the differences uh but the biggest difference is value task is a value type and if remember in C we've got reference types and we've got value types reference types live in the Heap value types live in the stack and the reason why value task is better in this case because it can't do everything a task can do but if we're just doing a method return totally fine you have nothing to worry about with value task um but because value task ises on the stack it can just pop it right on or push it right onto the top of the stack which that's an O of one that's super fast whereas anytime you have to add something to the Heap it's got to be indexed it's got to have a hash so those are a little bit more expensive and so in this case where we know we don't really need all this extra benefits from the task and especially because the hot path of our code doesn't use the await keyword then we can just return value task and get a little performance boost here and done all right so we fixed the code so I know I threw a lot at you so let's do a quick review um these asyn best practices never used weight never use. result uh nowadays everything should be asynchronous there's very rare cases unless you're working on an old code base or working with an old codebase like I said the only times I see it nowadays is if I've got to implement this interface this interface implements a method that doesn't return task and now I'm stuck so I have to use it but if we can never use do weight never use do result because it locks the calling thread so if that's thread one it's going to keep thread one right here while it still runs another uh background task on a different thread so we're using two threads and our app is frozen well that's happening so really really bad things but if you have to because you're using that old interface then get a waiter get results going to be your friend get a waiter get result returns or throws an exception with your code in it so if the the method throws an exception that we're calling. geta waiter get result on that exception is going to be your code with uh your exception instead of weight and result both throw system. aggregate exceptions and we didn't show this in the code today but get a wait or get result also replaces result it literally has the same behavior for weight and result so don't use it hopefully you can avoid it but in the rare cases where you have to get a wait or get result will give you a easier stack Trace debug in the future uh fire and forget tasks don't don't do what I used to do and call task. run and just say yeah this is running out of background thread I don't care when it finishes so I'm not going to await it because like we saw if our code throws an exception we're never going to be able to handle it it'll never get rethrown it'll never be bubbled back up to me the developer so for fire and forget highly recommend uh grabbing this noug get package async await best practices and using the safe fire and forget extension method but again it's open source so if your company's got weird rules about adding in third party libraries you can just copy paste the code I don't mind you've got my permission avoid return await so if the only place in our method where we use the await keyword is in the return statement we can remove the await we can remove async and we can just return the task that helps us avoid an unnecessary thread switch gives us a little performance boost but again there be Dragons Keep in mind if you have a TR catch block keep return a weight if you have a using block keep return a weight because again as soon as you hit that return statement your codee's exited but if you have return a wait it's at least going to wait for that task to finish before returning so try to avoid it if you can if you want to get the little performance boost unless you're going to try catch block or using block configur white false like I said I use this everywhere hopefully maybe someday the net team will give us us a a global property where I can say for all of this code I want configur weight false to be the default uh until then you're going to end up writing this a lot you're going to append it to every single task um but also keep in mind if you do need to return to that calling thread um so for UI things you always want to return to the UI thread so you can update the UI you don't want to use configure we false uh another example Entity framework EF core can be fickle with this sometimes needs everything to be on the same thread so some places where you don't want to use configur way Falls yes but my rule of thumb for me is a mobile app developer making net Maui apps if I'm in my view model layer if I'm on my services layer of my app where I'm never going to touch the UI and I'm not using crazy things like any the framework I can figure way Falls everything to get that performance benefit so I don't have to return to the calling thread now there is a caveat to this uh we didn't have time today to jump into What's called the synchronization context uh again that's linked on the website if you want to dig into it but basically the synchronization context is what helps your helps you get back to the UI thread um so pretty much every NET Framework has a synchronization context uh but asp.net cord doesn't so there's this weird caveat with configurate false because asp.net cord doesn't have a synchronization context to tell it I don't care what thread I return to so by default on asp.net core you're always is or rather you'll never return to the calling thread you're always going to go back to whatever threadpool thread is free uh just something to keep in mind you can still call configure we false in your asp.net core code it's not going to break anything I actually still do it as a best practice just in case we need to copy paste that code into somewhere else but there is a thing called synchronization context and asp.net core doesn't have one but zamin wind fors WPF wind UI Blazer pretty much every other NET Framework configure Way Fall else use it value task so if your method's hot path doesn't call the awake keyword return value task you'll get a little performance boost like we said value task is a value type whereas task is a reference type so instead of going through all that overhead of initializing a task that you're never going to use we can use value task instead but there are small differences like you should never reuse a value task you shouldn't pass around value task variables but you know if you're just creating a method and you're just going to await the method that's totally cool and by using value task we can get a little bit of a performance boost in our code iyn inumerable so this was that that cool method that gave us the await for each Loop highly highly recommend it it's a little weird to implement like we saw in my code you I had to create a list of tasks and put them in a while loop and it looks a little weird but I promise you'll get used to it and once you do it's beautiful because as soon as those tasks are done we get those results and for me as a mobile app developer I can start updating stuff on the list so the user's not sitting there waiting for every single story to finish before I display it to them they'll see the stories appear in real time as the API calls finish so highly recommend it I think Anu bable it is like I said it's a little strange it's a little weird to get used to but totally worth it and remember we always want to pass in cancellation token to anytime we have a async task method same is true with isync enumerable but we want to add this enumerator cancellation attribute to that cancellation token so that the isnc numeral just stops for us so net helps us out with that weight async so remember if there's ever a method where you have to await it you should be passing a cancellation token but sometimes sometimes that exists where the developer didn't give us that option so if you ever hit one of those scenarios totally okay because you can still call weight async at the end and basically just bolt on that cancellation token so it's essentially a way to still pass in a cancellation token to that task even if that developer before us didn't allow us to do it all right there is one thing we didn't talk about today uh or we didn't look at in code rather but it's really important and it's really really cool it's called I async disposable so we've talked about eye disposable where you know it's a using block and when that using block is finished then you can dispose properly of objects so for big things like file streams we always want to make sure we're good developers and we dispose of those with uh with net now we also have I async disposable and what I want to show you is just how this works because it's a it's a little weird it's a little unintuitive at least it was for me um but the way it looks is we say a weight using and then we can we still want to say configurate false assuming we don't want to return to the main thread and the way this executes is that it'll run all the code inside this block so we say we're saving the data to the file and then once this code is done at the closing squiggly curly bracket that's when we await the disposing of this uh file stream so so file stream it's a it's a net API they've already implemented IAS sync disposable for us and as long as we say await using then we get all the benefits of being able to offload disposing of that file IO object onto a background thread so we get a little bit more performance so yes file stream existed before iyn disposable did so it's also it also implements I disposable so you can still just say using instead of await using but if a method or if a if a if a class implements isync disposable you should probably use a weight using instead of just using okay so if you haven't had a chance now is the time to take out your phone phones grab a picture of this slide because this link is where you can find everything from today I know we moved really fast like I said there's a lot of information here um but these are all lessons learned that I made along the way when I thought I was doing things right turns out I wasn't and it wasn't until you kind of really dig down deep into the net runtime do you really figure out what's going on so here on this website Cod traveler. async AWA best practices that's that's my web page you can find everything we covered today so this is where you can find a recording I've given this talk at a previous NDC before they recorded it for us just like they're doing today I'll make sure to refresh this once the PTO recording's out for you um so you've got the recording that you can share with your friends and co-workers who maybe couldn't make it today you've also got the slides if you want to grab some of these slides and use them go for them copy them be my guest and also all that code so the the a the async A8 best practices library is all open source it's on GitHub it's got an MIT license so you're more than welcome to copy paste any of that code do whatever you want with it and if you want to keep going if you haven't had enough async a weight you want to keep going deeper down the rabbit hole you can find links to things like value task and synchronization context and all these really nitty-gritty things that probably most of your co-workers don't know about but now you can be an expert in them too thank you
Info
Channel: NDC Conferences
Views: 8,214
Rating: undefined out of 5
Keywords: Brandon Minnick, .NET, Concurrency, C#, Async Code, NDC, Conferences, 2023, Live, Fun, Porto
Id: L2bw-XLRcbU
Channel Id: undefined
Length: 58min 23sec (3503 seconds)
Published: Fri Dec 22 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.