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.
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
orsignal_ref
-
Calling
replace
,swap
, orset
-
Cloning a
Mutable
-
Dropping a
Mutable
-
When the
Mutable
changes, all of itsSignal
s 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
, orlock_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?
@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
orreplace
operation (similar tomem::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
?
@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 HashMap
s.
I was talking about dashmap v4
which is completely different.
But the new design does preclude returning the owned value.