With the patch for RFC 458 close to landing, I'm currently doing thought experiments about Send
and Sync
1, trying to investigate their boundaries etc, and one interaction I don't yet have a lot to say about is types that are Sync
but not Send
. I have concrete, useful examples2 for all other elements of the {!Send, Send} × {!Sync, Sync}
set, and it would be great to complete the table, rather than having to resort to hypothetical/contrived types.
The sort of type I'm looking for is a type T
for which it is not safe to transfer between threads by value, but it is safe to use a &T
in another thread,e.g. (ignoring lifetimes and moves etc.)
let some_t = ...;
// safe
/* thread A */
tx.send(&some_t);
/* thread B */
let reference_to_some_t = rx.recv();
// unsafe
/* thread A */
tx.send(some_t);
/* thread B */
let some_t = rx.recv()`
One sort of API that would exhibit this property is a C API like
// Create some data.
ContextObject* create();
// This can be called on any thread.
void do_something(ContextObject* ctxt);
// This must be called on the same thread as `init`.
void destroy(ContextObject* ctxt);
The key point being that only destroy
has to be called on the same thread as init
, but do_something
is perfectly thread-safe and can be called on any thread, but they both use the same piece of data. Rust bindings to such an API might look like (omitting unsafe {}
blocks for brevity)
/* C FFI ... */
enum ContextObject {}
extern { fn create... /* etc. */ }
struct Wrapper {
object: *mut ContextObject
}
impl Wrapper {
fn new() -> Wrapper {
Wrapper { object: create() }
}
fn do_something(&self) { do_something(self.object) }
}
impl Drop for Wrapper {
fn drop(&mut self) {
destroy(self.object);
}
}
unsafe impl Sync for Wrapper {}
It might then be used like
fn main() {
let object = Wrapper::new();
thread::scoped(|| {
// captured a `&Wrapper` reference
object.do_something();
});
// executing concurrently
object.do_something();
// attempt to drop on another thread is illegal, since
// it requires Wrapper: Send (which it isn't)
// thread::spawn(move || drop(object))
} // destructor called here, on the main thread
The one sticking point with that example is... I don't actually know of any concrete API (C or otherwise) like this. It's easy enough to construct contrived examples (e.g. with thread local storage), but most APIs have thread-safety considerations falling into a category like:
- all functions operating on a given piece of context have to be called on the same thread
- functions can be called on any thread, but only one can execute at a time (i.e. need a mutex around each call)
- some functions are totally thread-safe, but others have property 2
- everything is thread safe
I don't know of any real world APIs that have property 3 with property 1 instead of 2 (i.e. single threaded in a fine-grained way).
Do you?
1To be clear, the definitions of the traits I'm working with here are:
-
T: Send
means that transferring a value of typeT
between threads will not allow data races (or other unsafety) in safe code -
T: Sync
means that transferring a value of type&T
between threads will not allow data races (or other unsafety) in safe code
2
| !Send Send
-------|-----------------
!Sync | Rc<T> Cell<T>
Sync | ??? Arc<T>