How do I share a struct containing a phantom pointer among threads?

一曲冷凌霜 提交于 2020-07-08 12:27:25

问题


I have a structure that needs to be generic over a type, yet the type is not actually contained in the structure: it's used in methods of this structure, not in the structure itself. And so, the structure includes a PhantomData member:

pub struct Map<T> {
    filename: String,
    phantom: PhantomData<*const T>,
}

The phantom member is defined as a pointer because the structure does not actually own data of type T. This is per advice in the documentation of std::marker::PhantomData:

Adding a field of type PhantomData<T> indicates that your type owns data of type T. This in turn implies that when your type is dropped, it may drop one or more instances of the type T. This has bearing on the Rust compiler's drop check analysis.

If your struct does not in fact own the data of type T, it is better to use a reference type, like PhantomData<&'a T> (ideally) or PhantomData<*const T> (if no lifetime applies), so as not to indicate ownership.

So the pointer seems to be the right choice here. This, however, causes the structure to no longer be Send nor Sync, because PhantomData is only Send and Sync if its type parameter is, and since pointers are neither, the whole thing isn't either. And so, code like this

// Given a master_map of type Arc<Map<Region>> ...
let map = Arc::clone(&master_map);

thread::spawn(move || {
    map.do_stuff();
});

fails to compile even though no Region values or even pointers are being moved:

error[E0277]: the trait bound `*const Region: std::marker::Send` is not satisfied in `Map<Region>`
  --> src/main.rs:57:9
   |
57 |         thread::spawn(move || {
   |         ^^^^^^^^^^^^^ `*const Region` cannot be sent between threads safely
   |
   = help: within `Map<Region>`, the trait `std::marker::Send` is not implemented for `*const Region`
   = note: required because it appears within the type `std::marker::PhantomData<*const Region>`
   = note: required because it appears within the type `Map<Region>`
   = note: required because of the requirements on the impl of `std::marker::Send` for `std::sync::Arc<Map<Region>>`
   = note: required because it appears within the type `[closure@src/main.rs:57:23: 60:10 map:std::sync::Arc<Map<Region>>]`
   = note: required by `std::thread::spawn`

error[E0277]: the trait bound `*const Region: std::marker::Sync` is not satisfied in `Map<Region>`
  --> src/main.rs:57:9
   |
57 |         thread::spawn(move || {
   |         ^^^^^^^^^^^^^ `*const Region` cannot be shared between threads safely
   |
   = help: within `Map<Region>`, the trait `std::marker::Sync` is not implemented for `*const Region`
   = note: required because it appears within the type `std::marker::PhantomData<*const Region>`
   = note: required because it appears within the type `Map<Region>`
   = note: required because of the requirements on the impl of `std::marker::Send` for `std::sync::Arc<Map<Region>>`
   = note: required because it appears within the type `[closure@src/main.rs:57:23: 60:10 map:std::sync::Arc<Map<Region>>]`
   = note: required by `std::thread::spawn`

Here's a complete test case in the playground that exhibits this issue:

use std::fmt::Debug;
use std::marker::PhantomData;
use std::sync::Arc;
use std::thread;

#[derive(Debug)]
struct Region {
    width: usize,
    height: usize,
    // ... more stuff that would be read from a file
}

#[derive(Debug)]
struct Map<T> {
    filename: String,
    phantom: PhantomData<*const T>,
}

// General Map methods
impl<T> Map<T>
where
    T: Debug,
{
    pub fn new<S>(filename: S) -> Self
    where
        S: Into<String>,
    {
        Map {
            filename: filename.into(),
            phantom: PhantomData,
        }
    }

    pub fn do_stuff(&self) {
        println!("doing stuff {:?}", self);
    }
}

// Methods specific to Map<Region>
impl Map<Region> {
    pub fn get_region(&self) -> Region {
        Region {
            width: 10,
            height: 20,
        }
    }
}

fn main() {
    let master_map = Arc::new(Map::<Region>::new("mapfile"));
    master_map.do_stuff();
    let region = master_map.get_region();
    println!("{:?}", region);

    let join_handle = {
        let map = Arc::clone(&master_map);
        thread::spawn(move || {
            println!("In subthread...");
            map.do_stuff();
        })
    };

    join_handle.join().unwrap();
}

What is the best way to deal with this? This is what I've tried:

Defining the phantom field as PhantomData<T>. A proper value instead of a pointer. This works, but I'm wary of it because I've no idea what effect it has, if any, on Rust compiler's "drop check analysis", as per the docs quoted above.

Defining the phantom field as PhantomData<&'a T>. A reference. This should work, but it forces the structure to take an unneeded lifetime parameter, which propagates through my code. I'd rather not do this.

Forcing the structure to implement Send and Sync. This is what I'm actually doing at the moment:

unsafe impl<T> Sync for Map<T> {}
unsafe impl<T> Send for Map<T> {}

It seems to work, but those unsafe impls are ugly and make me nervous.

To clarify what T is used for: It doesn't matter, really. It may not even be used, just provided as a marker for the type system. E.g. only needed so that Map<T> has a type parameter so different impl blocks can be provided:

impl<T> struct Map<T> {
    // common methods of all Maps
}

impl struct Map<Region> {
    // additional methods available when T is Region
}

impl struct Map<Whatever> {
    // additional methods available when T is Whatever, etc.
}

回答1:


There's another option: PhantomData<fn() -> T>. fn() -> T has the same variance as T and *const T, but unlike *const T, it implements both Send and Sync. It also makes it clear that your struct only ever produces instances of T. (If some methods take T as input, then PhantomData<fn(T) -> T> might be more appropriate).

#[derive(Debug)]
struct Map<T> {
    filename: String,
    phantom: PhantomData<fn() -> T>,
}



回答2:


Zero-sized marker traits

My preferred solution is to use use throwaway structs for this purpose:

#[derive(Debug)]
struct Map<T: ThingMarker> {
    filename: String,
    marker: T,
}

trait ThingMarker: Default {}

#[derive(Debug, Default)]
struct RegionMarker;
impl ThingMarker for RegionMarker {}

// General Map methods
impl<T: ThingMarker> Map<T>
where
    T: Debug,
{
    pub fn new<S>(filename: S) -> Self
    where
        S: Into<String>,
    {
        Map {
            filename: filename.into(),
            marker: Default::default(),
        }
    }
   // ...
}

impl Map<RegionMarker> {
    pub fn get_region(&self) -> Region { /* ... */ }
}

fn main() {
    let master_map = Arc::new(Map::<RegionMarker>::new("mapfile"));
    // ...
}

playground

a structure that needs to be generic over a type, yet the type is not actually contained in the structure: it's used in methods of this structure, not in the structure itself.

My rationale is that you don't actually need to parameterize your struct over the type used in the methods, you just need to parameterize it over some type. This is a prime case for just having your own trait. It's probably even more powerful as you can have associated types or constants on the trait implementation.

Narrow implementation

but those unsafe impls are ugly and make me nervous.

As they should. One simple modification is to create your own wrapper type that narrowly implements those traits:

// Pick a better name for this struct
#[derive(Debug)]
struct X<T>(PhantomData<*const T>);

impl<T> X<T> {
    fn new() -> Self {
        X(PhantomData)
    }
}

unsafe impl<T> Sync for X<T> {}
unsafe impl<T> Send for X<T> {}

This prevents "accidentally" implementing these traits for your type if some other field is not Send or Sync.

playground



来源:https://stackoverflow.com/questions/50200197/how-do-i-share-a-struct-containing-a-phantom-pointer-among-threads

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!