BrandGhost

BEWARE! Async Lazy in C# Is Easy... But Is It Safe?

Now that you're using the Lazy class in C# to handle lazy initialization the dotnet way, you're encountering situations where you want to use async await with it. But does the Lazy class support asynchronous operations? And if it does... should we be using it? Let's find out. For more software engineering videos, check this out: https://www.youtube.com/watch?v=9NIhzWDAmzE&list=PLzATctVhnsghjyegbPZOCpnctSPr_dOaF Have you subscribed to my weekly newsletter yet? A 5-minute read every weekend, rig...
View Transcript
all right if you're watching this video then you're probably a lazy c-sharp developer or you want to be a lazy c-sharp developer and no I don't mean that you're actually lazy it's a terrible joke but you're taking advantage of the lazy class inside of C sharp to be able to do lazy loading or lazy initialization now like many of us working in C sharp at this point in time async and Away patterns are extremely popular they're very pervasive in code bases because essentially once you start using them you have to propagate a single weight throughout your entire application now you might see where I'm going with this but if you're using lazy and async in a weight you might have realized oh crap I actually can't really combine these two things easily and suddenly my lazy loading is becoming a challenge now I'm going to be totally transparent with you I've been programming in c-sharp for a very long time and I just figured this out somewhat recently so I feel a little bit embarrassed but that's why I wanted to make this video to share it with you but in my own asp.net core application I was doing some lazy loading some lazy initialization and making it such that the startup time of my service Could Happen much faster because I would use things just in time the problem was that all of my code is async await and when I was doing Lazy loading I didn't have a good way to actually await these asynchronous methods so I was cheating a little bit calling dot result and it felt kind of gross because I knew that I wanted to have a sink away propagated as much as I could so in this video I'm going to show you the answer to this problem and it's kind of funny because the answer has been right under our noses the whole time for many of you you might already know what this is but I'm going to share with the rest of you with a little secrets and towards the end of this video I'm going to talk through some considerations that we're going to want to have with lazy in general but especially when we start looking at Lazy with async combined with it so stay right to the end we'll go through that analysis and for now let's jump over to visual studio so individual Studio I have pulled up the example application that I put together for the last lazy video and if you haven't seen that yet I'll put a little card Link at the top of this right now you can go watch that first and come right back here if you're not familiar with lazy if you're already familiar with lazy then you can kind of skip that and we can just continue ahead with looking at lazy async so in this particular applications very contrived but the point that I wanted to illustrate with this is that we have some amount of initialization code and kind of think about your own application where maybe you're doing dependency injection with auto fact and you're loading things from modules whatever it happens to be think about all of the code that's running at the start of your application to get things set up and this could be anything from a console application to a asp.net core web application a Maui UI application anything where you're doing initialization work before you actually have a user interact with things or people able to be served by web requests so that's what this part of the code is supposed to illustrate again it's very contrived but we have this one line here on line three that's supposed to demonstrate that we had some expensive operation that we only want to do once to get a resource and then we wrapped it with lazy so that later on we can actually get that value just in time when we're actually doing work in our application so again looking at this code here it's very contrived but I just want you to have that example mapped out in your head so that you can compare it to your own application now when we're looking at this code if we wanted to start thinking about how this works with async and await patterns let's go ahead and change this to expensive operation once method on line 19 to something that looks like async await alright I've now converted this method to be do expensive operation once and then I just added async as a common naming convention and then I have async keyword out the front I have task wrapping an integer so it's basically the same sort of signature but now it's a asynchronous task and then I've converted the thread.sleep to an awaitable task delay here it's still at two seconds now one of the challenges when we do this is that all of a sudden the lazy class that we have is upset that it can't use this method and if I name it properly it still doesn't work so of course it wasn't working because it was missing the async part of the method name after my little change there but now you can still see we get a red squiggly line and essentially this doesn't work because the method signature that we have right lazy int wants a method that returns an integer but our method do expensive operation once async I'm kind of regretting that that's a bit of a mouthful to say but please bear with me it's no longer returning an INT it's actually returning a task that's wrapping an integer so it seems very similar and if you're using async await it kind of feels like you know I'm going to await this thing so it's definitely going to be an integer when it comes back but that's just not the case it is truly a task wrapping an integer so one thing that you could do and this is what I was kind of doing historically to cheat this is I would just do something like this right here and this to me is just gross because I don't want to have to ever call Dot result on my tasks uh it just does not feel like the right thing to be doing I'm not extremely well versed with all the nuances of tasks but my understanding is that once you start doing stuff like this you can have some interesting side effects that you're not actually expecting when it comes to concurrency and then the asynchronous nature of async await so all of the spots in my code base where I had something like this with lazy and then you know calling dot result or dot weight on things I said this isn't right I want to make this better but how do I make lazy work with you know tasks wrapping the object or the value that I'm trying to get so some of you even if you've never seen this before you might have already figured it out if you're just looking at the syntax here but for some reason this is something that completely eluded me the entire time until I started reading about it online and it's actually pretty obvious but instead of trying to ask for a lazy int we can now just say we want a lazy task that's an INT and if I change that in both spots here all of a sudden this code should now just do what we expect right so you can see on line three now it's actually totally happy with this setup because the return type of this method is actually a task event so that's one step in the process you can see line three is now totally happy great but line eight is now unhappy right it's not going to compile and that's because if I scroll down a little bit the do work method we have takes an integer which is our magic number okay so if we scroll back up to line eight well we now change this magic number this lazy uh integer right this lazy task that's wrapping an integer now the dot value property is no longer an integer it's of course it's a task wrapping an integer so this syntax here is not going to work but if you think about it if it's a task wrapping an integer we can just await it now and all of a sudden all of this code is now lazy and async at the same time which is awesome so something I just want to call out before we go to run this and kind of prove that it works is that you'll notice I didn't change the rest of this code I just left it and it never complained about compiling but there's a bit of a catch here this code is actually uh incorrect based on our expectations um I'm going to explain in just a moment but pause and think about it for a second and see if you can solve it on your own but the answer is that of course that we can still ask for the dot value property of this lazy magic number but the dot value is a task wrapping an integer right so it's no longer if you were to print this line out right this part here inside of the curly braces is a task wrapping an integer it is not the integer so the solution is in fact the exact same thing you put a weight in front of it now this is a really contrived example I've already said that many times this looks kind of ridiculous to keep awaiting the same thing what I might suggest is actually something like um something like this where we actually grab the magic number off of the lazy magic number here so we get the value and we await it and then just store that um again no one would hopefully no one's ever writing code like this just copying and pasting stuff like that but if you needed to reuse the uh the lazy part like the value that comes off of it you could certainly store it into a variable I believe um I don't know I haven't actually decompiled it to check but if you were to just leave it awaiting so if you had this part of the code copied multiple times um it's not it still will not go execute the task multiple times right it is abiding by you know the laws of the lazy class it's only going to do it once so that's really awesome but there's probably a tiny bit of overhead and like I said I haven't decompiled it but I'm assuming the compiled code when you have the await keyword in front of a task it's going to check if that task is completed so I think by definition the fact that if you were to copy and paste this part it has to do an extra check it's probably a tiny tiny bit slower than just uh you know storing it once and then kind of calling that variable directly so if this was on the hot path of your code I would recommend you know pull that thing into a variable so you don't have to keep calling a wait but I did want to to address that calling a weight multiple times it absolutely will not go run this method multiple times and that's just because the lazy class takes care of that sort of do once behavior for us all right so if we go run our application now let's just go double check some of the time stamps we have so you can see that we still have no time at all between starting and started and that's because we have our lazy class our lazy variable actually declared there but we're not asking for the value right so lazy is only going to pay that penalty when you're asking for the value now when we go to the next part so this is you know our simulated initialization for our application but we go to the next part this is where we're kind of simulating our application doing work and this is where we're going to pay the penalty for going to get that magic number just one time and you can see that because if we look at the time stamps here right we go from 751 to 751.05 so that's only five seconds that is the penalty that we're supposed to be paying for getting that magic number but we're not paying it each and every time we go to get that magic number it's only done once and and just for total transparency this is running the code that actually had a weight multiple times so if there is overhead for calling a weight on the completed task we're talking about a very very small amount of time it's not measured in seconds by any means so you know it could be micro or nanoseconds or milliseconds I like I said I haven't Benchmark it or decompiled it but you can see that at least in this case it's quite negligible but as I've mentioned if this is on a hot path you know you should be considering all types of optimizations when you're ready for that all right so that's it for the code because the syntax for getting lazy to work with tasks and async await patterns is actually quite simple like I said it was hiding under my nose the entire time I never thought that I could actually just you know quite simply put tasks and then the type that I needed I don't know why it never occurred to me but it works quite well but I wanted to spend some time now talking about some of the considerations that you're going to want to make if you're using something like this in your application and this applies to using lazy in general it will apply to just like you know General programming concepts for resiliency but I think that this lazy async pattern that we just looked at can kind of exaggerate some potential problems that you might encounter so I just want to talk through that I'm not going to tell you like you know you solve it by doing this and like prescribe anything but I think it's a really good thought exercise and you need to keep it in mind for your application so when we're talking about lazy right the idea is that it's going to run the code for you once it is thread safe which is awesome but it's going to run it once but it's also assuming that your code works right it's assuming that you're going to be able to run some code get the value that it can cache and then every time you ask for the dot value property it's right there for you technically your code can throw exceptions and this can happen like I said without using tasks inside of lazy you know you could quite literally write a lazy uh you know instantiation that throws an exception and I mean I don't know why you would do it but you could and the problem is going to be that when you're going to ask for that dot value it will have to reevaluate and if you're thinking through this and you're thinking through okay well when you start using async await and especially if we're talking about app initialization and you're in a situation where you do need async await it's probably because the complexity of your program is growing right so you might be calling into a a call stack that has to go out to a database to go get a value right you want to kind of do it one time so you want to have it be lazy initialized so you can use it just in time you're just doing it once so you can cache that value but it had to go out to a database to go fetch it like that could fail the database could not be available you could in this example replace database with web service right and this is just a simple example you could keep making that complexity greater and greater and the point is that however complicated that is and however expense of that operation is you just want to run it once and then you want to go use that value just in time so thinking through this if that's the behavior you want that's great and I think it makes sense to use something like lazy in this case but you need to be thinking about the resiliency and the fallbacks and the exception handling because what happens if something goes wrong so let's take a situation where your application's starting up and you've configured some you know some lazy instances to be able to go pull values from services or databases or whatever and now you're running your application let's assume it's a web application so someone's hitting a route now and cool just in time so the first time that route's called you have to go load up you know you're going to access the value of your lazy object and for the first time it's going to go do that web request access that service access that database whatever and it doesn't work what should happen right like what is the expected behavior in that case um does it just fail like do you need to restart your whole service um how are you going to debug that like it just becomes a lot more complicated it's like I said it's the kind of like same Concepts that you would have in programming in general but the fact that you're expecting like the expectation around lazy and accessing that value is that you have a cached value that's done once now all of a sudden when things are failing you're in a position where it's not done once it could get much more expensive if you're doing things like retrying it it could be much more expensive if that um in this example that request that route fails and the next time you go to call it it has to go execute that again like if that was a really expensive operation you're now doing that operation several times and it's failing and it's not getting you any further to caching that value so this is just something I wanted to put in front of you again I'm not prescribing anything because I don't know your application you know your application and you're going to want to think through that so I'm not trying to say you can't ever use tasks uh with lazy like I certainly do in my application my current situation is that I have some some things I would consider constant but I wanted them configured in the database just so that I can um do queries and actually map things together but because they're mostly constant like they're unit conversions for context I actually do want to be able to pull those values from the database and then you know be able to have them wrapped and lazy so they feel like constants in my code but of course in my situation if that lazy initialization fails it's it's because my application actually is unhealthy so in my case if that were to fail I would need to to basically redeploy or restart my application because something is in uh incorrect state it would mean that the database is inaccessible because the code path that I have is actually quite simple it's just a query to go pull that stuff but it is wrapped in lazy so just a quick summary the lazy class that we have access to in c-sharp it's great for being able to do lazy loading and lazy initialization you can certainly get some app startup time back if you want to delay things to just in time uh using lazy does not just magically make time go away unless you can run things in parallel and you're sort of reducing that um you know the critical path of execution so you can save time but not just by using lazy on its own and when we want to go use lazy with tasks it's actually really simple we just change the type inside of the angle brackets looks like a funny symbol here um you just change the type inside of the angle brackets to be task and then the type that you actually want to have returned as you would have used lazy otherwise and then finally the last thing we talked about was just some considerations for if you're going to be using this and even in General with lazy or in general with programming think about resiliency think about what happens in the not happy path and how you plan to handle that in your application so thanks so much for watching I hope you found this helpful and we'll see you next time

Frequently Asked Questions

What is the main issue with combining lazy loading and async/await in C#?

The main issue is that the lazy class expects a method that returns a value directly, while async methods return a task that wraps a value. This mismatch makes it challenging to combine lazy loading with async/await, as I found out when I tried to use them together in my own application.

How can I use lazy loading with async methods in C#?

To use lazy loading with async methods, you can change the type inside the lazy class from the regular type to a task type, like Lazy<Task<int>>. This way, you can await the task when you access the value, allowing you to combine lazy initialization with asynchronous operations.

What considerations should I keep in mind when using lazy loading with async methods?

You should think about resiliency and exception handling. If the async operation fails, it could lead to complications since lazy loading assumes the value will be cached after the first execution. If something goes wrong during initialization, you need to have a plan for how to handle those failures in your application.

These FAQs were generated by AI from the video transcript.
An error has occurred. This application may no longer respond until reloaded. Reload