Managing something that isn't threadsafe


(Nolan) #1

I’m trying to integrate a text-to-speech system that isn’t threadsafe. My crate for TTS calls out to external APIs, and doesn’t implement std::marker::Send. What I essentially need is an application-wide TTS instance that may be used from multiple systems–game-related systems for sending speech descriptions of events, input systems for speaking responses to commands, UI systems for speaking when items get/lose focus or change, etc. The worst that can happen if multiple systems access TTS simultaneously is that messages get pre-empted, and that’s usually desirable anyway.

Because I don’t implement Send, I can’t store the TTS instance as a resource. My next hope was that I could create a TTSSystem that managed the single TTS instance, then opened an event channel that received and processed events–essentially an actor. Unfortunately I can’t even do this:

struct TTSSystem(tts::TTS);

impl TTSSystem {
    fn new() -> Result<TTSSystem, tts::Error> {
        let tts = tts::TTS::default()?;
        Ok(TTSSystem(tts))
    }
}

impl<'s> System<'s> for TTSSystem {
    type SystemData = ();

    fn run(&mut self, _: Self::SystemData) {
    }
}

struct AccessibilityBundle;

impl<'a, 'b> bundle::SystemBundle<'a, 'b> for AccessibilityBundle {
    fn build(self, builder: &mut DispatcherBuilder<'a, 'b>) -> bundle::Result<()> {
        let tts = TTSSystem::new()?;
        builder.add(tts, "tts_system", &[]);
        Ok(())
    }
}

because even that level of abstraction without Send is rejected.

I understand that Rust is trying to prevent me from shooting myself in the foot here, but does Amethyst really not offer an escape hatch for creating a singleton system to guard access to a non-threadsafe API?

Thanks.


(Kae) #2

From Send trait docs:

Types that can be transferred across thread boundaries.

So Send doesn’t mean that the object must support being used from multiple threads concurrently, it only means that the type must support being accessed from other threads than the thread it was created on. If you support this, you can impl Send for your type and happily use it as a specs resource. specs has the same guarantees as the Rust lifetime system in terms of immutable/mutable references, so you don’t need to be afraid of concurrent mutable access.

Does that help?


(Nolan) #3

Hmm, so I can indeed do:

unsafe impl std::marker::Send for TTS { }

in my TTS crate. My concern is that this is a generic interface to multiple TTS interfaces, and I’m not sure how comfortable I feel making that guarantee for everyone, and every speech interface. And in my case, the worst that can probably happen if I’m wrong is that speech pre-empts and cuts itself off. I wouldn’t want someone to rely on this for something more critical, learn that it in fact isn’t thread-safe, then have the entire library misbehave.

So I can do that if I need to I suppose, but think it may not be a great idea. Am I over-thinking this? :slight_smile:


(Kae) #4

Well, Send does not mean that all functions that work with the object have to be thread-safe, so I think it should be OK for you. Even if you impl Send and Sync, all it does is let people move the object to another thread (Send), or borrow a reference to another thread (Sync). But all the usual rules of the Rust borrowing system still apply, so unless immutable references can cause mutation in TTL (like for Rc, where the shared refcount is increased on clone), I think you should be OK.


(Joël Lupien) #5

I’d suggest not using unsafe impl.

Try having a Arc<Mutex> resource instead. It doesn’t work in all cases, but in most it does.