Examples of API that are "intricately" single-threaded?

With the patch for RFC 458 close to landing, I'm currently doing thought experiments about Send and Sync1, 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:

  1. all functions operating on a given piece of context have to be called on the same thread
  2. functions can be called on any thread, but only one can execute at a time (i.e. need a mutex around each call)
  3. some functions are totally thread-safe, but others have property 2
  4. 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 type T 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>
1 Like

Here's one from Cocoa:

The NSView class is generally not thread-safe. You should create, destroy, resize, move, and perform other operations on NSView objects only from the main thread of an application. Drawing from secondary threads is thread-safe as long as you bracket drawing calls with calls to lockFocusIfCanDraw and unlockFocus.

1 Like

The trivial example is a struct that is mapped into a specific memory location. But you can't even move those around in the same thread. So I'm not sure those apply here.

Interesting! The necessity for locking it's not quite as clean as I was hoping; but I could model that type as something like:

struct NSView { ... }

impl NSView {
    fn new() -> NSView { ... }
    fn resize(&mut self) { ... }
    fn move(&mut self) { ... }

    fn lock_focus<'a>(&'a self) -> ViewFocus<'a> { ... }
}
impl Drop for NSView { ... }
unsafe impl Sync for NSView {}

The lack of Send means that the &mut self methods can never be called from a thread other than the one that created the NSView (there's no way to get a &mut NSView in another thread), but the &self of lock_focus means that it can be called in another thread, since one can pass a &NSView around. (I don't know how one could enforce that the NSView are only created on the main thread, but that's orthogonal.)

(At least, that's assuming it's legal to do the locking on any thread.)

Thanks @comex.

Ah, @oli_obk, that seems close, but like you, I'm not sure it applies.

Some C API (like OpenGL) allow you to map some kind of buffer in RAM. You have to call a function to map it, and another function to unmap it.

To do this properly with RAII, your wrapper would call the map function and create a Mapping object whose destructor calls the unmap function.

This Mapping object can be used from any thread and can implement Sync, since it's just a handle for a buffer, but if the C API requires you to map and unmap in the same thread (like OpenGL), then you can't Send it.

1 Like

Thanks @tomaka.

@tomaka and I discussed it a bit on IRC, relevant links (for interested readers and future-@huon):

1 Like

The example from RFC 458 that I found persuasive was the so-called RcMut. The idea was that you had a reference counted object that is owned by one "primary" thread, which is the only thread that will manipulate its reference counts. This means they don't need to be atomic. At the same time, you'd like to be able to share references so you process the tree in parallel (in a read-only fashion, or at least without changing its tree structure). You can achieve this by having a variant of Rc that requires an &mut self to adjust the reference count. This can then be considered Sync (and hence sharable) but not Send. (It can't be Send because that would allow an RcMut (or an &mut RcMut) to be transferred to another thread, which would give that thread the ability to manipulate the ref counts). An example where this might be useful is in a web browser, where the DOM is traditionally owned by a single thread. Overall this is very similar to the NSView (or Swing toolkit, etc) examples, but I find it a bit more compelling, because the RcMut doesn't require any extra operations or transformations to be safely used from other threads, it all just works fine.

5 Likes