Hi all,
I'm trying to build an async queue that puts consumers to sleep,
piggybacking on the implementations of
[JCTools](https://github.com/JCTools/JCTools/) for that.
I've also read the [article on
Seastar](https://www.scylladb.com/2018/02/15/memory-barriers-seastar-linux/)
mentioned here, being kindly pointed out to me on [this
issue](https://github.com/JCTools/JCTools/issues/224) by Francesco N. I
want to ask here as well, because I have a shoddy understand of the JMM
and I want to be certain that I get the implementation right.
Question: I understand that a SPSC queue (like JCTools's
`SpscArrayQueue`) needs an explicit full-fence (MFENCE) for this
use-case (putting consumers to sleep), as mentioned in the article and I
quote:
```
send_messages:
push messages into the queue
full memory barrier
if (consumer_is_sleeping)
wake it up
```
Question: but what about an MPMC queue?
A MPMC implies some sort of CAS-loop-based sync between the consumers
and producers when the queue is empty. So for an MPMC queue (like say
`concurrent.util.ConcurrentLinkedQueue`), on top of the JVM, do you
still need an explicit `Unsafe.fullFence`? And if no, how can I
ascertain that for any given implementation, like JCTools'
`MpmcArrayQueue`, or even more complicated for a MPSC queue like
JCTools' `MpscArrayQueue`?
Also, does a CAS-loop generate a full fence on its own? I have a CAS
loop right after determining that I want the consumer to sleep.
Here's some code:
```java
Future<A> consumer() {
// Step 1 (try pull)
var a = queue.poll();
if (a != null) {
return Future.successful(a);
}
// Step 2 (register promise)
// Does this require any more fencing or whatever?
var ref = await.get();
while (ref == null) {
val p = new Promise<Unit>();
if (await.compareAndSet(null, p))
ref = p;
else
ref = await.get();
}
// Step 3 (try again) - could this be re-ordered with the above step?
a = queue.poll();
if (a != null) {
return Future.successful(a);
}
// Step 4 (sleep, then retry)
//
// N.B. this is another CAS-loop managed by Promise ;-)
return await.future.flatMap(u -> pull());
}
```
And then the consumer:
```java
void push(A a) {
// Step 1 (push)
queue.push(a);
// Step 2 - is this absolutely needed for multi-producer queues?
Unsafe.fullFence()
// Step 3 - are there consumers waiting?
final Promise<Unit> ref = await.get();
// Step 4 — notify sleeping consumers
if (ref != null) {
ref.complete(unit)
}
}
```
My issue is that I'm building a configurable queue (SPSC, MPSC, SPMC,
MPMC, so all combinations are possible and the underlying implementation
selects the right one). And I want to see in what situations I can avoid
those explicit memory fences in case the underlying queue is
multi-producer and/or multi-consumer.
To make matters more complicated I also want to put producers to sleep
via the same mechanism, in case the underlying queue is limited in size
(e.g. a circular array), in which case the roles are reversed, but I'm
assuming it's the same thing.
--
Alexandru Nedelcu
alexn.org
--
You received this message because you are subscribed to the Google Groups
"mechanical-sympathy" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/d/optout.