Rust's concurrency model uses OS threads directly (unlike green threads in Go/Erlang), with key abstractions including channels for message passing between threads (MPSC - Multiple Producers Single Consumer), mutexes for mutual exclusion ensuring only one thread accesses shared data at a time, and Arc (Atomically Reference Counted) for safe shared ownership across threads. The move keyword enables transferring ownership of data to spawned threads, while join handles allow the main thread to wait for spawned threads to complete.
Rust Concurrency Explained: Threads, Channels, Mutex, and Arc
Added:hi guys welcome to the introduction to rs2 toriel my name is tensor today we'll be covering concurrency and rust so the main building block for concurrent programming is what is called a thread and there are two main types of threads in programming the first type is what is called an OS thread and the second type is what is called a green thread now the operating system thread is actually offered by the operating system itself and a green thread however is an abstraction that sits on top of the operating system thread the languages like go and Erlang and elixir use what are called green threads and this allows them to spawn many more threads than a normal language that only used operating system threads could for instance go may be able to spawn say 10 green threads for every operating system thread and this would scale up based on the power of the computer that you're actually writing on rust however uses the operating system threads directly and the reason why is for the sake of having a lower runtime by lower runtime of course I mean a lower amount of code included in each binary after it's compiled alright so here we have a fairly basic example first we need to bring in the thread namespace here so from the standard library we bring in thread and then in our main function we create a mutable vector that's empty and then we iterate from 0 to 9 so 10 iterations and for each iteration we push a new thread they get spawned with thread spawn into our vector and we use this thread spawn method with a move closure inside of it now this is very important this move keyword and this move closure then prints out the thread number with I embedded in it so for instance the first iteration will be through out 0 the second iteration will be thread 1 excetera all the way up to threat 9 then we have another for loop here which will iterate from 0 to 9 again and it will print out main thread 9 times all right so when we run this program you can see here that we get thread number 0 and we get thread number 1 then the main thread run sat for loop which iterates 10 times and then we get thread number 3 then thread number 2 and then thread number 5 when total we had 5 external threads run and the main thread run what happened to the other five external threads well quite simply even though our for loop here resolved itself the other five threads never got to print out their thread number and that's because the main thread actually terminated before they actually were able to do so so the threads spawn method will return what's called a join handle which is a known value when we call the join method this join method here on our joint handle it will force the main thread to wait for the thread that's attached to the join handle to finish we can come in here and we can say for J and C so we iterate through our vector here one thread at a time and then we call J dot join on each thread and what this will do is it will join up each of the threads to our main thread so it'll force the main threat to wait for all the other threads to resolve and you can see here that we get all of the Reds back but they of course spawn in a non-deterministic manner we get thread four first then we have thread zero and thread three then eight and seven and two one five nine and six if we run this again you'll see that the order will change so rust makes no guarantees about the thread order thread zero could run last and thread nine could run first and honestly in most cases we don't really care which thread ends up resolving first in which one resolves last all right let's consider this example so we create a V vector with one two three in it then we create a thread spawn with a move closure in it and inside of this thread spawn we have a println function with B inside of it and then we call handle that joint at the end to wait for the actual thread to solve itself now the reason we're considering this example is because we want to talk about the move keyword here if I remove the move keyword you'll see that we'll get an error here and the error is with the closure itself now the reason we're getting this error is because the move allows the closure to use the data from one thread to another thread essentially we're we're taking ownership of this main threads data inside of this closure here we've mentioned the move keyword before when we talked about closures and we also mentioned that the move keyword forces the closure to reference data by value rather than by reference in this way we can capture values from the environment while starting new threads however rust infers how to capture in this case V and the macro println only needs a reference so the closure tries to borrow v when we put in the move keyword here instead of borrowing V we're actually taking V and putting it inside of this thread and giving it a complete ownership we use the move keyword to force the closure to take ownership of the value and when we use the move keyword we guarantee to rust that the main thread won't be using the captured value anymore if I come down here and I make a println statement and I try to interact with me again it shouldn't work at all and you'll see here it says use of a moved values V is being completely removed from our main scope here and being put inside of our enclosure and our thread here so this concept is pretty important when thinking about some of the more complicated stuff inside of concurrency so the second main abstraction that rust uses for concurrency are what are called channels so you can see here that we're spawning a channel and we're spawning and we're setting it equal to a destructed tuple here with TX and rx in it then we're spawning a thread here with a move closure in it and inside of it we're calling this TX value and we're calling a send method on it with an unwrapped and then afterwards we're calling a println statement which calls this rx method with a receive V method and another unwrap attached to it so channels are used to pass messages around and a channel is made up of a transmitter which is this TX and a receiver which is this rx the transmitter is the part that sits upstream where we actually push the message in and then the receiver is where the message comes out of course we destruct a tuple that is returned by the channel method and the TX has a send method that takes the value we want to send and returns a result and that's why we're using this unwrap here and basically the result will return an error if for instance the rx portion goes out of scope it's been dropped for any reason and we can't actually complete the message sending the RX portion on the other hand has two useful methods one's called receive V and the other one is called try receive e receive v is a blocking method so it actually will block in this case the main threads execution and wait for the message to be passed through from the TX to the rx try receive e however is non-blocking and we would use it in cases where we don't need an immediate result and maybe we want to have the threat to continue doing other things while we're waiting for messages so in this case we're just using receive because we want the message immediately because we don't really have the main thread doing something else this program in essence should just return 42 which were then yet print out in this println statement and you can see here predictably we get got 42 another important thing to make note of is that our channel here comes from a namespace called MPSC now this stands for a multiple producers single consumer and this is a concept that permeates rusts concurrency model right so this example has multiple functions so we have a function called timer which takes in a you size and a sender and then up top we have this constant called num timers which has a 24 value in it then we spawn our thread inside timer and we have a println statement which prints out D which is our you size and then we make the thread sleep for D seconds and then we print out again that D was sent and then we send D through our transmitter ear with the send method then inside a main we create our channel here with TX in it then we iterate from 0 to 24 and we then call timer on I and TX clone so we clone the transmitter multiple times and then finally we iterate through our receiver here and we iterate through it 24 times and then we get the messages back from our timer function here so this basic example follows the multiple producer single consumer idea here we have 24 different producers and we only have one consumer so the main function is getting all of the results back from our multiple different threads here we've run it and you can see here we've got zero setting timer one setting timer and then this goes all the way up to 6 and then it goes zero cent then it goes 13 setting timer 8 setting timer 9 setting timer 10 up to 12 and then it goes seven setting timer etc and it keeps going going going till finally hits 23 and then we go zero received one sent one received two sent two received and this keeps going all the way up to 23 there's some time between each of the periods when it's being set when it's being sent and when it's being received because we've actually actively caused the thread to stop for a moment and we did that with our duration from seconds method here all right so channels are pretty powerful and they're pretty good way of sending data around from one thread to another however rust has a second abstraction that it uses to communicate shared memory inside of its concurrency model and that is called the mutex which stands for mutual exclusion basically a mutex only allows one thread to have access to a piece of data at a given time there are two main rules with the mutex is in first the thread that need they wants to access the mutex needs to acquire the lock of the mutex and then once you finish the data you must unlock the data so that the other threads can then acquire the data you can kind of think of a mutex sort of like a storage locker where you only have one key but you have multiple persons who have access to it each person who wants to gain access to the locker will need to have access to the key if another person wants to gain access to the locker then they need to go to the person who owns the key take the key from them and then go to the locker and open it now our mutex also sort of acts like a smart pointer this is similar to our box pointer or more appropriately the mutex lock method returns a smart pointer called mutex guard which implements the DRF and dropped so it will automatically let go of the lock when it goes out of scope let's take a look at an example of this we first create a mutex now this mutex is embedded in what's called an arc arc is an atomically reference counted type we need to do this because arcs convert the types into primitive types which are safe to be shared across threads essentially we're converting this mutex into a type that sort of acts like a primitive type well it's safe for being shared across multiple threads and then we're creating a mutable vector here then in our for loop we're iterating from zero to nine and then we're creating another arc and we're cloning a reference to our c mutex here then we're creating our threads here by spawning them we're using our move closure again inside of this we have a me twelve alyou which is assigned to c dot lock now this is of course our thread gaining the lock control of our mutex so gaining the value inside of our mutex in this case zero we increment that number so we need to of course this is a smart pointer so we need to dereference it then we want to increment it then after the thread goes out of scope remember that the rock gets dropped out so automatically our value will automatically become unlocked so that the next thread can grab hold of it then we want to push our H or a thread into our vector here then we're going to iterate through our vectors we're gonna call join so that our main thread will stop and wait for all the other threads to resolve and of course we call unwrap on both this and on our lock function here because the lock function also returns a result here and you can see that the lock method has a mutex guard in it which is our smart pointer finally we're going to print out our result here and this will be ad reffed version of c dot lock so again we need to call lock for our main thread and of course we're going to unwrap it too so predictably our result is 10 you can just look at it like this each thread is spawned and when it's spawned it then gains the access to the mutex increments the mutex by 1 then releases its access to the mutex and then the next thread gains access to the mutex increments it by 1 releases it and so on and so forth and then finally at the end after all the threads resolve themselves with the join method our main thread gains access to the mutex and then prints it out now if we wanted to see the incrementing actually happening we could print out the number here and you'll see here that the threads go one two three four five six seven eight all right so now let's look at an example that actually implements both a mutex and a channel first we have a function called is prime and this checks the prime allottee of our numbers and this is a really really expensive way of doing so so what we're doing is we're iterating from 2 all the way to N and in this case we're starting at 10 million so we're iterating from 2 all the way to 10 million and then we're checking for prime ality with this then we have our producer function this takes in our channel transmitter which is our sync sender here and then it returns a thread joint handle here it spawns a thread and inside of the closure here it iterates from a hundred million all the way to infinity and while doing so it sends a message through our transmitter and of course we unwrap that as well because it's a result here then we have our worker which takes in an ID of you 64 our shared rx which is a mutex for our receiver so we've actually wrapped our receiver with a mutex and an arc in here we spawn another set of threads and we loop them over and over again now each one will loop over and over again and we have a value n which is mutable and then we are going to match over our rx unlock so we're going to unlock the actual rx in the thread and then match over it and if we get a ok back then we are going to try to receive the message if we get the message then we're just going to set it equal to n otherwise if we get an error we're just going to pass back a unit type and if we can't match over the rock or for instance if it can't unlock the mutex then we just send back another unit type here as well by the end n is not equal to 0 which it shouldn't be because we should have passed a few messages through here then we check if the number itself is prime by passing it up through this is prime function here in which case it'll go through and print out each of the prime numbers so you see here in our main function we create our sync channel here and in this case with a st. Channel we have to setup the amount of memory that we want to put into it in this case we're putting 1024 buffer memory inside of it then we're setting up our arc mutex with our X inside of it then we're going to iterate from 1 to 5 and spawn our workers so we're going to have 5 worker threads and inside of this we pass in I itself so we start with one and then we pass in our shared rx then finally we're going to run our producer and we're gonna call join so that the main thread will actually wait for all of the other threads to stop and we're going to of course unwrap it just a quick note before I run this program when dealing with really large numbers you can put underscores here similar to like putting commas to every thousandth place you can put an underscore and then another underscore so in this case for a 10-million we put two underscores here it's nice to have them just for readability sake I don't think I ever went over that when we talked about integers though all right so let's run this so as I mentioned before this is a pretty expensive way of looking for primes and you can see here that we have worker to found a prime and this says ten million and thirty seven then we have worker one found a prime which is 39 and it keeps going like this and it will keep going and going and going and iterating over itself and trying to find Prime's every time we get back a message it will then send back another prime it's slowly getting more and more primes and this in theory should continue to iterate until we run out of memory or until Russ just can't parse the numbers anymore so if the numbers get too big and Russ can't parse them anymore then it will automatically just kick out of the program you can see here we're already up to a fairly large number again like I said it's a fairly expensive way of looking for prime numbers all right so I'm just going to manually quit out of this all right guys well I hope you enjoyed this tutorial if you did feel free to like and subscribe you have any questions or comments feel free to leave them in the comment box below and if you disliked it then download it as much as you'd like anyway have a good night
Up Next

Building gRPC Services in Rust with Tonic: A Comprehensive Guide
@dreamsofcode
71.6K views•2024-02-22

BitTorrent Protocol Explained: Piece Selection & Peer Choking
@StevenGordonAU
481 views•2013-02-22

HTTP Requests Explained: GET, POST, PUT, DELETE
@codecademy
103.1K views•2021-10-07

Enigma Machine Mechanics: WWII Encryption Explained
@JaredOwen
13.2M views•2021-12-11
Related Study Plans & Knowledge Roadmaps
Structured learning paths in Computer Science








![[linux.conf.au 2014] The Rust language: memory, ownership and lifetimes](https://i.ytimg.com/vi/rVOwQknpH9A/sddefault.jpg)



















![Visualizing memory layout of Rust's data types [See description/first comment]](https://i.ytimg.com/vi_webp/rDoqT-a6UFg/maxresdefault.webp)




![Lock-free programming with modern C++ - Timur Doumler [ACCU 2017]](https://i.ytimg.com/vi_webp/qdrp6k4rcP4/maxresdefault.webp)




