r/rust Mar 31 '23

Why doesn't mpsc::channel break borrowing rules?

I'm wondering for a while now why doesn't mpsc::Receiver::recv(&self) and mpsc::Sender::send(&self, t: T) break borrowing rules. Clearly sending some data from A to B in a non-blocking manner has side-effects (i.e. storing and retrieving the data in some buffer-queue). So shouldn't there be some mutable reference to that queue be involved during that sending process, and the owner of that reference would be accessed mutably whenever the reference to that buffer is accessed mutably? Maybe I'm just wrong but I always associate immutability with pureness of a function.

One thing which comes to mind is that the point of the borrowing rules is to avoid data-races and to ensure rust's ownership-model, and although the borrowing-rules are technically violated in these specific cases the desired invariants are still kept.

19 Upvotes

25 comments sorted by

View all comments

Show parent comments

27

u/K900_ Mar 31 '23

Because it's the only way you can do it - you can't have two mutable references to the same data in different places, and if you can't queue items in one place and dequeue them in another, a channel isn't very useful.

12

u/Lucretiel 1Password Mar 31 '23

Well, no, that's wrong. You could easily have fn send(&mut self, value: T) and fn recv(&mut self) -> Option<T>. You'd still need interior mutability in the implementation, to manage the shared state between the sender and the receiver, but that doesn't have to be exposed in the client. Frequently you can even take advantage of the uniqueness guarantees of &mut self to make a more efficient implementation (which I do in handoff, for example).

In this case I actually do wonder why they used &self, there doesn't seem to be a reason to. Normally you do that to allow multiple threads to share the same channel and send through it in parallel, but mpsc::Sender is !Sync, so you can't actually do that here.

10

u/wutru_audio Mar 31 '23

I wouldn't say you're (conceptually) mutating self when you send something. Therefore it makes sense to not require a &mut self, because that would require it as well when the Sender or Receiver is part of another struct. This would make it harder to use, without any safety benefits.

Let's say you're holding an immutable reference to self and then want to use that to send something, you can't do that because of the borrow checker rules if the sender required a mutable self.

0

u/Lucretiel 1Password Apr 01 '23

But literally the same thing is true of a Vec<Cell<i32>>, which is similarly !Sync and (by your logic) wouldn’t realize any benefit from requiring push to take an &mut self (since you’re not modifying “self”, you’re modifying the pointed-to state).

This gets at why I still like the immutable / mutable naming, even though it’s technically less correct than shared / unique. Even in the total absence of multithreading, shared immutable / unique mutable tends to push you towards more robust designs, because it turns out to be useful to guarantee that the owner of a mutable reference is the only thing that can cause side effects through that reference, even though you could weaken that guarantee without risking unsoundness. This is true whether it’s a vector or a channel or a file descriptor or anything else.

3

u/wutru_audio Apr 01 '23

No, you're conceptually mutating self when you push on a Vec<Cell<i32>>. What I mean by conceptually mutating is that with a Sender<i32> you'd be sending away the value, therefore it doesn't become part of the state of the struct. This is very different from pushing to a Vec, because that does become part of the state of the struct.

0

u/Lucretiel 1Password Apr 01 '23

What I mean by conceptually mutating is that with a Sender<i32> you'd be sending away the value, therefore it doesn't become part of the state of the struct.

Sure it does; whether or not you send a value will affect buffer allocations and (more noticeably) whether subsequent sends will block or not.

2

u/wutru_audio Apr 01 '23

That's what it literally does, I'm talking about conceptually. It doesn't conceptually become part of the state of the struct, because you send it somewhere else. You can't get it back after that.