Correcting Common Async/Await Mistakes in .NET 8 - Brandon Minnick - NDC London 2024

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
okay welcome everybody thanks so much for joining me today uh close the door behind you please as you're coming in my name is Brandon mck and in this session we're talking about correcting common async AWA mistakes specifically inet 8 now we're going to move fast there's a lot of material we have to cover uh ASN aait came out in C 5 and now we're on C 12 so there's been a lot of goodies added so don't worry about trying to take notes don't worry about trying to keep up because I've actually taken all the notes and given all the things for you right here on this website so if you get a chance take out your camera or take a picture of this slide you can use this QR code to go to Cod traveler. asyn weight best practices this is where you can find these slides this is where you can find a recording of this talk this is where you can find uh the open source code for all the demos we're going to do today so relax soak it all in like I said we're going to be moving quickly and if you do have any more questions I'll be around all week and you can always find me on Twitter my handle is the code traveler so feel free to reach out if you have any more questions okay well let's start by looking at this async task method this is a method called get libraries it's going to call HTTP client. getet async hit this API return a response ensure we got a good status code then we're going to read that response get the content deserialize it and return the list of libraries now what's really going on on here is let's say thread one kicks off this method so thread one comes in thread one initializes this variable response and then as soon as thread one hits that await keyword it returns and a background thread comes in in this case we'll say thread 2 but it could be any free thread in the thread pool will run HTP client. getet async and this is good because thread one is a very very important thread it's also known as the UI thread it's also known as the main thread thread one is the only thread that can redraw our UI it's the only thread that can respond to a user when they Tap a button on our app thread one is responds that button click if they scroll on a list it's thread one that's redrawn the UI so we don't want to overuse thread one and we definitely don't want thread one making any API calls that take a couple seconds because if thread one does that essentially means our app is Frozen for a couple seconds so it's a good way to get a onestar review get a you have to use your force quit your app and tweet about how much they hate you on Twitter so with the keyword we're running this on a background thread meanwhile thread one is free to interact with the user respond to the UI button clicks all that fun stuff so when thread 2 is done it calls back out to thread one and says hey thread one I'm all done you're back up so thread one jumps back in thread one runs response. insur status code and initializes the variable stream and then we hit the await keyword again so what happens well we grab another background thread whatever thread is free in the thread pool we'll come in and run run read as stream async and likewise with last time when thread five is done here it's going to call back to thread one and thread one's going to jump back in again it's going to initialize this variable for libraries and then we hit the weight keyword again so background thread will'll deserialize give us our list of tasks and then finally thread one jumps back in for our last time to return the result so to understand what's really going on here uh it helps a look at the compiler generated code so I have up here on my screen we've gone to a website called sharp. if you've never heard of sharp laab this is a really really cool website where you can see what the compiler is actually doing with your code uh in fact it's so good that the net team at Microsoft actually uses this because we can see here that it has all the different branches of the Rosen repo different platforms so literally the C team when they're implementing new features for us they'll come in here to try them out so highly recommend sharp. if you haven't used it before but what we have here on the left is that same code so this is our our get libraries method and on the right this is what the compiler has generated so if you don't know the way the compiler works when you first click compile it does what's called lowering so it takes the code we write and turns it back into something that the compiler can understand because if you think about it the compiler's got a support all the way back to c 1 and when c 1 was invented async of weight didn't exist like heck multi-threading thread pools wasn't even really a thing uh even things like we get like VAR that gets lowered back into the type so it can go through the compiler in a way it understands and what happens here with our async task method is we see that the compiler has created a private struct that inherits from iyn State I async State machine now quick fun fact we're looking at the comp the code as if it was in compiled for release mode if you're when you're in debug mode it looks a little different it'll be it'll be a class but when we're compiling for release the compiler goes goes ahead and optimizes a little bit for us so so right so we get a private struct and you know right away that's a little weird right because I just wrote a method but now the compiler generated code is this struct that inherits from IAS sync State machine uh it's got all these funny names in it like angle bracket get libraries dore2 and this if you squint and look closely this this is the name of my method this is my git libraries method but what the compiler does because it's generating this class and it doesn't want to a naming conflict with any other classes or structs that already exist in our class that adds these angle brackets which is a cool way to cheat because we can't do that like we can't add an angle bracket in our code we'll get all sorts of weird errors for our class names and the compiler knows that so it does it writes some illegal code that it knows we'll never be able to write that way we don't have any overlap with its compiler generated code but let's take a look at what's going on here so inside this method or inside this struct is a method called move next and right away this is is something that perplexed me when I first started using asyn um I've been making IOS and Android apps in C using zamarin and net Maui for about eight years now and I'll never forget when I first put first published my app to the App Store uh I kept seeing these weird stack traces I'd get back these crash reports and Bug reports from the app and move next would be everywhere and I was like what is this because I never wrote a method called move next uh turns out the whole time it was just the compiler generating it for me but what move next is is it's essentially a giant State machine or more explicitly or more specifically uh it's a giant switch statement and inside of this switch statement the compiler adds a case every time we use the awake keyword so in this example we're using the awake keyword three times and you can see that the compiler generated case Zero case one and case two for us and there's also this default case here which every every time this gets generated it'll always have at the default case and then every time we add the a keyword in our method the compiler adds another case and if we look at what's going on here this is actually just our code and again it looks a little silly um like there's these angle brackets but we see we're just calling get async um when we first enter this and so so the first time move next is called it looks at this variable called num and num is just whatever the current state is and the current state the first time this runs will always be negative 1 and since there is no cas case of negative one we jump into the default case and that's where we call get async now what the compiler does or the code the compiler writs we'll first look to see did that task Complete because you know sometimes we write methods that are asnc task but we return task. completed and it's pretty cool because the compiler is smart enough to know hey if that task is completed we don't need to switch threads we don't need to do that it's expensive we don't want to do it and so the first thing it'll do it'll say is that completed because if so we can just move on but we know in our example we're calling an API it's going to have to reach out this is going to take a couple hundred milliseconds if not a couple seconds so it's not completed yet which means we enter here where it updates the state to zero and returns and this return is how thread one is now free to return and interact with the user but again before it left it said it set the state to zero so as get async is now running out of background thread once it finishes and it shouts back and says hey I'm all done and thread one jumps back in again it sets this value of numb to the current state jumps into the switch and now we're in state zero so this is how thread one knows where to pick up where it left off and in this case it's a little strange because the compiler is using go-to statements and we probably don't and probably shouldn't use go-to statements in our code um but all this is saying is to jump to this line il0 7e which is down here and in case one we can see our code again where we're calling uh we're get well first we're getting the result setting that variable result to the get result value and then ensuring we have a a success status code and then again just like last time it kicks off read as stream async and just like we expect it first sees or first checks to see is that already completed if not okay I'm going to go return thread one I'm going to go hang out with the user so that uh our app doesn't freeze and so and so this goes back and forth and this is how um the C team was able to design for us async a weight so like we get this pretty syntax here in our code where it just says we have this async keyword we have this awake keyword it's very readable uh we can read it top to bottom left or right but if you think about it this didn't exist when C was first created so super super smart people on the Microsoft runtime team were able to figure figure out a way to take an existing language an existing runtime and create this state machine for us while us as the developers consuming it we get to use this really cool syntax so so that's how it's working under the hood but what I want to highlight for you is this try catch block here and if you look this TR catch block consumes the entire code so this Tri catch block again I didn't write this I don't have a tri catch Block in my code but this Tri catch block takes all of the code from our acing task method and puts it inside of a tri catch block which means if any of my code throws an exception it gets caught right here it gets caught in this exception Handler and this is okay because as long as you use the awake keyword this exception gets rethrown but when I was first learning asyn and waight I thought I knew what I was doing and I would do things like ah well I want to run this code on a background thread so I would say task. run i' put a bunch of code in it and I'm like I don't need to await it because I don't care when it finishes and that is actually what led to all those bugs in my first app because what I didn't realize was sometimes my code was thrown an exception you know I'm a mobile developer as mobile developers we know we can't trust your internet connection on your phone because it's going to switch between Wi-Fi and cellular and different towers and maybe you're on a bus and you lose your Wi-Fi all together so uh sometimes my code was failing but it was inside a test run that was swallowing up the exception and because I wasn't awaiting test. run I never saw that exception and you might think to yourself hey that's great I hate exceptions they crash my app I love this but in in reality exceptions are good we want those exceptions to be thrown because if we get a timeout exception if we get an HTTP exception we want to be able to handle that gracefully because different scenarios are going to pop up and we need to be able to tell our app what to do when things fail whereas what was happening in my app was it was just failing and then the rest of my code kept going as if everything was cool so weird weird things were happening and it was all because I didn't know about move next and how this worked with this Tri catch block so so let's do a quick review here the the async keyword adds about 80 bytes and that's because every time and we compile our async code that async method becomes a struct when in release and if it's in debug mode then that's still a CL so that the debugger can uh better handle it struct is a little bit more performant which is why it does that in release mode and this is actually a fun Improvement in net 8 this used to be about 96 bytes I think and in net 8 they made some optimizations squeezed it down to 80 bytes but we're talking 80 bytes this really isn't much nowadays like even as a mobile developer who has a limited amount of ram to work with I don't really care if my app size increases by 80 bytes probably most of you don't either but fun fact to know about because maybe one day you'll be working on an embedded system or an iot device and you don't want to increase your app size and now we know that the asent keyword increased our app size by about 80 bytes every time and we want to await every task and again this is because non awaited tasks hide exceptions again it's might sound like a good thing but it's a bad thing we want those exceptions to be rethrown we want to be able to handle them gracefully we want to be able to tell our app hey something went wrong maybe display to the user error internet connection could not be established something like that whereas if we don't await the task that exception gets swallowed up forever okay so now that we're basically async await experts let's jump into some code I have and fix it so uh this is an app that I've created uh called Hacker News that uh just if you never heard of Hacker News it's a website where you can go and find kind of the latest text stories and I made this app that just goes out to The Hacker News API fetches all the top stories so when you do pull a refresh it goes gets all the top stories and then you can tap on any of these to see or to read the story so this is the code we're going to be fixing today and like I said earlier don't worry about memorizing anything because right now we're working on this class called news view model bad asyn O8 practices but all the things we're going to fix are already fixed inside of good asyn weight practices so when you come back later you can just compare them side by side and you don't have to remember everything we did step by step but we got a lot of things to refactor in here we got a lot of things to fix so let's jump into it now this first refactor uh I'm getting yelled at because I'm calling an async task method here in my Constructor and it's yelling at me because I'm not awaiting the call and you know this this this warning isn't wrong it says you know you should use the await keyword but it doesn't say anything about the fact that it's going to swallow up exceptions I wish it would actually I I used to work at Microsoft and tried really hard to get the net team to add that in because I was like people don't know about this we need to add it in the warning um but it's also correct in saying that the the execution will continue to the next line while that method's running in the background but but anyways yeah we're here in the Constructor we have an async task method and I literally just said we need to await every task so how do we do that right I mean if we put await here when we add async here we're just going to get red squiggles because the Constructor can't do async await and it shouldn't Constructors are just there to initialize our app it's to maybe assign a couple variables and then place all this code into memory so Constructors shouldn't be doing any async a weight which is why they don't allow us to do async a weight so that's nice that means they they don't allow us to kind of shoot ourselves in the foot there but if we can't do async a we here what are our options well one option is I could create another method called refresh and then and I can call this code inside of it where I can await it and then back here in the Constructor we'll just call that method now I can I can feel some tension in the room because I just wrote an async void method and what does every teacher tell you never use async void and that's not bad advice uh my my advice is actually avoid async void um but the problem I have when folks tell you never use async void is they never tell you why so so why is this bad why is this dangerous well first things first let's look at let's add a little bit of code here so just to show you refresh this is the method that runs when the user does a pull to refresh and it's going to go get the top stories it's going to add them to our top Story collection and so if we add some code here that says maybe top story collection. clear and top story collection. add and so you know now we're working with um the top Story collection here in the Constructor but let's think about what's happening cuz let's say thread one initializes this Constructor so thread one comes in it hits this refresh meth method so thread one comes into here but then it hits the awake keyword so thread one returns and that means thread one moves on to this line of code here meanwhile this refresh method is still running in the background so what we just introduced is a race condition and these are fun because we're editing top Story collection here but meanwhile this refresh method is also running in the background and also editing top Story collection here so by having this Asing void method in there we've just introduced a weird condition and we're going to see weird bugs on our app that'll take us forever to figure out and fix uh well not us because now we're experts at it but this is bad and so the other the other problem with this is you know intellisense doesn't tell us that this is async void if I zoom in here and if you're like me I I rarely read the docs I I use a lot of intellisense and only when I hit a problem do I ever go back and look at the docs um but if we look at the intellisense here it says this is a private void method so as C developers we are absolutely in the right to assume that this method is going to finish before the next line of code runs but with as an async void method now whoa now we know that this method's actually still running in the background and and sure you know you could argue hey this method's running here like how can you miss it it's always going to be right there but what if it wasn't right like what if that this method this async void method is in a different class and you know you have somebody on your team that doesn't know it's asnc void doesn't understand how asyn void works well they're going to write this code and have that assumption that because it returns void this method's done running once it returns but now we know that that's not true so so that's one of the reasons asnc void is dangerous another reason is what if this code threw an exception you know the refresh method might throw an exception in itself but you know we can just say Throw new exception here just to really ensure that exception happens and you might think yourself well okay yeah I mean no problem exceptions happen all the time we were just talking about how good exceptions are so I'll just put this in a try catch block no big deal right well again let's think about what's going on so thread one enters the Constructor thread one enters to try catch block enters the refresh method hits the await keyword and then returns so thread one continues on on and thread one continues to the next line of code so thread one's now down here and meanwhile this task is still running in the background so we've effectively we've effectively just exited our triy catch block and now when this exception happens our app's going to crash there's no way we can catch it so this is the other reason Asing void's dangerous is it's almost impossible to catch an exception if it gets thrown inside or gets thrown from an asyn void method so so this is bad we don't we don't want to do we want to avoid async void so let's uh let's scrap this but what do we do right because we have this code we want to kick it off and you know for me I want to make sure that I'm showing the latest stories as soon as the user launches the app because if I don't call refresh here it's a really bad user experience to launch this app and just see a blank screen and then have to pull to refresh it yourself like can you imagine like watching Instagram and just seeing a sign that says pull to refresh why I just opened the app do it for me so I really want this here but what can we do because we don't want to use async void well I've created an extension method for us called safe fire and forget now with safe fire and forget if we dig into its source code what it's doing is essentially the same thing it's utilizing async void because again this this is a valid use case of async FOID but there'll be dragons so with safe fire and forget it still uses async void but it has all the guard rails there for us so what we do here is we have our task run inside of a try catch block and we allow you to pass in an exception Handler so that if you want to you can handle any exceptions that pop up so for example we could say trace. right Line pass in that exception and make sure that we still acknowledge it and still recognize it and the thing I really like about uh this safe fire and forget extension method is it's very explicit so other developers uh when they come and look at your code and other members of your team you're being very explicit saying hey I'm not waiting for this method to finish so yeah maybe don't do this top Story collection stuff here because that's going to be a race condition um so it's a safe way to do these fire and forget methods and it's better than what I used to do which was just t. run forget about it because it'll still allow us to catch the exception handle the exception uh just like we always want to so so perfect so first one's done next up we get to this refactor now now this looks a little weird right because we are creating a task so we're saying task. delay and we're setting it to this variable and this is another thing we can do we can always we can create this task first and then we could await it later uh so in this case this is actually a little trick that we use for mobile app development where uh it's sounds counterintuitive but every time user does the p a pull to refresh we're going to make him show that activity indicator that spinning activity indicator for at least two seconds because you know sometimes data gets cached and you know if that API request comes back within 200 milliseconds then that's not a time enough time to update the UI and show that activity indicator and so the user is going to do a pull to refresh and they're going to think we didn't do anything I'm like ah wait it didn't happen something's broken they'll just keep pulling to refresh and they don't know that they have the latest data but they'll think our app's broken and that's another good way to get a one star review so again a little weird but something I recommend doing is having that consistency by having this minimum uh minimum refresh time task now I got a little squiggle here and this one's yelling at me because I'm not passing in a cancellation token now as good C developers we always want to anytime we have an async task method we always want to have a cancellation token as a parameter so anytime we write uh Asing task method always allow the other developers to pass in a cancellation token because maybe I don't want this to run forever like maybe it'll take 20 seconds for this to time out well that's not a good user experience you know I want to be able to have a cancellation token in that says yeah you know try it for 5 seconds and if it doesn't work that's when we'll alert the user so so as good C developers we pass in our cancellation token and you know lucky for us test. delay has that as a parameter so we just pass it in here but let's pretend like it doesn't uh let's let's pretend like we're using somebody else's code and they're not good C developers like us and they didn't allow us to pass in cancellation token like what do we do well there's a cool extension method called weight async and we can pass the token into here and what this extension method basically does is bolts on that cancellation token to this test so if you're ever working with somebody else's code or a library especially if it's older they might not have or they might not have given you a parameter to pass in your cancellation token that's okay because we can always say weight async and we can pass in our own cancellation token and it's essentially bolted that on to the task so in this case since test out delay allows us to pass it in right there we'll just keep it in here now the next thing to refactor Let's see we go inet read the top stories great so we got to get the top stories before we can add them to our collection this makes sense uh but what do we do here what's what's wrong with this right like we're we're still calling we're calling a wait this looks like good code um but again let's remember what's going on here and so let's say thread one kicks off this method because you know pull a refresh that's a UI event so it's highly likely that uh thread one will enter uh the method refresh and get to here and then thread one returns so great we're doing this all on background threads everybody's happy thread one's free to interact with the user they can scroll they can click on the stories do whatever they want my app great but when it's done this like we saw earlier this shouts back to the calling thread and says hey I'm all done thread one get back in here and now thread one has to come in to finish this method now is that a good use of thread one not really cuz we're we're in my view model this is uh this is code that never touches my UI so I know I never need thread one anywhere in this class but yet we have thread one coming back in to do extra work and then on top of that what if thread one's busy right like maybe the user's scrolling so thread one's got to redraw the UI on the screen as they're scrolling through well now we have to wait for thread one to be free before it can come back in so we also run the risk of our code sitting idle along with overusing the UI thread so what we can do is we can add configur we false to the end and configure we false basically tells net I don't care what thread we continue on when I'm done just grab whatever thread is free from the thread pool and keep running the code and so now instead of returning to thread one after this line of code to clear the top Story collection and iterate over it to add them to the list nown net will just go to the thread poool and say hey who's free and it'll take whatever threads available and it'll continue on running the code so with configurate false we can be sure we're not overworking and overusing our UI thread and we get a little bit of a performance bump because again if thread one's busy well we got to wait for it to free up before we can jump back in So my rule of thumb is you know with mobile and net Maui specifically uh we use the mvvm architecture so everywhere in my view models everywhere in my service layer everywhere in my code that doesn't touch the UI I configure weight false everything but obviously if you're writing UI code and you know you need to return to the UI thread to maybe update some button text or something like that of course don't use configure we false uh the default is actually configure we true so if you don't have configure we on there it'll Default configurate true uh but then also new in net 8 is configure weight options we can now pass in an enum here and this enum has a couple options and right away you'll see the values here on the side so this is actually this is a flag so we can actually chain these together but let's talk about what they do first so there's there's configure options. none and this is going to be a little counterintuitive because none is the same as configure O8 false so none means don't return to the calling thread again little counterintuitive when I first saw this I was like oh cool configure white options I assume nun's the same as true right because they're both the default false no configure we configure weight options. none is the same as configure we false it will not return to the calling thread uh the next one here on the list in tsense is configure weight options. Force yielding uh so this is basically the modern. net way of saying await task. yield so remember when we looked at that compiled code it said every time we kicked off a task it would immediately check and be like hey is this task already completed because I don't want to have to switch threads if I don't have to well Force yielding says always switch threads and again probably a little weird to think about but if you've ever had to use a weight test. yield in your code to force a thread switch then this is the way to do it nowadays in net 8 and this can be super helpful because again if we're updating code that needs to yield then maybe we want to update something on the screen with the UI thread then Force yielding will make sure that once this code runs we will always make that thread switch so there's Force yielding uh there's suppressed throwing and this one goes against everything I just talked about because I always said you want to rethrow those exceptions we want to use the awake keyword uh but now you have the option not to so don't ask me I don't know when you would want to use this in your code but you can say configure weight options. supress throwing and if an exception is thrown in your task net will not rethrow it for you so little dangerous I don't I don't know where anybody's going to use that if you do let me know I'd love to know a real world example and then the last one here is continue on captured context so this is the same as configure a weight true so continue on captured context we'll return to the calling thread before running the next line of code so again these are also this is an enum flag meaning we can chain them together so we can always use that bitwise or and say uh we want to force yielding and suppress throwing by putting them in there together so play around with those but I will say for my code most of it I'm using configur away false unless for some reason I can think of a a good reason to use use the other other ones otherwise okay cool so that refactor done now what's next oh great so down here we finally have our minimum refresh time task so in our finally block this is where I'm going to say hey if all this code if all this a all these API calls took less than 2 seconds hang out here for a bit because I want to be sure the user knows we their pull to refresh and they update the UI they get the activity indicator 2 seconds is just enough time to say hey I acknowledge you but not enough time for them to be angry at us for uh wasting their time but the problem here is I'm calling dot weight now I have a rule of thumb and that is to never never never never never use do weight do weight is really bad uh the reason do weight's bad is unlike the await keyword that will return the calling threads so again when we use a weight the thread's able to return either back to the thread Pole or if it's thread one it can interact with the user again weight says uhuh calling thread you're staying right here and it locks the calling thread on this line of code meanwhile this minimum refresh time task is already running on a different thread so bad things are happening because this is going to wait 2 seconds so if if we are on the UI thread that means our ass can be frozen for two two seconds but it's also bad CU we're using two threads when we should only be using one and even if you're thinking to yourself like hey I don't really care about this UI stuff I'm a backend developer I make asp.net core apis what do I care well if you use weight even if you don't have a UI to worry about you're still using two threads when you only need to be using one and eventually you're going to reach what's called threadpool exhaustion because every server every computer only has a finite amount of threads that net can have in the thread pool and that all depends on how much memory and how much based on how much horsepower your server has net will determine how many threads it can it can spin up um but yeah even if you're just making apis and you you don't care about freezing a UI well every time you call weight you're using twice as many threads as you need and you're going to hit that thread pool exhaustion twice as fast so so what do we do well easy easy solution here because we're in an asyn task method so we're just going to await but let's say maybe you can't right now it's getting really really rare nowadays especially in theet 8 world where you'd ever have to write synchronous code uh but it does still happen you know probably 99% of the time we can just use the awake keyword but that 1% we might have to use weight and I just said don't ever use it well what do I recommend well we can instead use get a waiter get result now this is still bad this is still going to do everything weight does it's still going to lock the calling thread we're still using two threads when we should only be using one but if an exception is thrown so let's say that our cancellation token uh timed out and this is going to throw an exception a timeout exception well get a wait or get result throws our code it throws that timeout exception whereas if we're using weight or result that throws what's called a system. aggregate exception and if you if you're unfamiliar with those they're really weird because an aggregate exception is an exception that holds exceptions so it has a list of exceptions inside of it and that's where our code lives so if you've ever seen those uh those uh stack traces system aggregated exceptions are weird they're difficult to debug especially for new developers because you're looking at it you're going okay I don't even see my code that threw an exception I have no idea what this aggregated exception even means whereas if you use geta wa or get result it'll rethrow our code with our exception in that stack Trace so it's still bad we still don't want to use it if we can just use async a weight instead but if we have to get a weight or get result is better than weight and result it actually Place replaces both of them um because it'll rethrow our code with our exception okay but for here we're not going to do any of that that is more of a fun fact than anything else because we're in the async task method and we'll just await it and also fun fact yes you can use asyn aade in your trif finally blocks okay next up we've got this method called get top stories now this looks pretty good um another cool fun fact here we're using a frozen set this is something new also in net 8 uh it has nothing to do with async A8 but I love it because I'm a big fan of immutable data I strongly believe anytime we make an API call anytime we pull data from a database that we should store that in an immutable list and that's what Frozen set is you can see it's in the namespace system. collections. frozen so new and.net 8 is Frozen set and Frozen dictionary and some of you might be thinking to yourself like hey wait a minute I've already used things like I read only list and immutable array and immutable collection like why did they go create a third one what the heck's the difference well the Frozen set is the only one that'll actually freeze the data um yes if you're using IAD only list or a mutable list or mutable collection or any of those existing ones yes you aren't able to add or remove anything from the list but a Savvy developer might know you can actually go into and replace the data in there so I could go back to index zero of my I read only list and I could swap out that data Frozen set doesn't allow you to do that so we finally have a purely immutable collection that we can use in.net and I'm a big fan because like I said I always use uh immutable data structures like this anytime I make API calls anytime I make uh database calls and the way you can use it is you just say do two frozen set so super easy and super convenient now looking at this code this code actually looks really good but it's telling me I got to refactor it so what's wrong here because you know we're using the await keyword we're calling configure AWA false but if we look at this it's kind of a product of how The Hacker News API works because The Hacker News API requires me to call and get the top story IDs before I can actually get the top stories so whoever designed the API I wish they would have just allowed me to get all the top stories in one API but nope the way they do it is they got to give me a list of IDs and then serially I have to make a request to get every story in the list so if I want to get 50 of the top stories well I make this first API call and then I got to make 50 more and because it's in a for each Loop this is all happening serially so I got to wait for that first one to get back then I make the next one and so you know even if they take half a second if I'm making 50 API 50 API requests that's still like 25 seconds so so we can do better and we can use something and instead of rewriting this I'll jump over to the good async weight best practices to show you we can use something called I async enumerable Now isync inumerable is really really cool uh it is an async method uh but it allows us to return the results in real time so let's check out this code real quick uh because it's this iyn inumerable that allows us to consume it as an await for each Loop so await for each is this really really cool feature where if we have an isync enumerable we can say await for each for every story in our GP stories and it's again we can just use configure weight false here we pass our cancellation token so we're still everything's still looking good um but as soon as one of those API calls come back I can add it into my collection so instead of forcing the user to sit there just looking at a spinning indicator for 25 seconds and Bam all that data shows up at once with this await for each loop I can feed them the data as it comes back and we'll see in a second I'm doing it all in parallel so so the way I've designed this iyn innumerable is you know I still got to go get those top story IDs but once I have them I instead of instead of awaiting each each uh API call to get that story I've created a list of tasks and so what I do I add all those tasks to this list and then I jump into this while loop where I say as long as that task is not empty or as long as that list is not empty and as long as the maximum story count hasn't been reached because you know maybe the user only wants 20 stories and that's fine they can set that uh keep going so what we can do is we can say await task. win any and what this does we pass in a list of tasks here and task. when any anytime one of those tasks is completed we'll give it to us so as soon as one of these 50 tasks is done it returns it here and now I have it so a I want to remove it from the list because I know I don't need it anymore in that list and then I can get the story from that task and I can yield return it and yield return is that's the same as an I inumerable yield return and if you've never used it it doesn't actually escape the method it doesn't return the method instead it returns that one piece of data and it's this yield return if we jump back up to the four each loop it's that yield return that allows us to run this code and subsequently show this data to our to our user on the UI so so we're able to basically stream the data to our user as it's coming in and at the same time we're running all of these in in parallel instead of running them um sequentially so our app's going to load a lot faster it's a better UI experience and I do want to point out one one thing here is I just said that whenever a task completes that's what we get back so technically this task is completed but I'm still awaiting it so there is the one valid use case for using do result even though I just told you never to use it um we can use do result here because we know this task is completed we know that this won't lock any threads because there's going to be no thread switching so do result is valid here but I still don't recommend using it because this code will get refactored someday we're going to have new developers on our team they're going to copy paste our existing code into their code and I guarantee you this line of code will appear somewhere else in in your app one day and if we say do result here and then somebody else copy pastes it and all of a sudden now our apps freezing performance is bad uh because they didn't understand that the task was already completed so it definitely feels like Overkill to say a weight and configure weight false but we know those are the best practices so we still want to do it even though we know that task is completed okay next method we have a method called get story now so this this looks pretty good you know we're using async of weight I could probably add in configure a weight false here um but there's actually a better optimization here because this get story method if we zoom in a little bit bit this get story method returns a task of type story Model and if we look at this method it also returns a type of task story Model so what we can do is we can remove a sync and remove a weight and just return that task and what this does is it saves us a thread switch so again if you think about what's going on here let's say thread five calls this method thread five comes in here hits the await statement returns back to the thread pool this is now running on a different background thread maybe thread seven then when thread Seven's done it says Hey thread five I'm all back or I'm all done or even if we had configure a weight false in here it would still have to go back to the thread pool and get a different thread just to return the value so we had two thread switches when we don't need to and if we if we remove a sink and we remove a weight now we just return the task we saved those context switches and now whoever's calling the git story method meod they can decide how they want to use it so that's why up here you can still say A weit you can still say configure weight false and we've just improved our performance a little bit by saving our ourselves a contact switch okay one more method down here uh this one's called get top story IDs and let's actually remove this for just a second because right now we're looking at the same thing right this is just like the last method it's a async task method and the only place where we have the await keyword is in the return statement so that's easy right Brandon you just told us we remove await we remove async and there we go like this is going to compile there's no red squiggles uh but no this is actually a trick question because when we return We exit the method so as soon as we hit that return statement we're given back the task which means we've exited this method which means we've exited this Tri catch block so we're no longer inside this Tri catch block as soon as we return the task so what's happening is this method is still running on a background thread but if it throws an exception we're never going to catch it here so this was actually a trick question we do want to keep async a weight here even though the only place we're doing it is in the return of weight statement just because we're inside a tri catch block now adding in that that previous code back uh something else I want to show you is let's say you know wouldn't it be smart if every time they did a pull to refresh we check to see is the data still good like Hacker News doesn't update every second every minute so I kind of said hey if the data is recent like within the previous hour let's just skip that API call let's save ourselves a round trip uh we don't need to burn through the cellular data for the user and we'll just return what we already got like we already got the IDS just give them back um and so what happens now is you know the first time we run this method we won't have any data so this will be FAL false and we'll have to do return a weight which is totally fine and we should be good and add in configure weight false here uh but the second time this runs assuming that pulled refresh happens within an hour this part of the method runs and the third time this code runs we use this return statement the fourth time and what you start to see is the hot path for the method is right here and the hot path does not use the awake keyword so if you ever have a method like this where nine times out of 10 you don't need that AWA keyword instead of returning a task we can return a value task now value task is similar to task it's not exactly the same like for example you shouldn't pass around tasks or you shouldn't pass around value tasks you shouldn't reuse value tasks but certainly safe to use it as a return for a method that you can await and the reason it's a little bit better is value task is a value type so if we remember in C in net value types live on the stack in memory whereas reference types which a task is live on the Heap and so if we're not going to use the task and we're not going to need all that overhead well we can get a performance boost by using value task because adding something to a stack o of one we just push it right onto the stack whereas anytime we add something to the Heap well we've got to index it it takes a little bit longer so if we're not going to use async await and we're not going to need the await keyword in our method say 90% of the time we can get a little bit of a performance Bump by using value task okay so let's talk about all those async weight best practices uh never use weight or do result if you can avoid it avoid it it's really bad it locks the calling thread you're going to end up using two threads when you only should be using one and we should always be using the await keyword but if for some reason you have to like I see it still like old interfaces maybe it's an interface that returns a type Bo instead of a task of type buol then okay fine uh what we'll do instead is get a waiter get result get a waiter get result again replaces weight and it replaces do result but it's also bad because it's going to lock the calling thread and you're going to end up using two threads when you only should be using one it just gives you better better stack traces better exception handling and helps out future you when you're debugging code fire and forget tasks don't do what I used to do and just say task. run run this in the background forget about it because we always want to await every task so if we do want to fire and forget a task and we know this is totally cool we can run in the background it's I'm fine with the code continuing to run after this use the safe fire forget extension method I have it available in my Nate package async way best practices I highly recommend it and I know I'm a little biased because I made this library but we just reached over 1.5 million downloads so couple hundred, developers also think it's pretty cool too and actually saw Mozilla using it one of their c apps which is crazy mind-blowing so uh Nate async weight best practices will give you the safe fire and forget extension method avoid return await so if the only place in your method you're using the await keyword is in the return statement you can remove it you can remove async and just return the task but keep in mind if you're inside of a TR catch block keep it keep return a weight because you're going to exit that TR catch block right away likewise if you're in a disposing like a using block and you're going to dispose of something again keep that return await because you're going to exit the method and then objects are going to be disposed that you're probably using in the background and then you're going to get weird object dispose exceptions it's happened to me I'm telling you this from experience keep it in those scenarios but otherwise totally cool to remove it configure weight false so if you don't need to return to the calling thread configure way false like I said I use it everywhere in the view model layer the service layer of my app uh because it gives us a little bit of a performance bump when we know we don't need to return to the calling thread now caveat uh configurate false uses something called the synchronization context and almost every framework in net has a synchronization context but the odd one out asp.net core so if you're using WPF if you're using Wind forms if you're using Blazer zamon net Maui whatever they all have synchronization contexts so configur false we'll make sure you don't return to the calling thread um with asp.net core was created they made the decision not to include a synchronization context which if you don't know what a synchronization context is that's cool I've got a link to it on the web page it's basically what allows net to return back to the UI thread for us but definitely check out those blog posts if you want to get real deep into it because there's synchronization context and execution context this this all goes way deeper than even what we're touching on today but right so in asp.net core there is no synchronization context so that means if you write configure weight fals and asp.net cords the same thing is writing configure way true so slight slight caveat there but for all the other net Frameworks it's good now there's also configur weight options so again new in. net 8 we can we can use this enum we it's a flag we can chain them together and just keep in mind the weird caveat with none is the same as configur false meaning it will not return to the calling Trend it's going to be a little weird to get used to that but you know by the time net 15 rolls around we'll be Masters we'll know these like the back of our hand but the other options are to to continue on the capture context we can also say Force yielding which will force the thread switch even if the task is completed and then there's also suppressed throwing which terrifies me and would love to know why and when anybody would want to use it and again same caveat it still uses or still requires a synchron synchronization context and so asp.net core you're the odd man out cuz you don't have one utilize value task so when your method's hot path doesn't call a weight you can return a value task instead of a task get that performance boost because now again you're pushing that to the stack instead of allocating on the Heap and your coach is going to run that much faster and you'll notice a lot of things under the hood in net now use value task instead of task as well so we can be just as smart as those guys using value task in our code iyn AAL if you're streaming data we want to be able to show that to the user we want to be able to uh stream that real time I think enumerable allows us to do that so instead of telling the user to wait and just showing them a little spinning indicator saying loading loading loading we can actually give them what they're looking for we can run our API calls in parallel and it's a better using experience for all of us and don't forget oh I I forgot actually I didn't even tell you about a numerator cancellation let's jump back to the code real quick so what is this so right so we're good C developers so we're always going to provide a cancellation token for our async methods um and when you do that for an iyc enumerable it'll yell at you it'll say hey you need to add this or you should add this enumerator cancellation attribute and what this does this tells net that this is the token to pass into the net async enumerable under the hood and the reason this is really cool you know if we didn't have this in here we would have to check that token right like we would have to say things like token. throw and we'd probably want to do it down here and we have to make all these checks because if the token's canceled we should we should abort but with enumerator cancellation attribute we don't have to do that because we're essentially telling net like hey every time you iterate just check the token for me so not too not too difficult to remember because again if you forget it you'll get a little warning message and it'll say don't forget that attribute okay uh weight async so again if if we ever have to consume an an async task method that doesn't allow us to pass in a cancellation token shame on them shame on that developer who wrote it but that's cool we can just use the weight async extension method and pass in our cancellation token there and then it's basically the same thing it's as if they did allow to pass in a cancellation token to their task all right and we also didn't get a chance to talk about isync disposable uh so we can we can show it off here uh it's just like ey disposable but you can await it so this is really cool for certain things that take a long time to clean up uh if you've never used I disposable or I async disposable it's basically a way for the creator of a class to clean up after itself so you might have some managed and unmanaged resources you need to get rid of uh when you know know that the user consuming your class is done like file stream for example it has a bunch of buffers and does a bunch of system IO and so when it disposes of all those things it takes a while and we don't want to lock the calling thread so they now added isync disposable to things like file stream and the way this works it's very similar to the using block we're used to but instead we say a weight using and then again we can use configure a weight false right here and so what happens is our code runs here in the middle so inside the curly brackets just like with a normal disclosing block our code runs and then once it's done once we get to that End closing squiggle curly bracket that's when that await executes so that's when you'll see that configure weight false actually [Music] happened okay well now's your chance if you haven't already take out your phones grab a picture this slide head to codet traveler. async AWA best practices because this is where you can find everything we covered today this is where I've posted a video of this exact talk so if you have any friends or co-workers who couldn't make it today you can send them this link and they can watch the video they can become asyn experts just like us and like I mentioned a minute ago this is also where you can find the resources so uh you can find links to the asyn A8 best practices Nate package so you can use safe fired forget uh there's links to the open source demo that we did here today so you can compare good and bad uh what good view model bad view model side side by side to remember what everything we touched on and like I said earlier there's this goes really deep you this feels like an advanced topic that we just covered today we're still way up here at the tip of the iceberg uh so there's more things you can learn about about how value task Works about what's an e uh what's a synchronization context what's an execution context how all this stuff work under the hood I've included a bunch of uh blog posts there that were helpful with me when I was learning and first starting out my asyn weight journey and I know they'll be helpful for you too thank [Applause] you
Info
Channel: NDC Conferences
Views: 16,424
Rating: undefined out of 5
Keywords: NDC, Conferences, 2024, Live, Fun, London, .NET, .NET 8, C#, Concurrency, AWS, Code, Brandon Minnick
Id: GQYd6MWKiLI
Channel Id: undefined
Length: 56min 23sec (3383 seconds)
Published: Thu Apr 11 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.