-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: Provide a select! macro #39
Comments
Hi Nikola, |
Thank you for considering it! The problem I have with communicating through channels using enums is that the producer must know the enum variant that it should push. The cancellation is the best example I can come up with. I install a SIGINT handler on main. Once the signal is received, I would like to propagate that termination to multiple independent execution units in my code. Go's Now, if we don't use select, and use enum, I would have to create a separate channel for each independent execution unit. Then, once I receive SIGINT, on each of those channels, I would have to push a correct enum variant. But let's set aside this inconvenience.
But why should the worker know about the termination type? And even worse, what stops the worker from terminating the listener by sending a termination variant? This completely removes information hiding and allows components responsibilities that they should not have in the first place. If you ask me, select is needed so we can compose multiple independent execution units that communicate through their own channels. The caller should choose the frequency, prioritization or how to react to each message. Each producer does not care about the context in which it is invoked. It simply communicates the type it knows. |
Hi Nikola, Thank you for your detailed explanation. I understand your concerns regarding the use of enums and channels in Rust as compared to the select mechanism in Go, especially in the context of implementing a SIGINT handler to terminate multiple independent execution units. In Rust, the issue of a worker accidentally terminating the listener by sending a termination variant can indeed be a concern. However, this issue is not unique to Rust and can also occur in Go if a coroutine mistakenly closes a shared channel. The key in both languages is to design your system with clear responsibilities and encapsulations. In Rust, you can achieve a similar level of encapsulation and safety by using a combination of enums, channels, and encapsulated logic in structs or functions. This way, you can ensure that only specific parts of your code have the ability to send certain messages, like a termination signal. Here's an example in Rust: use kanal::{unbounded, Sender};
use std::thread;
enum Message {
Termination,
WorkerMessage(String),
}
// WorkerSender struct to encapsulate the Sender
struct WorkerSender {
sender: Sender<Message>,
}
impl WorkerSender {
fn new(sender: Sender<Message>) -> WorkerSender {
WorkerSender { sender }
}
fn send(&self, message: String) {
self.sender.send(Message::WorkerMessage(message)).unwrap();
}
}
fn main() {
let (tx, rx) = unbounded();
// Spawn workers using the WorkerSender
for i in 0..5 {
let worker_sender = WorkerSender::new(tx.clone());
thread::spawn(move || {
worker(worker_sender, i);
});
}
// rest of your logic here
}
fn worker(worker_sender: WorkerSender, id: i32) {
let message = format!("Message from worker {}", id);
worker_sender.send(message);
// Note: Worker does not have direct access to send Termination
} Let me know what you think. |
Oh, you are completely right, but your example beautifully demonstrates the need for a fn worker(msg_tx: Sender<String>, cancel_rx: Receiver<()>) {
// do something
tx.send(message);
} When you look at the API of this function, you know the worker produces strings. It may receive a cancellation signal. It is completely independent of the context in which it is invoked. It has an API where it produces strings and may receive cancellation signals. Then, the caller decides to handle multiple things. I will write it here in Go to illustrate it: ch := make(chan string, 1)
workerStop := make(chan struct{}, 1)
go worker(ch, workerStop)
stop := make(chan struct{}, 1)
select {
case <- stop:
// start cleaning up internal state
// decide how to stop workers, for simplicity, just close the channel
close(workerStop)
// handle rest of the cleanup
case <- ch:
// handle message
} As you can see, the worker is completely independent of the context, and the select handles multiple paths of execution. Without select, we would have to create an enum with select variants, then write a wrapper like you did to limit the scope and then use the concurrent worker. So if you want to start a worker in another context, you would have to create a different wrapper with different message types. And for performance argument (although this is probably not a problem unless you are running on embedded), enum takes the size of the largest element. The worst case scenario for the example above is that worker B produces large messages, while worker A produces small messages. Worker B needs only 1 element buffer size while worker A needs 10. Buffer allocation would be Lastly, please feel free to close this issue if you think it is not worth doing |
@nikola-jokic maybe tokio::select + tokio_utils::CancellationToken should do the trick |
Oh for sure, I'm using tokio select macro in my async environments, but I wanted to use this library for my sync environment. |
Select would be very helpful especially when one channel is used to communicate cancellation while another is communicating elements.
However, I did not test closing a channel, but coming from go background, it would be very nice to have a select statement.
The text was updated successfully, but these errors were encountered: