Round-trip Rc to other thread

Looking for refutation of the soundness of this idea:

I take an Rc, created on one thread, assume it does have clones, and I convert it such that it won't automatically drop (either through ManuallyDrop or Rc::into_raw). I then use unsafe to send it to another thread. The other thread never dereferences, clones, or drops the Rc (leak on panic). At some point, I send the handle back to the original thread, where it is converted back to an Rc and used.

Is this sound? Does the soundness depend on what is stored in the Rc?

Seems sound to me.

With ManuallyDrop be careful that things like derived Debug implementations could accidentally create code paths that dereference that Rc on the wrong thread... actually, as long as the target is Sync, dereferencing the Rc from another thread shouldn't be a problem either. Just make sure to do nothing that could inspect or modify the reference count (or the weak reference count).


I remember a discussion somewhere concluding that any value of any type T: !Send can actually be sent to other threads, as long as you do absolutely nothing with that value on the wrong thread other than moving it. (In particular don't drop it there either!)

3 Likes

long-term stability, maintainability, and safety of a program are almost always more important than tiny gains in efficiency that using unsafe may yield

3 Likes

What if T: !Send because it depends on thread-local storage? Then, once T shifts to a new thread, it loses the memory it once depended on.

What do you mean? If you have a value x: T than that value is a bunch of bytes on the stack. Moving them to another thread, then back to the original thread isn't a problem - I don't understand what kind of "thread-local storage" setting you have in mind.

Yes, a T: !Send, if solely comprised of bytes without any access to thread-local-storage (on top of the requirements you already listed), could safely be moved to another thread. I'm just thinking of the very rare counterexample where T is comprised of bytes that are in some way a function of a state that exists in thread-local storage.

The value in T might provide access to whatever you like, be it thread-local storage. It doesn't matter since this "access" comes through some methods or functions that interact with T.

You must not call any API on the value of type T from the wrong thread, just moving around the value is allowed:

so any kind of "access" to anything that a value of type T provides is entirely irrelevant.

This is about as harmless as the operating system writing your entire memory to disk when you suspend the PC, later loading it back up. Also kind-of a different "thread" that moves around your data without accessing it in any other way. (I know, arguably that's on a different level and might be considered to logically be not moving the data at all.)

2 Likes

This would be a weird program, but I'm sure there are real-world use cases that depend on similar patterns: what if T is a simple accumulator that adds a series of values that are posted to thread-local storage. It depends on the values posted to that thread-local storage. Let's say T's state has already been changed several times on thread A. Then, somebody moves T to thread B. Now, the values that get accumulated inside T are a function of an "exterior" thread that may or may not be compatible with the program. This means that the state of T is invalid.

No, that's not how it works. If I own a value x: T (and it's currently not borrowed, so I can move it around), then there absolutely nothing else that can hold a reference to that value or otherwise modify it, at least as far as data is concerned that is directly contained in x, not behind some form of pointer. Anything that's behind a pointer does not actualy move in any way shape or form if I move the value x. No other "function" or whatever else can even find out about the fact that I move the x: T that I own into a stack frame of a different thread.

I didn't find the discussion yet. But here are two (somewhat popular) crates offering a safe abstraction for this pattern

send_wrapper - Rust

fragile - Rust

even if, in this case, my Rc contained a thread-local field, if I never dereference the Rc on a non-origin thread, the thread local behavior would never come into play, right?

Yes, this is a given. I'm just making the case that the value of T (let's say it's a wrapped u64 that is !Send) might depend on singularly the values posted on a single-thread for a given time-period. If I want to accumulate values posted on a single-thread, then accumulating values from another thread would defeat the purpose of the program.

Yeah, you're fine. I'm just being pedantic, lol

1 Like

I understand the point. I don't understand how it could happen, I'm convinced it can't. If I own a "wrapped u64" then either that u64 is directly in the value I own on the stack, but in this case there can't be any accumulating going on that I'm not doing myself; or it's behind a pointer, in which case accumulating could be going on. If I move that value to a different thread without doing anything to it, then

  • if the u64 directly contained in the value, since I don't / mustn't access it, there's no accumulating the other thread going on
  • if the u64 is behing some (shared) pointer, the functions on the thread it came from that did the accumulating can continue doing so; I didn't move the u64 when moving the value that holds a pointer to it

You can't just be super vague about stuff, then assume that multiple things, which could never all happen at the same time, can happen at the same time (i.e. the u64 being both moved (hence not behind a pointer) but also somehow used as an accumulator from the wrong thread without me accessing the value in any way shape or form), and then complain that the outcome in this impossible scenario is undesired.

2 Likes

This is another reason why it is safe in the OPs case to move a !Send type to another thread. In my pedantic counter-example, it doesn't make any assumptions that you might expect from a "well-behaved" or "well-designed" program (the assumptions you're making)

In cases where the inner type does implement Send, another way you could wrap this in a safe API is to provide a separate UniqueRc<T> type that can be converted to/from a uniquely-owned Rc<T>. This type can implement Send as long as T: Send.

I submitted a PR to implement Send for the UniqueRc type in the pin-init crate: Implement Send for UniqueRc by mbrubeck · Pull Request #1 · nbdd0121/pin-init · GitHub

There's a crate for that:

send_wrapper was already linked up-thread, but it addresses a different set of use cases than UniqueRc.

1 Like