Race Conditions in C# .NET Core

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
when it comes to race conditions you either know what you're doing or you don't if you don't know what you're doing you're leaving yourself open to potentially some nasty issues that could happen in production your data is going to be messed up you're going to need to go fix that data you're gonna need to bring down your service and fix that mean or any other developer want you to be in the situation that's why in this video I'm going to show you how to simulate and test race conditions and then how to avoid them including in a distributed scenario my name is Anton welcome to the raw coding YouTube channel let's go ahead and take a look at this example and don't forget if you're enjoying the video like And subscribe we will start off slow and then pick up the pace starting off with what is an actual race condition or a race condition to happen you need some kind of resource and for now this can be a number and then you have multiple processes working in parallel let's say that this is worker one and he picks up the resource we will then have worker 2 that is going to pick up the resource as well worker one is going to do some work on the resource and then worker 2 is also going to do that work on the resource they then go ahead and place whatever work they've done back into the resource all then console right line resource to the console.net run and we're going to see one being printed to the console a classical race condition although performed sequentially but essentially this is the formula shared resource multiple things performing work on that shared resource let's stick with this example we are gonna have a number and we're gonna start off with it being zero first of all let's say that we want to perform some kind of work so we're just gonna take the number and increment it let's output this also to the console to begin with we will start off with a for Loop and we will increment it a thousand times essentially taking a thousand workers and lining them up in a queue to perform work on the number if we run our application because everything happens sequentially there is is no race condition here a thousand is our successful result so if we have a race condition and we're solving it correctly we should get a thousand hundred percent of the times now this is running sequentially if we want to take this operation and place it on a thread to run in parallel we would have to go to task and all the Run function Supply a Lambda and then put this work inside of the Lambda if we format this code open up the terminal and actually forgot a semicolon over there let's rerun the application and we're gonna get zero the lesson to learn here is that the time it takes to create a task put it on a thread and then actually run that thread is going to take some time there is resource allocation there that you cannot just ignore the time it takes for net to put this task onto a thread is longer than the rest of our application and our application finishes early if we actually want to wait for this task to finish we have to await on it but now we're kind of in the same sequential situation where we're taking a task we're putting it on a processor everything happens there sequentially and then we're just waiting for that to complete so really what we want to do is schedule a thousand tasks instead of doing a thousand iterations on a processor so these are going to be a thousand tasks that will potentially run in parallel if we run the application now and we still get a thousand what is happening here is we're taking a task we're putting it on a thread and it is running in parallel and with the await keyword we're waiting for it to finish so that is going to do a single incrementation we're going to come back and we're gonna do the same so we're just putting all the tasks and we're waiting for them to finish we actually want to stop waiting for them and as we put the task just carry on creating tasks and putting them onto other threads to run in parallel now if we run our application at this point we may see some kind of number we may see a very small number but the results are random if I actually run this enough times we may see a zero the reason for that is While We're looping we're creating a task and we're scheduling it it doesn't mean that the task is actually going to run remember that we need to wait on the task in order for it to actually complete for this reason we're going to take all of the tasks that we're creating and we're going to store them in a list once we have aggregated all of our tasks in this collection we want to use task when all to await on all of these tasks to complete Arrow will say away let's run the application now and now if I run this a couple of times the situation that we should see is that most of the numbers are going to stay in the upper bound at this point we are seeing a race condition however our simulation is not not quite correct see what might happen is as we're kicking off a task it is gonna go ahead and perform its work if the thread pull is actually going to organize the tasks in a manner where we kick off a task we add it to the list and it completes if we kick off the next task and we add it to the list and it completes hopefully you see the pattern here that all of the tasks will indeed still run sequentially as we can see from the console the chances of that are going to be very low and I'm going to say it here right now that this is going to also highly depend on what kind of CPU you have so the results that you may see when you're running a race condition like this may vary from system to system nevertheless the problem that we're outlining here is that the operation of interest is the incrementation of the number the thing that is running in parallel is scheduling of tasks and then executing those tasks including with our operation we you want to narrow down the scope to our specific operation how do we do that well if we take a look at an Olympic race we have a gunman that is standing there with the gun and then when he fires the gun all of the racers go at the same time we need our own personal gunman and we do that with a semaphore so War semaphore equals new semaphore slim we're gonna give the gunman zero number for the amount of times that he has shot and then on the semaphore we are gonna call wayley sync notice that we're not gonna await on this and this is going to be our gunshot the place where we want to await on The Gunshot is going to be inside the task we will also have to make this asynchronous and if we run our application now it is actually just going to hang because we never actually released the semaphore so let's stop it here before we await on all of the tasks we want to go to the semaphore and release the trigger this is going to fire the gunshot and all of the tasks that have lined up at The Gunshot will go and execute the operation of interest for us so running the application we get our race condition essentially so 700 829 and 803 we are getting a pretty good race condition here although I'm gonna say it also that I still have a chance of getting a thousand over here and as correct of an implementation as this is currently you can still get outliers so it is important when you're testing for race conditions you're running this simulation more than once and one more very important thing that can be very overlooked in this area we have a task.run and then an asynchronous Lambda what is happening over here is does not run is still coming with the workload of scheduling a task that is going to perform some work on the background the work that it's performing is this asynchronous function so we essentially have two tasks happening here and the perhaps a little bit of an unseen effect that could happen here is that not all of the tasks are actually going to be at The Gunshot because they're being scheduled in parallel so when we call release not all of them may start at the same time the way we avoid this is we take our asynchronous function and we say that this is the work that should be done instead of scheduling the task we invoke the word function every single time we loop we're gonna enter the function we're gonna arrive at The Gunshot and then because we're awaiting it here and we're not awaiting it here the task will be created the execution will sequentially reach The Gunshot the task will be created added to the tasks and then the next looping will happen this way by the end of the for Loop we have insurance that all of the tasks have lined up at the gun shop hopefully you can see how for the simulation of the race condition what we're trying to do is remove as much of that unnecessary work that is happening around scheduling of our work of Interest which is this number incrementation so the only thing that is going to run in parallel is going to be this work let's run the application now we get 990 running this a couple of times comes close to a thousand so we can see the varying results if I run this enough times again I can still see a thousand I can still see the correct result so it is important that you run this more than one times and this is where the numbering incrementation really is the best and the worst example at the same time it's such a small and quick operation it is kind of like a boundary for testing is your race condition simulation actually good or not and if you are seeing the correct results you may think my simulation isn't very good however you can get the correct result regardless so when you are simulating for race conditions I'm going to reiterate this you want to be running it multiple times now for some of you that are sitting there and perhaps you haven't learned anything new let's comment out all the semaphore stuff another thing that you can use for The Gunshot is a c-sharp promise something that you don't see very often in c-sharp however it can also be used to align all of your tasks let's create a promise this is going to be a new task completion Source all we have to do is await on the promise task and instead of releasing like we're doing with the semaphore we go to the promise and we set the result bring up the terminal or running this a couple of times and we're still getting the race condition where all of the tasks are aligned right before we have to do our work now who the hell has number incrementation in their race conditions as the main we're doing IO right you're communicating with servers databases file systems that is where the race conditions are critical and are prime for messing your system up this is why we have a saved number we have a file we're reading from there we're going to parse the number we're going to increment it and then we're going to write it back okay so if you have parallel file access let's go ahead and see what happens I'm going to take the saved number we'll assign a save number to the number and then we're going to increment it if we run our application with a race condition we're going to get an exception right so if you get a race condition on file access you will see this issue so what synchronization methods do we have in C sharp first and foremost we have the lock statement this will require some kind of object this can be an instance of anything in memory we place the object in the lock and the main thing with the lock is that you cannot put asynchronous stuff here so you will have to make it synchronous and actually await on this so you will have to do like get a waiter and then get result if we run the application and the application is essentially hanging what we're experiencing here is called thread starvation I'm going to cancel the application and we're going to talk about this a little bit let's say we have created 10 tasks and nine of which are going to run into the lock and one is going to get into the get results and then we only have five threads to run the first task acquires the lock and then because there is asynchronous code inside of it the thread is going to relieve control of it and pick up another task and all of the other five tasks while this other one has been relieved control of while it was reading a file or something like that the five lock tasks will congest the threads and and starve them if you want to simulate race conditions with a lock instead of tasks you will need to create threads what's going to happen with tasks is once a task is on a thread it does not get swapped out for another task however if you have multiple threads that are locked and let's say you have a thousand threads what the CPU what the processor is actually going to do is called context switching it's going to take one thread pick it up and place another one on top of it if you have a thousand threads there are locked the CPU is going to schedule them one by one and over time the lock will sort itself out the lock will not sort itself out in the case of asynchronous tasks in this situation so locks are essentially no good for us another way to do synchronization is by using a mutex so a mutex equals new mutex kind of like the semaphore if we take the mutex we can wait for one not the safe handle wait 1 and then by the end of it on the mutex we release mutex mutex can be used with asynchronous code let's remove the getaway to get a result if we run our application here we will experience a thread starvation for all the same reasons because weight one is a blocking operation in this situation the mutex does fail but however a very good situation where a mutex can be used is if you have a computer and you have two applications on that computer and two of the applications are trying to access a file instead of using a distributed lock you can use a mutex to achieve synchronization between two different applications the mutex will be provided by the operating systems but then you actually have to request it correctly through the class and ask to try open an existing one but that is beside the point here most of our applications are going to be asynchronous and the main thing that we do want to be using for synchronization in our application is a semaphore we want to say that we have opened up the gates for a single process to enter so we really think I'll wait and then we're gonna wait for the incrementation of our number once we're done with the process we're gonna go to the semaphore and we're going to release let's run the application and the application finishes if we look to the number it is a thousand if you are again wondering how is that a thousand when we create safe number we're sitting it with a zero and then this is just happening a thousand times coming back to program CS if we do remove the synchronization remember that we're just gonna get a big fat exception now let's say that we're in a distributed scenario where we have a server form so 10 machines all trying to access the same resource and doing work on that resource in parallel the way you do synchronization around a resource in a distributed scenario one of the easiest ones that you can do is by using stack exchange redis so you spin up a redis instance a single one and all of the boxes will talk to that redis instance to acquire a lock I've already added the stack exchange to read this package to it this is what this is going to look like and I also have a redis connection running so if I type in read this CLI and I look for all of the keys currently it is empty let's establish a connection so we are going to go to the connection multiplexer and we're going to connect we're connecting to one two seven this is our connection multiplexer from the connection multiplexer you want to get database and this is going to be ADB let's comment out this work that we're doing over here let's again take our task the way to do a distributed lock with redis is you go to the database and you look for a look you want the lock take async we'll allow it on this you want this to be something unique then you need some kind of sample value and then you also need the timeout what you don't want to do is set an infinite timeout and then if the operation fails and you don't release a lock you don't want your application to be deadlocked so time span depending on how long this operation takes let's say that we are going to lock it for one second or maybe even 10 just to be sure this operation is going to return a Boolean so VAR taken while this has not been taken so it's not like the operation is going to stop here and the Block it's actually going to continue so we want to retry this and you know if you hear retries volley is what you want to use however what I'm going to say is await task humble delay and let's say for like five milliseconds I'll also take this asynchronous operation and put it over here remove the taken variable so while we fail to acquire the lock just wait for the lock to be free also when you're performing your operations what you want to make sure you're doing is you're performing them in a try catch and you always have a finally block which is going to be responsible for releasing the lock same story with the semaphore for release and same story over here so to the database we go again we search for lock lock release async we put the same variables number and the same Value Place them here semicolon on the end oh wait and here is essentially our distributed lock let's come back to the application rerun it it's going to take a little bit of time to complete but in the end we do get a thousand and the reason it takes a little bit of time well we got all of these delays and there is a thousand of them so you know it's a little bit expected and that is pretty much all there is in order to simulate a race condition you want to use some kind of synchronization to line up all of your parallel tasks right before you execute the logic which might potentially contain a race condition and then you start all of the tasks at the same time in order to resolve a race condition you can either use a lock a mutex a semaphore or a distributed lock for the distributed lock I highly recommend readers although if you have any other database you can get a distributed log by using that database if your database doesn't have logs but has transactions you can create your own lock most of us are going to be working with running multiple tasks asynchronously this is why I recommend you invest in the semaphore and don't touch the lock or mutex and then if you have multiple machines running you want to achieve synchronization by using a distributed lock this will be it for this video thank you very much for watching if you enjoyed it don't forget to leave a like And subscribe if you have any questions go ahead and leave it in the comment you can also use the comment section to say thank you but better yet you can come support me on patreon get the source code very big thank you to all of my patrons that are already supporting me your help is very much appreciated hope you all have a good day and goodbye
Info
Channel: Raw Coding
Views: 8,562
Rating: undefined out of 5
Keywords: csharp, dotnet, race condition, test, solve, lock, mutex, tutorial, semaphore, redis, distributed lock, simulate, .net, asp.net core
Id: M1N9huHatLo
Channel Id: undefined
Length: 18min 58sec (1138 seconds)
Published: Tue Dec 20 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.