Share Rust Thread Data With Mutexes πŸ¦€ Rust Tutorial

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey guys my name is Trevor Sullivan and welcome back to my video channel thank you so much for joining me for yet another video in the rust programming fundamentals video series on my YouTube channel now the concept that I wanted to cover in this particular video actually Builds on top of some of the things we discussed in earlier videos if you're following along with my rust programming tutorial playlist so in the last couple of videos we took a look at the introduction to threading and then we also understood what scoped threads are which allow us to share data between the main thread as well as any threads that we instantiate from the main thread but there's actually other ways of sharing data between different threads within a rust program and one of the ways that we can accomplish that is by using a built-in construct in the standard Library known as a mutex now if you don't come from a computer science background and the word mutex sounds kind of intimidating it's actually a very very simple concept basically a mutex is a mutual exclusion what it basically means is that you can take a piece of data in memory you can wrap that into what's known as a mutex and the mutex helps to guard that data so that only a single thread is able to access that data at any given point in time that way if you build a rust application that maybe has 10 or 20 or 50 different threads running of course depending on how many CPU cores you have in your system then you can make sure that your code can safely access that memory from all 50 different threads but in a mutually exclusionary manner right so we only want one thread to actually have access to that data but as soon as that thread is done access accessing that data we want to then basically give it back to the system so that that another thread can then access that data so mutexes are actually really simple to understand we're going to go through and actually take a look at how to code around a mutex and access some state from our main thread from Child threads in our rust application but first I wanted to point you to an article here in the rust programming language book which is kind of one of the primary sources for learning about rust in the rust documentation here if you go under chapter 16 they have chapter 16 subsection 3 that talks about shared State concurrency and this example talks a little bit about mutexes so if you'd rather read about this topic than watch this video about this topic you can go right here and they do a pretty good job of describing how mutexes work but in this really simple example that they have right here basically what we're doing is we create this new mutex and this just wraps a piece of data right in this case it's just an integer but you can use other data types in here as well and then what you want to do with a mutex is call this method called the lock method and what the lock method is going to do is it's going to give you exclusive control over the data that's been wrapped by the mutex so inside of this subscope right here within this application this isn't even a separate thread right this is just a subscope within the same main thread in an application and so what this scope is doing right here is it's calling this lock method that gives this scope exclusive access over this bit of memory that's wrapped by the mutex and then it unwraps the underlying data type so very similar to the option type that allows you to get some or none of a value we basically unbox or unwrap the underlying data and then we can go ahead and mutate that data as long as we have a mutable reference to the underlying data now in a multi-threaded situation you have to make sure that your thread is politely giving access back to the mutex right so you might think well if I'm going to lock this data and have exclusive access to the mutex how do I exactly unlock it well until future versions of rust actually have an unlock method on the mutex currently that's an experimental feature in the nightly builds of rust as you'll see in the actual documentation for this module called the sync module which is part of the standard crate but in the current version of rust basically what you do is you just let that mutex handle go out of scope it's called a mutex guard in fact and once that mutex guard goes out of scope then the control is handed back to the mutex so that another scope or another thread can then lock and retrieve and mutate the data that's wrapped by the mutex so it works for subscopes and it also works for thread based approaches as well so now there are other types in the sync module that deal with wrapping different parts of memory but just to keep things simple so that we really understand what a mutex does by itself we're just going to focus on the mutex type for this particular video now if you jump into the documentation for the standard crate right here there is a child module so let's just go up to standard here and then if you take a look at the modules there's all of these built-in modules in the standard crate and if you scroll down a little bit towards the alphabetical sorted s here we have the synchronization Primitives here in the sync child module so we go to standard then sync and then if we go to the data structures that are exported by the sync module here you'll see that we have this mutex struct along with the mutex guard and those two different data structures work hand in hand in order to wrap the data in memory and then temporarily Grant access to the memory now if we take a look at the mutex type right here we can simply create a new instance by calling the new method on the mutex type and then pass in our data so that'll create a new Mutual exclusion lock and then of course there is the lock method right here which gives us this lock result which gives us the mutex guard that we can use to have exclusive control over the data that's wrapped by the mutex now if you look for the unlock method right here you'll see that this is a nightly only experimental API if you're running one of the production builds of rust I think I'm on 1.72 right now then this unlock function is not going to be available in general unless you are doing really cutting edge development you probably don't want to be using the nightly builds just because there's a lot of unpredictability things are changing rapidly and you might just run into new bugs in that nightly build so for most people out there you're probably going to want to stick with the production builds and this unlock method on the mutex itself isn't currently available so wait a little while for that to come out it is kind of a nice enhancement to the language just so that it shows in your code that you're explicitly giving control back to the mutex but for now you'll just have to remember that when the mutex guard goes out of scope the control is automatically handed back to the mutex all right so there's another method down here called trilok now there's a key difference between the lock and the try lock methods down here so one is going to give you control back to the thread the other one is going to block access to the thread so with the lock method right here this is the one that you probably just think to use by default because it just has a very clear name but if you call the lock method this will actually cause your thread to hang until the mutex is unlocked what that means is that your thread will not be able to do any other work until the mutex is released and that thread can then go ahead and grab the lock handle to the mutex if you want your thread to continue doing useful work work while it's checking to see if it can get a lock on the mutex then the trilok function right here on the mutex is going to allow your thread to continue you running if you would like that to happen so this will go ahead and just not block your code so you can just periodically check in with the mutex and say hey are you ready yet are you ready yet can I have a lock can I have a lock and if the answer is no you just go off and do some other work but maybe every five seconds or maybe maybe every 30 seconds you just go back to that Muse text try to get a lock on it and if you can't get a lock on it that's fine we'll just go do some other meaningful work in the meantime so those are a couple of patterns that you can use with the mutex type here so now we're going to jump into some code and actually figure out how this works from a practical standpoint before we jump into the code sample though I just wanted to remind you that I am an independent content creator this is my job to help you guys understand rust and other types of technical topics so please go out to my channel youtube.com Trevor Sullivan subscribe to the channel that really helps me to grow this community and bring you more great content also if you learned anything new by the end of this video please leave a like on the video and a comment down below to let me know what you thought of this particular video all right so let's go ahead and jump in to our code here so we're going to be building on this code that I've previously been creating here so under main.rs right here we've got all of these extra modules that we've been calling out to so what I'm going to do is just comment out any references to the scoped threads module that we previously discussed and we're going to go ahead and create a new module here to play around with mutexes so I'll just call this my mutex DOT RS and we'll expose a public function from this module here so let's call this Pub FN and test mutex for example and then we'll go back here to our main function and we'll say Pub mod my mutex so we can use the functions and then at the end we'll just say my mutex and call test mutex so now everything is set up for us to write some code inside of this function that creates a mutex and then attempts to lock access and mutate the data that's wrapped by the mutex so let's start out by declaring a number right so how about let's do score and we'll say score is equal to an unsigned 16-bit integer but what we want to do is actually provide a default value so we'll just do an initial score value of zero and then we need to wrap the unsigned 16-bit integer in the mutex type so to do that we're going to do mutex double colon new and then pass in the data that we want to wrap in the mutex now because we haven't referenced the mutex type here in our module we need to do a use standard sync and then mutex and that will import the mutex struct so that we can call into that struct and use the new method here in order to instantiate the mutex so now you can see right here the rust compiler is inferring that my score variable is pointing to a mutex that wraps an unsigned 16-bit integer of course if I change the initialized data right here that'll change the underlying type of the mutex as well so the rest compiler is able to determine just based on the value that I'm passing in what data type is appropriate for the score variable here all right so now we've got this mutex right so if we wanted to change something inside of this mutex let's say we wanted to add 5 to the score well let's say we do score is plus equal to 5 right well first of all we can't mutate an immutable variable so you might think all right well let's mutate let's make the score here mutable right well we can't directly add an integer value to a mutex right so we actually have to get access to the underlying data that the mutex is wrapping so that we can manipulate that value so you might say all right well let's do score dot unwrap or something like that well unwrap isn't even available right there the only functions that we have available here on the mutex type are of course to deal with the mutex itself so you might say well maybe I'll get a mutable reference to it and of course we can't do that either so what we actually need to do is get the mutex guard so that we can access the underlying data the way that we do this is by doing the mutex Dot Lock and this lock function right here is going to return the mutex guard inside of a result right here so that we can get access to the data so let's go ahead and assign the results here to unlocked data and we'll say let and then we should be able to get an exclusive lock to mutex here because there's no other code inside of our rust program here that is trying to lock it this is the only reference to the mutex and the only call to the lock method so once we have the unlocked data here we still need to unwrap the underlying data so then we can call unwrap right here and this will be our actual data right because right up here we get the mutex guard that's wrapping the data but then when we call unwrap we actually get access to the data itself so let's go ahead and try to do data plus equals five right here and you'll see we can't mutate the immutable variable so let's do mute on data and I think I also need to call another function here actually no in this case what we have is a mutex guard right and so in this particular case I can't just do plus equals five what I actually need to do is say data and then what we can do is use one of these methods right here on the type so what we can do is say data dot add assign and then we can specify a value that we want to add to the underlying data and so let's say five right so what add a sign does here is it takes the underlying unsigned 16-bit integer whatever value it currently has it adds 5 to it and then it assigns it back to the original mutex so now what we can do is say you know print line just grab the value here and pass in data and let's do a debug print value here and then let's do a cargo run so sure enough you can see that we have the value of five but what happens if another thread comes along and wants to access this exact same data well in order to unlock the data what we have to do is make sure that the mutex it the mutex guard rather goes out of scope and so what we can do is explicitly do that by dropping the mutex guard so if we drop unlocked data here actually let's try dropping data then we should have access again to mutate the mutex by getting a new lock on it so we're going to actually do a multi-threaded scenario so that you can see in practice how this actually works so let's get rid of this code right here I'll just comment that out and then we'll create two different threads okay so our first thread is going to be a closure let's do my func we'll make it a closure here so if you watched my video on closures these type characters denote the input arguments to the closure or Anonymous function and then we can run some code inside of here like let's put a semicolon there so we'll say four one or I in 1 2 10. we'll go ahead and just add I back to the mutex so let's start out by getting a lock from the mutex so we'll do score and this doesn't actually need to be mutable itself so we'll say score Dot Lock and we'll say dot unwrap as well so we'll say let data equal that and then inside the for Loop we'll just do data dot add assign and we'll pass in whatever I is and of course we need to make this data reference mutable so that we can change the underlying data of the unsigned 16-bit integer and then after the closure ends so after the scope of the closure ends right here on line 17 with that ending curly brace then the mutex guard is going to automatically go out of scope and return control back to the caller so let's go ahead and attempt to call this closure and see if we can access the mutex from the main thread after the child thread actually executes so what we're going to do is say use standard thread and you should already know this from our last couple of videos and we're going to call spawn here so we can spawn a new thread and now we'll say spawn and pass in our closure which is my funk and then the other thing we want to do is to join because that will allow our main thread to synchronize with the child thread all right so it is saying that the closure May outlive the current function but it borrows probably the mutex itself right so yeah so it borrows score so what we should be able to do is move ownership into that child thread all right so now what we're doing here is moving ownership of the score into the closure and that will probably mean that we can't use it after the thread executes right let's go ahead and just discard this result right here let's run our application so it compiles and it executes just fine right but we're not attempting to access the mutex after the fact so let's do a print line here and we'll say debug output and then we'll do our new text Dot Lock dot unwrap and sure enough it says borrow of moved value and that's because we are moving the mutex into this child closure here so in this case what we actually want to do is wrap it in the scope type here so let's do a scope instead and we'll say scope and then we want to spawn a scope and inside of that scope we are going to call the myfunk so we'll do scope spawn and then we'll do my funk right here and put a semicolon at the end and let's just comment this out for a moment right here to see what we've got here all right so we've still got the move keyword in here but because we are doing a scoped operation here we don't need to worry about move and that will allow us to borrow the mutex inside of our child thread here so now that we've wrapped it in a scoped operation we are able to borrow the mutex inside of the child thread but then we still have the ability because score is still owned by this function test mutex right here we can still use that variable from the scope that it was actually declared in so that's again why you want to generally use scoped threads because when you start getting into borrowing values from that parent scope it just makes things a lot easier to deal with so now if we run this here we can see we get 45 which is basically 1 through 10 added together now let's create another thread okay so what we're going to do is copy this here and we're going to create a second thread here and each of these threads is going to attempt to lock the mutex and then we're going to add 1 through 10 from each of these threads so we should have basically double the value of 90 instead of just 45. so now we'll go down here and we'll say s dot spawn my func to and then we'll do some debugging statements in here just so that we can kind of see what's going on so thread 2 is adding I and that'll just kind of show us the current iteration of this for Loop here and I'm also going to say print line at the very beginning of thread number two and Say thread 2 is waiting for mutex lock just to show that it's pausing execution because lock here is going to block any additional execution of that thread until it does actually receive a lock and then let's go ahead and copy these into our first thread as well so we'll say thread one is adding whatever I is and we'll say thread one is waiting for a mutex lock all right so thread whoops let's see thread one is waiting for the mutex lock there we go just had a little visual issue there so we're spawning both of those threads then we're accessing the mutex from the main thread after the fact so we have the main thread accessing the mutex and two child threads that are accessing the mutex as well and if we do cargo run you can see that thread number two is waiting for lock and apparently thread 2 actually got the lock first so even though we invoked the second thread after the first thread in our spot our scoped spawn operation right up here on line 33 and 34 thread number two actually got access to the lock first and then thread number one is also waiting for the lock and after thread number two the mutex guard goes out of scope right so let's go over that really quick so on line 25 we declared the mutex guard as mutable and then after the scope ends after the thread is finished executing that mutex guard automatically goes out of scope right and so then what happens is that thread number one is able to retrieve its lock because it's basically hanging there in the background waiting for the lock and it goes ahead and does its work and then the main thread at the very end is able to again get a lock on that same mutex and retrieve the results so that we can display that result so that's how mutexes work at a basic level however there's a little bit of a gotcha what exactly is going to happen in our program here if one of these threads fails so what we'll do up here in thread number let's actually do it in thread number two because I think pretty consistently for some reason thread number two is actually causing the lock to be retrieved first so what we're going to do here is after we get the lock on the mutex we're going to go ahead and panic and say error in thread two so what do you think is going to happen well we kick off these two threads thread number two generally gets access to the mutex first so what do you think is going to happen when we get a panic well what happens here in this particular case thread number two waits for its mutex lock it gets the lock and then it panics and that causes the entire application to panic because inside of our scoped threads here we did not actually handle any panics in the child threads so normally the scope here just goes ahead and joins each of these threads so it waits for the threads to finish but if you don't handle any errors or panics inside of these threads then the entire main thread is just going to crap out and it's just going to crash your entire program so what we want to do is actually handle things a little bit more gracefully at the thread level detect if there is a problem and then see if we can release the lock on the mutex right so what we're going to do inside of here is get a couple of handles to each of the threads so we'll say handle one and then dot join because the join function here is what gives us the handle back and then we'll say let handle 2 equal Bond dot join all right so now we have these two handles we're spawning the threads we're joining them and then what we want to do is to check to see if there was an error so we'll say if handle to dot is error then we want to basically just say something like thread to add an error let's handle it here right so we'll go ahead and do a cargo run here and now we're attempting to handle things a little bit more nicely here so what you'll see is that it looks like this time thread one actually got the mutex lock first so let's try that again see if we can get thread two to get the mutex lock first of course now it's probably not going to cooperate with me so what I'm going to do is actually just switch these around so let's do my funk 2 first and then we'll do my funk one all right so what you're going to see happen here is that thread number two got the mutex lock and then it panicked but what's happening here is that thread number one is never able to get the lock because we never actually dropped the value of the mutex Guard right so if an error does actually occur in a thread and you don't drop the mutex guard then the other thread is never going to be able to handle the mutex right see what you can do is if an error occurs inside of your program in the thread we can go ahead and call drop on the mutex guard and that will force it to be returned and this is a more graceful way of handling things because now thread number one can actually go get the lock and thread number two can just have its error and deal with it in some other way but at least that way our thread isn't holding up thread number one from executing right so that's something that you'll want to be aware of when when you are dealing with multiple threads accessing the same mutex as being able to handle any errors that occur in those threads and make sure that they politely give access to the mutex back to any other threads that need to access it now let's take a look at how the asynchronous method Works where we can try to get a lock but if we don't get a lock then we can do some additional work right so what we're going to do in one of our functions here is let's actually drop the Panic here so I don't need to explicitly drop the mutex guard anymore and what I'm going to do is go in here and we're going to say Loop so once we enter this child thread we're just going to Loop and then we're going to try to get a lock inside of this Loop so we'll say let data actually we'll just kind of move this right up here so we'll move the mutex guard into the loop right here and we'll also say thread 2 is waiting for the mutex lock right beforehand and if for some reason we don't get a mutex lock then we want to sleep for a certain duration and maybe do some other work in the meantime and then later we'll try again to get another lock right because the try lock method here is going to be asynchronous so what we want to do is import the sleep so we'll do use standard time and then sleep and then we can call the Sleep function actually we want sorry duration here we're going to use thread sleep so let's import sleep from the thread module and then we have to pass it a duration which is why we have to import the duration type and so we'll say sleep and say duration uh new let's do new actually from milliseconds not new and we'll just say sleep for 300 milliseconds all right so now what we want to do is say after we attempt to get the lock right here right so after we attempt to get the lock on the mutex if it does succeed then we want to run this code right here that does the add operation for our program so what we'll do is say if the data is successful in getting a lock so I think we want a function here saying if it is successful let me figure out exactly where that is in here so actually I think what we need to do is just try to get the lock and then we'll actually unwrap it in just a second here so we'll say let guard equal try lock and then we'll say let data equal guard Dot unwrap and make that mutable so then on the guard itself not the underlying data we want to say is it okay and if it's okay as in it got the lock then we want to go ahead and run this code here all right so it looks like we have borrow of moved value let's see what's going on here and of course what I just realized I didn't do is actually unwrap the data inside of this if statement here because as long as the guard successfully got a lock then I need to actually go inside of this if statement in order to actually unwrap the data so we're just going to move that statement to unwrap the data right inside of here and that lets us use the guard variable right here in the conditional statement of the if loot of the if block all right so then we are going to go ahead and sleep after the if block if for some reason it fails to get the lock and then after the if statement succeeds then we can break out of the looping construct that we have because our work is finished so this thread my func 2 is going to infinitely loop it's going to infinitely attempt to get a lock on the mutex for every 300 milliseconds and if it doesn't get a lock it's just going to keep looping and as soon as it does get a lock it's going to do the processing on the underlying value of the mutex then it will break out of the function gracefully and the thread will finish and it should drop the Guard the mutex guard and that'll give control back to the mutex alright so let's give this a try and see what we get here also I don't think I really need this error handling stuff right here anymore because we're not going to panic inside of any of our threads here and so now it looks like thread number two was actually first to get the lock so let me switch these around again so put the second one second and the first one first and this time everything runs normally again but in order to kind of demonstrate what would happen if thread number two was not able to immediately get a lock it's able to get a lock just because the operation is happening so fast here but what we're going to do is actually sleep a little bit inside of the thread number one here on every iteration of our Loop so let's just say that we're going to sleep for maybe 400 milliseconds for each iteration so now when we do cargo run we should see that thread number two is waiting for the mutex lock here and it should be printing that out repeatedly here so let's see why it isn't doing that and I'm guessing the reason it's doing that is because we are doing a join operation here so we're actually waiting for the first thread to finish so we don't want to join in here we're just going to do a spawn operation and let it automatically join thanks to the scoped threads so let's remove the join from there as well and now we're going to kick off both threads immediately with each other all right so this is a little bit more what I was thinking was going to happen here but I got the threads reversed again so let's put number two first and this is what I was hoping would actually happen here so now you see every few hundred milliseconds that thread number two is waiting for the mutex lock and this is that looping construct actually taking effect here where every 300 milliseconds it's going to re-attempt to get a lock by calling trilok but then the lock is failing so we don't have is okay for the guard right here and so it's just skipping over this entire if block that we have right here it's going to sleep for 300 milliseconds and on the next iteration of the loop here it's going to once again try to get another lock and so you can see that while thread number one is going through every 400 milliseconds and adding a number here thread number two is continually checking to see if it can get a lock and finally after thread number one is finished after adding the number nine right down here then thread number two successfully retrieve leaves the lock and it does its work returns back to the main thread and the main thread is then able to access the mutex on line 51 and unwrap the underlying value and simply print it out so mutexes are pretty cool they're very interesting to work with they're also very easy to work with so again even if that term is kind of intimidating to you as somebody who doesn't come from a computer science background hopefully this gives you a better understanding of how they work and you can Implement them into your own programs anyway that's all I had for this particular video again please leave a like on the video If you enjoyed it leave a comment down below let me know what you thought of this video And subscribe to the channel and we'll see you in the next video take care
Info
Channel: Trevor Sullivan
Views: 3,340
Rating: undefined out of 5
Keywords: rust, rustlang, rust developer, rust programming, rust software, software, open source software, systems programming, data structures, rust structs, rust enums, rust coding, rust development, rustlang tutorial, rust videos, rust programming tutorial, getting started with rust, beginner with rust programming, rust concepts
Id: ycM5jqYT9Og
Channel Id: undefined
Length: 35min 35sec (2135 seconds)
Published: Tue Sep 05 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.