Pauan/rust-signals

Blocking behavior documentation

cfoster0 opened this issue · 7 comments

Would it be possible to add information to the docs about the guarantees rust-signals makes regarding blocking behavior?

Considering whether to use this crate in an audio project, where it's important not to block the real-time thread (by doing things like allocation or waiting for a lock). I see that Mutable uses Mutex and RwLock under the hood, but it's hard to track how that impacts the behavior of the signal sender and receivers as a whole.

Pauan commented

That's a good question. As you noted, it currently uses Mutex and RwLock. It will block under the following conditions:

  • Calling signal, signal_cloned or signal_ref

  • Calling replace, swap, or set

  • Cloning a Mutable

  • Dropping a Mutable

  • When the Mutable changes, all of its Signals will block for a short period of time

In all of the above cases, the lock is only held for a very short amount of time.

In the following conditions the lock can be held for longer (potentially a long time):

  • Serializing a Mutable

  • Debugging a Mutable

  • Calling get, get_cloned, replace_with, set_if, set_neq, lock_ref, or lock_mut

The reason they can take a long time is because they call user-defined code. So the amount of time they take will depend on that user code. In practice they are very fast.

If you know of any efficient lockless data structures which support arbitrary data, I'd love to hear about it. I'm very interested in making futures-signals as fast as it can be.

However, something to keep in mind is that Signals integrate with Futures, and Futures are usually run on a thread pool (which will block as needed). This keeps them off of the main thread, which is possibly good enough for your use case.

Really appreciate the writeup.

Does this mean that polling a Future/Stream of a signal can block a .set() call on its associated Mutable? If so, it might be worthwhile to expose an additional, nonblocking .try_set() method.

Regarding lock-free and wait-free primitives, there are a couple crates out there. What would you need the data structure to do?

Pauan commented

@cfoster0 Yes, that's correct. The Signal will block it only for as long as it takes to Copy/Clone the value, but it is indeed a block.

The lock free data structure must have these requirements:

  • Support any data type (including non-Copy and non-Clone data).

  • Support some sort of swap or replace operation (similar to mem::replace).

  • Support some sort of set operation.

That's enough to support a lock-free channel. In order to support Mutable it needs this additional requirement:

  • Support giving a & reference to the data (even if it can only be accessed within a closure).

Also, keep in mind that creating a Mutable (or calling signal/signal_cloned/signal_ref) always requires a heap allocation, this is unavoidable. And spawning a Future/Stream/Signal also requires a heap allocation. However, polling a Signal after it's been created doesn't do any allocation.

Does replace need to return you the owned value?
Because otherwise dashmap does all of this.
Or am I way off and you mean a replacement for Mutex/RwLock?

Pauan commented

@qm3ster Does replace need to return you the owned value?

Yes it does.

Or am I way off and you mean a replacement for Mutex/RwLock?

Yes, a replacement for Mutex/RwLock. But a HashMap works too, since each Mutable could simply get a unique key (which is the index into the HashMap).

Because otherwise dashmap does all of this.

From looking at the source code, dashmap uses a RwLock<HashMap<K, V, S>> internally, so it won't work. The point of this issue is to avoid locking completely (by using lock-free algorithms which use things like CAS).

dashmap doesn't do that, instead it just creates multiple HashMap (by default 4 times the number of CPUs) and spreads the values between the HashMaps.

I was talking about dashmap v4 which is completely different.
But the new design does preclude returning the owned value.

Pauan commented

@qm3ster Ah, interesting, though as you say it only gives you access to a shared reference.