Random Rust Notes - 1

nsengupta

Nirmalya Sengupta

Posted on April 17, 2023

Random Rust Notes - 1

[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.

Using channels to communicate between threads

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. Here goes:

  • Channels provide mechanism for any two threads to communicate asynchrounously. It is unidirectional in structure and behaviour. Every channel has (at least) a Sender and a Receiver. The names indicate which end of the channel is occupied by which.

  • If we can set up a channel in such a way, that one thread gets hold of the sending end (in other words, is the Sender) and the other thread gets hold of the receiving end (in other words, is the Receiver), then they can converse.

  • Using std::sync::mpsc crate, the pattern to create a channel is:
    let (sender, receiver) = channel::<Conversation>();

  Conversation is an enum. The messages that are sent over the channel, are of type Conversation. Almost every example I have come across on the 'Net, uses i32 or &str, but such an enum is contextually more meaningful (in my view 😁 ). Thus, the name.

  • In keeping with the norms followed in tramission of signals, senders are named as tx and receivers, as rx. Many blogs/dicsussions use these. Doesn't matter, does it? tx == sender and rx == receiver!
  • Two threads are going to converse. Therefore, one thread has to have access to the sender and the other thread, obviously, gets the receiver.

Channels are unidirectional

  • A channel is unidirectional: the messages always go from the sending side to the receiving side. In other words, the thread holding the sender, begins the conversation by sending a message; the thread holding the receiver, takes action on the receipt of the message.
  • A key understanding is that the receiver does not respond to the message through the same channel. These channels are not bi-directional and hence, cannot be used for a Request-Response model.

How do two threads make use of the channel?

  • Create the channel first. Now, we have the sender and the receiver.
  • The act of creation must happen on some thread. After all, we are executing a piece of code, on some existing thread!
    // ... 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
Enter fullscreen mode Exit fullscreen mode
  • The order in which another_thread is sending the messages, is the order in which the receiver is receiving them. This is because the channel is FIFO in structure and behaviour. This is an important observation, because what if there are more than one senders? More about it later.

How do threads know when to stop?

  • An ongoing conversation stops, when one of the participants stops. Therefore, if either the Sender stops or the Receiver stops, the other side must have to know. This is important to understand.
  • Significantly, neither of std::sync::mpsc::{Receiver,Sender} provides any mechanism to stop sending or receiving. But, they are dropped, meaning they cease to exist, perhaps because the scope in which they are defined, comes to an end.
  • We note that the sending side (one_thread above) and the receiving side (another_thread above) are completely disconnected (naturally, one argues, because they are threads). Neither of the two is aware of the continued existence of the other side. Unless the channel through which they are communicating offers help!
  • To help, the channel specifies this: if a sender fails to send - in other words, the act of sending returns with an error - the sender comes to know that the receiver has become non-existent (no point sending any more). Similarly, if the sender goes out existence (viz. being dropped perhaps), the receiver is handed an error (nothing is going to arrive any further). Based on this, the threads can decide how to deal with a disconnection from one another.
  • Because the action of send/receive may return with an error, the retrun type of of these functions is a Result.
  • A call to send() returns a Result<(),SendError>. A call to receive() returns a Result<Conversation,RecvError> . Of course, that Conversation is my example type. It can be any valid type (generically speaking, Result<T,RecvError>).
  • This Result<..> explains the use of unwrap() in the code snippet, above.

Takeaways

  • 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() and recv() functions.
💖 💪 🙅 🚩
nsengupta
Nirmalya Sengupta

Posted on April 17, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related