Random Rust Notes - 2
Nirmalya Sengupta
Posted on April 17, 2023
[Cover image credit: https://pixabay.com/users/engin_akyurt-3656355/]
This blog is a collection of notes of key learnings / understandings / Aha moments, as I am kayaking through demanding yet exciting waters, of Rust (#rustlang). If these help someone, I will be happy of course. OTOH, if someone happens to find defects in the code or gaps in my understanding, please correct me. I will remain thankful.
As a part of a personal project, I am in the middle of learning how to use channels to communicate between two threads. This is quite a common topic. Many people seem to have questions around this topic. Many answers / explanations are available.
These are my notes, though.
Takeaways from the previous note
The previous note is here (for a quick recap, in cases needed)
- A channel is created with two ends: one used for sending and other for receiving. While creating a channel, we specify the type of data that will pass through it, when the program runs.
- One thread gets ownership of the end that sends; the other thread gets ownership of the end that receives.
- The channel is always unidirectional, from the sending end to the receiving end. No Request-Response style inter-thread communication is possible, using a single channel.
- Any of the two ends may decide to stop working, independent of the other. No handshaking and Good-Bye is necessary. The thread that holds the sending end, may decide on its own, if and when to stop sending; similarly, for the thread that holds the receving end.
- If one of the ends becomes non-operational, the other end receives an error. This behaviour is captured by the return type of
send()
andreceive()
functions.
The case of many senders (MPSC)
For those who remember the famous *Producer-Cconsumer * problem from the textbooks (for me, a few decades ago 😁 ) - here's a wikipedia page for a quick reference - the mechanism of channels should seem familiar. The thread that sends data into the channel, is the producer thread; the other thread which reads from the same channel, is the consumer thread.
With only two threads, there is one producer and one consumner, commonly referred to as SPSC. What if there are multiple producers, a case of MPSC?
Obviously, each thread (a producer) has to have access to the sending end of the channel. Put even more simply, every thread has to have a sender for itself.
Referring to the code snippet in the previous note, we have:
// ... continuing in current thread, let's call it one_thread
let (sender, receiver) = channel::<Conversation>();
let another_thread = // another thread starts here ...
thread::spawn(move || {
let a_message = Conversation::Hello;
sender.send(a_message).unwrap();
let another_message = Conversation::HowAreYou;
sender.send(another_message).unwrap();
}) // another_thread ends here
// ... continue in one_thread
let receipt_of_hello = receiver.recv().unwrap(); // message 'Hello' is here
let receipt_of_how_are_you = receiver.recv.unwrap(); // message 'HowAreYou' is here
// .. go on and do the rest
Because of the move
, the ownership of sender
is transferred to closure above. another_thread
then runs the closure. This easy arrangement is not an option, when there is yet_another_thread
. The ownership of sender
cannot be transferred to the closure for yet_another_thread
, as well. What do we do?
We simply duplicate the sender
and pass one each to the closures. The above code takes the form:
// ... continuing in current thread, let's call it one_thread
let (sender, receiver) = channel::<Conversation>();
let another_sender = sender.clone();
let another_thread = // another thread starts here ...
thread::spawn(move || {
let a_message = Conversation::Hello;
another_sender.send(a_message).unwrap();
let another_message = Conversation::HowAreYou;
sender.send(another_message).unwrap();
}) // another_thread
let yet_another_sender = sender.clone();
let yet_another_thread = // another thread starts here ...
thread::spawn(move || {
let a_message = Conversation::Hello;
yet_another_sender.send(a_message).unwrap();
let another_message = Conversation::HowAreYou;
sender.send(another_message).unwrap();
}) // yet_another_thread
// ... continue in one_thread
let receipt_of_hello = receiver.recv().unwrap(); // message 'Hello' is here
let receipt_of_how_are_you = receiver.recv.unwrap(); // message 'HowAreYou' is here
// .. go on and do the rest
Obviously, such an arrangement is applicable to more than two producers also. We just need as many duplicates of sender
.
for i in 0..5 { // '5' could have been, say '1000'!
let duplicate_sender = sender.clone();
// another thread starts here; we are
thread::spawn(move || {
let a_message = Conversation::Hello;
duplicate_sender.send(a_message).unwrap();
let another_message = Conversation::HowAreYou;
duplicate_sender.send(another_message).unwrap();
}) // another_thread
}
Multiple producers done, what about single consumer?
The channel is unidirectional. Producers send messages in the direction of the Consumer. The consumer has to keep waiting for the messages to arrive, until the recv()
returns an error indicating that all producers are done with their action (refer to the previous note). Almost intuitively, a consumer is a loop:
while let Ok(message_received) = receiver.recv() {
println!("Message received: {:?}",message_received);
}
A Rust playground is here.
A crucial understanding is about the order in which the consumer receives the messages. The channel is FIFO for every Producer -> Consumer, but not for all Producers -> Consumer. If producer-1 sends 2 messages (say, m-1-1 and m-1-2) and producer-2 sends 2 messages (say, m-2-1- and m-2-2) to the same consumer, then the order in which the messages reach can be:
- m-1-1, m-2-1-, m-2-2, m-1-2, or
- m-1-1, m-1-2, m-2-1, m-2-2 , but
- m-1-1 always reaches before m-1-2 and m-2-1 always reaches before m-2-2-
Takeaways
- Using channels, multiple producers can send messages to a single consumer.
- Each such producer must have its own copy of the sender-end of the channel.
clone
the sender-end for this. - Channels are unidirectional and FIFO in structure; therefore, every producer's messages to the consumer reach in the same order in which they are despatched.
- Multiple producers' messages may reach in any random order, though.
Posted on April 17, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.