com.softwaremill.jox.Channel Maven / Gradle / Ivy
package com.softwaremill.jox;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Comparator;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static com.softwaremill.jox.CellState.*;
import static com.softwaremill.jox.Segment.findAndMoveForward;
/**
* Channel is a thread-safe data structure which exposes three basic operations:
*
* - {@link Channel#send(Object)}-ing a value to the channel. Values can't be {@code null}.
* - {@link Channel#receive()}-ing a value from the channel
* - closing the channel using {@link Channel#done()} or {@link Channel#error(Throwable)}
*
* There are three channel flavors:
*
* - rendezvous channels, where senders and receivers must meet to exchange values
* - buffered channels, where a given number of sent values might be buffered, before subsequent `send`s block
* - unlimited channels, where an unlimited number of values might be buffered, hence `send` never blocks
*
* The no-argument {@link Channel} constructor creates a rendezvous channel, while a buffered channel can be created
* by providing a positive integer to the constructor. A rendezvous channel behaves like a buffered channel with
* buffer size 0. An unlimited channel can be created using {@link Channel#newUnlimitedChannel()}.
*
* In a rendezvous channel, senders and receivers block, until a matching party arrives (unless one is already waiting).
* Similarly, buffered channels block if the buffer is full (in case of senders), or in case of receivers, if the
* buffer is empty and there are no waiting senders.
*
* All blocking operations behave properly upon interruption.
*
* Channels might be closed, either because no more values will be produced by the source (using
* {@link Channel#done()}), or because there was an error while producing or processing the received values (using
* {@link Channel#error(Throwable)}).
*
* After closing, no more values can be sent to the channel. If the channel is "done", any pending sends will be
* completed normally. If the channel is in an "error" state, pending sends will be interrupted and will return with
* the reason for the closure.
*
* In case the channel is closed, one of the {@link ChannelClosedException}s is thrown. Alternatively, you can call
* the less type-safe, but more exception-safe {@link Channel#sendOrClosed(Object)} and {@link Channel#receiveOrClosed()}
* methods, which do not throw in case the channel is closed, but return one of the {@link ChannelClosed} values.
*
* @param The type of the values processed by the channel.
*/
public final class Channel implements Source, Sink {
/*
Inspired by the "Fast and Scalable Channels in Kotlin Coroutines" paper (https://arxiv.org/abs/2211.04986), and
the Kotlin implementation (https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/channels/BufferedChannel.kt).
Notable differences from the Kotlin implementation:
* we block (virtual) threads, instead of suspend functions
* in Kotlin's channels, the buffer stores both the values (in even indexes), and the state for each cell (in odd
indexes). This would be also possible here, but in two-thread rendezvous tests, this is slightly slower than the
approach below: we transmit the values inside objects representing state (in case of `Continuation`/
`StoredSelectClause`), or directly set them as state (in case of the buffered state). Since `Continuation` is a
channel-specific class (unlike in Kotlin), we have the freedom to add the payload as field there.
* as we don't directly store values in the buffer, we don't need to clear them on interrupt etc. This is done
automatically when the cell's state is set to something else than a Continuation/buffered value.
* instead of the `completedExpandBuffersAndPauseFlag` counter, which is used to make sure that each interrupted
receiver state is observed by `expandBuffer`, it seems that as an alternative, we can maintain an additional
per-segment counter. This "processed" counter is decremented (for buffered channels) when a cell is an interrupted
sender, or if `expandBuffer` completes processing a non-interrupted-sender cell. Only when all 3 counters reach 0
(pointers, processed, interrupted), the segment is removed. That way, segments containing only interrupted senders
are removed, and segments with interrupted receivers are only removed after being processed by `expandBuffer`.
* the close procedure is a bit different - the Kotlin version does this "cooperatively", that is multiple threads
that observe that the channel is closing participate in appropriate state mutations. This doesn't seem to be
necessary in this implementation. Instead, `close()` sets the closed flag and closes the segment chain - which
constraints the number of cells to close. `send()` observes the closed status right away, `receive()` observes
it via the closed segment chain, or via closed cells. Same as in Kotlin, the cells are closed in reverse order.
All eligible cells are attempted to be closed, so it's guaranteed that each operation will observe the closing
appropriately.
Other notes:
* we need the previous pointers in segments to physically remove segments full of cells in the interrupted state.
Segments before such an interrupted segments might still hold awaiting continuations. When physically removing a
segment, we need to update the `next` pointer of the `previous` ("alive") segment. That way the memory usage is
bounded by the number of awaiting threads.
* after a `send`, if we know that R > s, or after a `receive`, when we know that S > r, we can set the `previous`
pointer in the segment to `null`, so that the previous segments can be GCed. Even if there are still ongoing
operations on these (previous) segments, and we'll end up wanting to remove such a segment, subsequent channel
operations won't use them, so the relinking won't be useful.
*/
// immutable state
private final int capacity;
final boolean isRendezvous;
private final boolean isUnlimited;
// mutable state
/**
* The total number of `send` operations ever invoked, and a flag indicating if the channel is closed.
* The flag is shifted by {@link Channel#SENDERS_AND_CLOSED_FLAG_SHIFT} bits.
*
* Each {@link Channel#send} invocation gets a unique cell to process.
*/
@SuppressWarnings("FieldMayBeFinal")
private volatile long sendersAndClosedFlag = 0L;
@SuppressWarnings("FieldMayBeFinal")
private volatile long receivers = 0L;
@SuppressWarnings("FieldMayBeFinal")
private volatile long bufferEnd;
/**
* Segments holding cell states. State can be {@link CellState}, {@link Continuation}, {@link SelectInstance}, or a user-provided buffered value.
*/
@SuppressWarnings("FieldMayBeFinal")
private volatile Segment sendSegment;
@SuppressWarnings("FieldMayBeFinal")
private volatile Segment receiveSegment;
@SuppressWarnings("FieldMayBeFinal")
private volatile Segment bufferEndSegment;
@SuppressWarnings("unused")
private volatile ChannelClosed closedReason;
// var handles
private static final VarHandle SENDERS_AND_CLOSE_FLAG;
private static final VarHandle RECEIVERS;
private static final VarHandle BUFFER_END;
private static final VarHandle SEND_SEGMENT;
private static final VarHandle RECEIVE_SEGMENT;
private static final VarHandle BUFFER_END_SEGMENT;
private static final VarHandle CLOSED_REASON;
static {
try {
MethodHandles.Lookup l = MethodHandles.privateLookupIn(Channel.class, MethodHandles.lookup());
SENDERS_AND_CLOSE_FLAG = l.findVarHandle(Channel.class, "sendersAndClosedFlag", long.class);
RECEIVERS = l.findVarHandle(Channel.class, "receivers", long.class);
BUFFER_END = l.findVarHandle(Channel.class, "bufferEnd", long.class);
SEND_SEGMENT = l.findVarHandle(Channel.class, "sendSegment", Segment.class);
RECEIVE_SEGMENT = l.findVarHandle(Channel.class, "receiveSegment", Segment.class);
BUFFER_END_SEGMENT = l.findVarHandle(Channel.class, "bufferEndSegment", Segment.class);
CLOSED_REASON = l.findVarHandle(Channel.class, "closedReason", ChannelClosed.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
/**
* Creates a rendezvous channel.
*/
public Channel() {
this(0);
}
/**
* Creates a buffered channel (when capacity is positive), or a rendezvous channel if the capacity is 0.
*/
public Channel(int capacity) {
if (capacity < UNLIMITED_CAPACITY) {
throw new IllegalArgumentException("Capacity must be 0 (rendezvous), positive (buffered) or -1 (unlimited channels).");
}
this.capacity = capacity;
isRendezvous = capacity == 0L;
isUnlimited = capacity == UNLIMITED_CAPACITY;
var isRendezvousOrUnlimited = isRendezvous || isUnlimited;
var firstSegment = new Segment(0, null, isRendezvousOrUnlimited ? 2 : 3, isRendezvousOrUnlimited);
sendSegment = firstSegment;
receiveSegment = firstSegment;
// If the capacity is 0 or -1, buffer expansion never happens, so the buffer end segment points to a null segment,
// not the first one. This is also reflected in the pointer counter of firstSegment.
bufferEndSegment = isRendezvousOrUnlimited ? Segment.NULL_SEGMENT : firstSegment;
processInitialBuffer();
bufferEnd = capacity;
}
private void processInitialBuffer() {
// the cells that are initially in the buffer are already processed (expandBuffer won't touch them): we need
// to mark them as processed, so that segment removal works properly for these initial segments; however, the
// buffer might span several segments, so we need to iterate over them. The cells that are initially in the
// buffer will never become interrupted senders.
var currentSegment = bufferEndSegment;
// the number of segments where all cells are processed, or some are processed (last segment of the buffer)
var segmentsToProcess = (int) Math.ceil((double) capacity / Segment.SEGMENT_SIZE);
for (int segmentId = 0; segmentId < segmentsToProcess; segmentId++) {
currentSegment = findAndMoveForward(BUFFER_END_SEGMENT, this, currentSegment, segmentId);
var cellsToProcess = (segmentId == segmentsToProcess - 1) ? (capacity % Segment.SEGMENT_SIZE) : Segment.SEGMENT_SIZE;
if (cellsToProcess == 0) cellsToProcess = Segment.SEGMENT_SIZE; // the last segment is entirely processed
//noinspection DataFlowIssue
currentSegment.setup_markCellsProcessed(cellsToProcess);
}
}
public static Channel newUnlimitedChannel() {
return new Channel<>(UNLIMITED_CAPACITY);
}
private static final int UNLIMITED_CAPACITY = -1;
// *******
// Sending
// *******
@Override
public void send(T value) throws InterruptedException {
var r = sendOrClosed(value);
if (r instanceof ChannelClosed c) {
throw c.toException();
}
}
@Override
public Object sendOrClosed(T value) throws InterruptedException {
return doSend(value, null, null);
}
/**
* @return If {@code select} & {@code selectClause} is {@code null}: {@code null} when the value was sent, or
* {@link ChannelClosed}, when the channel is closed. Otherwise, might also return {@link StoredSelectClause}.
*/
private Object doSend(T value, SelectInstance select, SelectClause> selectClause) throws InterruptedException {
if (value == null) {
throw new NullPointerException();
}
while (true) {
// reading the segment before the counter increment - this is needed to find the required segment later
var segment = sendSegment;
// reserving the next cell
var scf = (long) SENDERS_AND_CLOSE_FLAG.getAndAdd(this, 1L);
var s = getSendersCounter(scf);
// calculating the segment id and the index within the segment
var id = s / Segment.SEGMENT_SIZE;
var i = (int) (s % Segment.SEGMENT_SIZE);
// check if `sendSegment` stores a previous segment, if so move the reference forward
if (segment.getId() != id) {
segment = findAndMoveForward(SEND_SEGMENT, this, segment, id);
if (segment == null) {
// the channel has been closed, `s` points to a segment which doesn't exist
return closedReason;
}
// if we still have another segment, the segment must have been removed
if (segment.getId() != id) {
// skipping all interrupted cells, and trying with a new one
SENDERS_AND_CLOSE_FLAG.compareAndSet(this, s, segment.getId() * Segment.SEGMENT_SIZE);
continue;
}
}
// performing the check only now, as even if the channel is closed, we want to move the send segment
// reference forward, so that segments which become eligible for removal can be GCed (after the channel
// is closed, e.g. when the channel is done and there are some values left to be received)
if (isClosed(scf)) {
return closedReason;
}
var sendResult = updateCellSend(segment, i, s, value, select, selectClause);
if (sendResult == SendResult.BUFFERED) {
// a receiver is coming, or we are in buffer
// similarly as above, not clearing the previous pointer
return null;
} else if (sendResult == SendResult.AWAITED) {
// the thread was suspended and then resumed by a receiver or by buffer expansion
// not clearing the previous pointer, because of the buffering possibility
return null;
} else if (sendResult == SendResult.RESUMED) {
// we resumed a receiver - we can be sure that R > s
segment.cleanPrev();
return null;
} else if (sendResult instanceof StoredSelectClause ss) {
// we stored a select instance - there's no matching receive, not clearing the previous segment
return ss;
} else if (sendResult == SendResult.FAILED) {
// the cell was broken (hence already processed by a receiver) or interrupted (also a receiver
// must have been there); in both cases R > s
segment.cleanPrev();
// trying again with a new cell
} else if (sendResult == SendResult.CLOSED) {
// not cleaning the previous segments - the close procedure might still need it
return closedReason;
} else {
throw new IllegalStateException("Unexpected result: " + sendResult + " in channel: " + this);
}
}
}
/**
* @param segment The segment which stores the cell's state.
* @param i The index within the {@code segment}.
* @param s Global index of the reserved cell.
* @param value The value to send.
* @return One of {@link SendResult}, or {@link StoredSelectClause} if {@code select} is not {@code null}.
*/
private Object updateCellSend(Segment segment, int i, long s, T value, SelectInstance select, SelectClause> selectClause) throws InterruptedException {
while (true) {
var state = segment.getCell(i); // reading the current state of the cell; we'll try to update it atomically
if (state == null) {
// reading the buffer end & receiver's counter if needed
if (!isUnlimited && s >= (isRendezvous ? 0 : bufferEnd) && s >= receivers) {
// cell is empty, and no receiver, not in buffer -> suspend
if (select != null) {
// cell is empty, no receiver, and we are in a select -> store the select instance
// and await externally; the value to send is stored in the selectClause
var storedSelect = new StoredSelectClause(select, segment, i, true, selectClause, value);
if (segment.casCell(i, state, storedSelect)) {
return storedSelect;
}
// else: CAS unsuccessful, repeat
} else {
// storing the value to send as the continuation's payload, so that the receiver can use it
var c = new Continuation(value);
if (segment.casCell(i, null, c)) {
if (c.await(segment, i, isRendezvous) == ChannelClosedMarker.CLOSED) {
return SendResult.CLOSED;
} else {
return SendResult.AWAITED;
}
}
// else: CAS unsuccessful, repeat
}
} else {
// cell is empty, but a receiver is in progress, or in buffer -> elimination
if (segment.casCell(i, null, value)) {
return SendResult.BUFFERED;
}
// else: CAS unsuccessful, repeat
}
} else if (state == IN_BUFFER) {
// cell just became part of the buffer
if (segment.casCell(i, IN_BUFFER, value)) {
return SendResult.BUFFERED;
}
// else: CAS unsuccessful, repeat
} else if (state instanceof Continuation c) {
// a receiver is waiting -> trying to resume
if (c.tryResume(value)) {
segment.setCell(i, DONE);
return SendResult.RESUMED;
} else {
// when cell interrupted -> trying with a new one
// when close in progress -> subsequent cells are already closed, this will be detected in the next iteration
return SendResult.FAILED;
}
} else if (state instanceof StoredSelectClause ss) {
// Setting the payload first, before the memory barrier created by potentially setting `SelectInstance.state`.
// The state is the read in select's main thread. Since we have this send-cell exclusively, no other thread
// will attempt to call `setPayload`.
ss.setPayload(value);
// a select clause is waiting -> trying to resume
if (ss.getSelect().trySelect(ss)) {
segment.setCell(i, DONE);
return SendResult.RESUMED;
} else {
// select unsuccessful -> trying with a new one
return SendResult.FAILED;
}
} else if (state == INTERRUPTED_RECEIVE || state == BROKEN) {
// cell interrupted or poisoned -> trying with a new one
return SendResult.FAILED;
} else if (state == CLOSED) {
return SendResult.CLOSED;
} else {
throw new IllegalStateException("Unexpected state: " + state + " in channel: " + this);
}
}
}
// *********
// Receiving
// *********
@Override
public T receive() throws InterruptedException {
var r = receiveOrClosed();
if (r instanceof ChannelClosed c) {
throw c.toException();
} else {
//noinspection unchecked
return (T) r;
}
}
@Override
public Object receiveOrClosed() throws InterruptedException {
return doReceive(null, null);
}
/**
* @return If {@code select} & {@code selectClause} is {@code null}: the received value, or {@link ChannelClosed},
* when the channel is closed. Otherwise, might also return {@link StoredSelectClause}.
*/
private Object doReceive(SelectInstance select, SelectClause> selectClause) throws InterruptedException {
while (true) {
// reading the segment before the counter increment - this is needed to find the required segment later
var segment = receiveSegment;
// reserving the next cell
var r = (long) RECEIVERS.getAndAdd(this, 1L);
// calculating the segment id and the index within the segment
var id = r / Segment.SEGMENT_SIZE;
var i = (int) (r % Segment.SEGMENT_SIZE);
// check if `receiveSegment` stores a previous segment, if so move the reference forward
if (segment.getId() != id) {
segment = findAndMoveForward(RECEIVE_SEGMENT, this, segment, id);
if (segment == null) {
// the channel has been closed, r points to a segment which doesn't exist
return closedReason;
}
// if we still have another segment, the segment must have been removed
if (segment.getId() != id) {
// skipping all interrupted cells, and trying with a new one
RECEIVERS.compareAndSet(this, r, segment.getId() * Segment.SEGMENT_SIZE);
continue;
}
}
var result = updateCellReceive(segment, i, r, select, selectClause);
if (result == ReceiveResult.CLOSED) {
// not cleaning the previous segments - the close procedure might still need it
return closedReason;
} else {
/*
After `updateCellReceive` completes and the channel isn't closed, we can be sure that S > r, unless
we stored the given select instance:
- if we stored and awaited a continuation, and it was resumed, then a sender must have appeared
- if we marked the cell as broken, then a sender is in progress in that cell
- if a continuation was present, then the sender must have been there
- if the cell was interrupted, that could have been only because of a sender
- if a value was buffered, that's because there was/is a matching sender
The only cases when S <= r are when:
- awaiting on the continuation is interrupted, in which case the exception propagates outside of this method
- we stored the given select instance (in an empty / in-buffer cell)
*/
if (!(result instanceof StoredSelectClause)) {
segment.cleanPrev();
}
if (result != ReceiveResult.FAILED) {
return result;
}
}
}
}
/**
* Invariant maintained by receive + expandBuffer: between R and B the number of cells that are empty / IN_BUFFER should be equal
* to the buffer size. These are the cells that can accept a sender without suspension.
*
* This method might suspend (and be interrupted) only if {@code select} is {@code null}.
*
* @param segment The segment which stores the cell's state.
* @param i The index within the {@code segment}.
* @param r Index of the reserved cell.
* @param select The select instance of which this receive is part of, or {@code null} (along with {@code selectClause}) if this is a direct receive call.
* @return Either a state-result ({@link ReceiveResult}), {@link StoredSelectClause} in case {@code select} is not {@code null}, or the received value.
*/
private Object updateCellReceive(Segment segment, int i, long r, SelectInstance select, SelectClause> selectClause) throws InterruptedException {
while (true) {
var state = segment.getCell(i); // reading the current state of the cell; we'll try to update it atomically
if (state == null || state == IN_BUFFER) {
if (r >= getSendersCounter(sendersAndClosedFlag)) { // reading the sender's counter
if (select != null) {
// cell is empty, no sender, and we are in a select -> store the select instance
// and await externally
var storedSelect = new StoredSelectClause(select, segment, i, false, selectClause, null);
if (segment.casCell(i, state, storedSelect)) {
expandBuffer();
return storedSelect;
}
// else: CAS unsuccessful, repeat
} else {
// cell is empty, and no sender -> suspend
// not using any payload
var c = new Continuation(null);
if (segment.casCell(i, state, c)) {
expandBuffer();
var result = c.await(segment, i, isRendezvous);
if (result == ChannelClosedMarker.CLOSED) {
return ReceiveResult.CLOSED;
} else {
return result;
}
}
// else: CAS unsuccessful, repeat
}
} else {
// sender in progress, receiver changed state first -> restart
if (segment.casCell(i, state, BROKEN)) {
expandBuffer();
return ReceiveResult.FAILED;
}
// else: CAS unsuccessful, repeat
}
} else if (state instanceof Continuation c) {
// resolving a potential race with `expandBuffer`
if (segment.casCell(i, state, RESUMING)) {
// a sender is waiting -> trying to resume
if (c.tryResume(0)) {
segment.setCell(i, DONE);
expandBuffer();
return c.getPayload();
} else {
// when cell interrupted -> trying with a new one
// the state will be set to INTERRUPTED_SEND by the continuation, meanwhile everybody else will observe RESUMING
// when close in progress -> the cell state will be updated to CLOSED, subsequent cells are already closed,
// which will be detected in the next iteration
return ReceiveResult.FAILED;
}
}
// else: CAS unsuccessful, repeat
} else if (state instanceof StoredSelectClause ss) {
// resolving a potential race with `expandBuffer`
if (segment.casCell(i, state, RESUMING)) {
// a send clause is waiting -> trying to resume
if (ss.getSelect().trySelect(ss)) {
segment.setCell(i, DONE);
expandBuffer();
return ss.getPayload();
} else {
// when select fails (another clause is selected, select is interrupted, closed etc.) -> trying with a new one
// the state will be set to INTERRUPTED_SEND by the cleanup, meanwhile everybody else will observe RESUMING
return ReceiveResult.FAILED;
}
}
// else: CAS unsuccessful, repeat
} else if (state instanceof CellState) {
if (state == INTERRUPTED_SEND) {
// cell interrupted -> trying with a new one
return ReceiveResult.FAILED;
} else if (state == RESUMING) {
// expandBuffer() is resuming the sender -> repeat
Thread.onSpinWait();
} else if (state == CLOSED) {
return ReceiveResult.CLOSED;
} else {
throw new IllegalStateException("Unexpected state: " + state + " in channel: " + this);
}
} else { // buffered value
segment.setCell(i, DONE);
expandBuffer();
// an elimination has happened -> finish
return state;
}
}
}
// ****************
// Buffer expansion
// ****************
private void expandBuffer() {
if (isRendezvous || isUnlimited) return;
while (true) {
// reading the segment before the counter increment - this is needed to find the required segment later
var segment = bufferEndSegment;
// reserving the next cell
var b = (long) BUFFER_END.getAndAdd(this, 1L);
// calculating the segment id and the index within the segment
var id = b / Segment.SEGMENT_SIZE;
var i = (int) (b % Segment.SEGMENT_SIZE);
// check if `bufferEndSegment` stores a previous segment, if so move the reference forward
if (segment.getId() != id) {
segment = findAndMoveForward(BUFFER_END_SEGMENT, this, segment, id);
if (segment == null) {
// the channel has been closed, b points to a segment which doesn't exist, nowhere to expand
return;
}
// if we still have another segment, the segment must have been removed; this can only happen if all
// cells have been interrupted (either as a receiver or a sender). If this (r) cell was an interrupted
// receiver, the segment would not be removed (and the cell marked as processed) until buffer expansion
// processes the cell. As we are only processing it now, it must have been an interrupted sender.
if (segment.getId() != id) {
// skipping all interrupted (& removed) cells as an optimization if possible
BUFFER_END.compareAndSet(this, b, segment.getId() * Segment.SEGMENT_SIZE);
// restarting buffer expansion as this cell was an interrupted sender
continue;
}
}
var result = updateCellExpandBuffer(segment, i);
if (result == ExpandBufferResult.DONE) {
// done - notifying the segment
segment.cellProcessed_notInterruptedSender();
return;
} else if (result == ExpandBufferResult.CLOSED) {
segment.cellProcessed_notInterruptedSender();
// continuing to mark other closed cells as processed as well
}
// else: the cell is an interrupted sender - restarting (cell already marked as processed)
}
}
private ExpandBufferResult updateCellExpandBuffer(Segment segment, int i) {
while (true) {
var state = segment.getCell(i); // reading the current state of the cell; we'll try to update it atomically
if (state == null) {
// the cell is empty, a sender is or will be coming - set the cell as "in buffer" to let the sender know in case it's in progress
if (segment.casCell(i, null, IN_BUFFER)) {
return ExpandBufferResult.DONE;
}
// else: CAS unsuccessful, repeat
} else if (state == DONE) {
// sender & receiver have already paired up, another buffer expansion already happened
return ExpandBufferResult.DONE;
} else if (state instanceof Continuation c && c.isSender()) {
if (segment.casCell(i, state, RESUMING)) {
// a sender is waiting -> trying to resume
if (c.tryResume(0)) {
segment.setCell(i, c.getPayload());
return ExpandBufferResult.DONE;
} else {
// when cell interrupted -> trying with a new one
// the state will be set to INTERRUPTED_SEND by the continuation, meanwhile everybody else will observe RESUMING
// when close in progress -> the cell state will be updated to CLOSED, subsequent cells are already closed,
// which will be detected in the next iteration
return ExpandBufferResult.FAILED;
}
}
// else: CAS unsuccessful, repeat
} else if (state instanceof Continuation) {
// must be a receiver continuation - another buffer expansion already happened
return ExpandBufferResult.DONE;
} else if (state instanceof StoredSelectClause ss && ss.isSender()) {
if (segment.casCell(i, state, RESUMING)) {
// a send clause is waiting -> trying to resume
if (ss.getSelect().trySelect(ss)) {
segment.setCell(i, ss.getPayload());
return ExpandBufferResult.DONE;
} else {
// select unsuccessful -> trying with a new one
// the state will be set to INTERRUPTED_SEND by the cleanup, meanwhile everybody else will observe RESUMING
return ExpandBufferResult.FAILED;
}
}
// else: CAS unsuccessful, repeat
} else if (state instanceof StoredSelectClause) {
// must be a receiver clause of the select - another buffer expansion already happened
return ExpandBufferResult.DONE;
} else if (state instanceof CellState) {
if (state == INTERRUPTED_SEND) {
// a sender was interrupted - restart
return ExpandBufferResult.FAILED;
} else if (state == INTERRUPTED_RECEIVE) {
// a receiver continuation must have been here before - another buffer expansion already happened
return ExpandBufferResult.DONE;
} else if (state == BROKEN) {
// the cell is broken, receive() started another buffer expansion
return ExpandBufferResult.DONE;
} else if (state == RESUMING) {
Thread.onSpinWait(); // receive() is resuming the sender -> repeat
} else if (state == CLOSED) {
return ExpandBufferResult.CLOSED;
} else {
throw new IllegalStateException("Unexpected state: " + state + " in channel: " + this);
}
} else {
// buffered value: if the ordering of operations was different, we would put IN_BUFFER in that cell and finish
return ExpandBufferResult.DONE;
}
}
}
// *******
// Closing
// *******
@Override
public void done() {
var r = doneOrClosed();
if (r instanceof ChannelClosed c) {
throw c.toException();
}
}
@Override
public Object doneOrClosed() {
return closeOrClosed(new ChannelDone());
}
@Override
public void error(Throwable reason) {
if (reason == null) {
throw new NullPointerException("Error reason cannot be null");
}
var r = errorOrClosed(reason);
if (r instanceof ChannelClosed c) {
throw c.toException();
}
}
@Override
public Object errorOrClosed(Throwable reason) {
return closeOrClosed(new ChannelError(reason));
}
private Object closeOrClosed(ChannelClosed channelClosed) {
if (!CLOSED_REASON.compareAndSet(this, null, channelClosed)) {
return closedReason; // already closed
}
// after this completes, it's guaranteed than no sender with `s >= lastSender` will proceed with the usual
// sending algorithm, as `send()` will observe that the channel is closed
long scf;
var scfUpdated = false;
do {
var initialScf = sendersAndClosedFlag;
scf = setClosedFlag(initialScf);
scfUpdated = SENDERS_AND_CLOSE_FLAG.compareAndSet(this, initialScf, scf);
} while (!scfUpdated);
var lastSender = getSendersCounter(scf);
// closing the segment chain guarantees that no new segment beyond `lastSegment` will be created
var lastSegment = sendSegment.close();
if (channelClosed instanceof ChannelError) {
// closing all cells, as this is an error
closeCellsUntil(0, lastSegment);
} else {
closeCellsUntil(lastSender, lastSegment);
}
// only for buffered channels
if (capacity > 0) {
// Running `expandBuffer` for all remaining cells in the segments, so that they are marked as processed, and
// segments full of closed/interrupted cells can be removed.
// This is safe, as after closing all cells are either closed, interrupted (sender/receiver), done or broken
// (there are no pending sends, and no new sends will be allowed).
var lastGlobalIndex = (lastSegment.getId() + 1) * Segment.SEGMENT_SIZE - 1;
while (bufferEnd <= lastGlobalIndex) {
expandBuffer();
}
}
return null;
}
private void closeCellsUntil(long lastCellToClose, Segment segment) {
if (segment == null) {
// we've reach the end of the segment chain, previous segments have been completed and discarded
return;
}
var lastCellToCloseSegmentId = lastCellToClose / Segment.SEGMENT_SIZE;
int lastIndexToCloseInSegment;
if (lastCellToCloseSegmentId == segment.getId()) {
lastIndexToCloseInSegment = (int) (lastCellToClose % Segment.SEGMENT_SIZE);
} else if (lastCellToCloseSegmentId < segment.getId()) {
// the last cell to close is in a segment before this one, so we need to close all cells in this segment
lastIndexToCloseInSegment = 0;
} else {
// the last cell to close is in a segment after this one, so all cells are already closed
return;
}
// closing the cells in reverse order - that way, a later receiver won't be paired with a sender, while an
// earlier receiver becomes closed
for (int i = Segment.SEGMENT_SIZE - 1; i >= lastIndexToCloseInSegment; i--) {
updateCellClose(segment, i);
}
closeCellsUntil(lastCellToClose, segment.getPrev());
}
private void updateCellClose(Segment segment, int i) {
while (true) {
var state = segment.getCell(i);
if (state == null || state == IN_BUFFER) {
if (segment.casCell(i, state, CLOSED)) {
// we treat closing a cell same as if it there was an interrupted receiver: only the interrupted
// counter is decremented, while the processed counter is not. To remove segments full of closed
// cells, we call `expandBuffer()` after closing completes, so that cells become processed.
segment.cellInterruptedReceiver();
return;
}
} else if (state instanceof Continuation c) {
// potential race with sender/receiver resuming the continuation - resolved by synchronizing on
// `Continuation.data`: only one thread will successfully change its value from `null`
if (c.tryResume(ChannelClosedMarker.CLOSED)) {
segment.setCell(i, CLOSED);
segment.cellInterruptedReceiver();
return;
} else {
// new state is already set or will be set soon, trying again (there might be a value to discard)
Thread.onSpinWait();
}
} else if (state instanceof StoredSelectClause ss) {
if (ss.getSelect().channelClosed(closedReason)) {
// select state is successfully set to closed; not setting the cell state & updating counters, as
// the cell will be cleaned up, setting an interrupted state (and informing the segment)
return;
} else {
// new state is already set or will be set soon, trying again (there might be a value to discard)
Thread.onSpinWait();
}
} else if (state instanceof CellState) {
if (state == DONE || state == BROKEN) {
// nothing to do - a sender & receiver have already met
return;
} else if (state == INTERRUPTED_RECEIVE || state == INTERRUPTED_SEND) {
// nothing to do - segment counters already decremented or waiting to be decremented
return;
} else if (state == RESUMING) {
Thread.onSpinWait(); // receive() or expandBuffer() are resuming the cell - wait
} else {
throw new IllegalStateException("Unexpected state: " + state + " in channel: " + this);
}
} else {
// buffered value: discarding
if (segment.casCell(i, state, CLOSED)) {
segment.cellInterruptedReceiver();
return;
}
}
}
}
@Override
public ChannelClosed closedForSend() {
return isClosed(sendersAndClosedFlag) ? closedReason : null;
}
@Override
public ChannelClosed closedForReceive() {
if (isClosed(sendersAndClosedFlag)) {
var cr = closedReason; // cannot be null
if (cr instanceof ChannelError) {
return cr;
} else {
// channel is done, checking if there's anything left to receive
return hasValuesToReceive() ? null : cr;
}
} else {
return null;
}
}
private boolean hasValuesToReceive() {
while (true) {
// reading the segment before the counter - this is needed to find the required segment later
var segment = receiveSegment;
// r is the cell which will be used by the next receive
var r = receivers;
var s = getSendersCounter(sendersAndClosedFlag);
if (s <= r) {
// for sure, nothing is buffered / no senders are waiting
return false;
}
// calculating the segment id and the index within the segment
var id = r / Segment.SEGMENT_SIZE;
var i = (int) (r % Segment.SEGMENT_SIZE);
// check if `receiveSegment` stores a previous segment, if so move the reference forward
if (segment.getId() != id) {
segment = findAndMoveForward(RECEIVE_SEGMENT, this, segment, id);
if (segment == null) {
// the channel has been closed, r points to a segment which doesn't exist
return false;
}
// if we still have another segment, the segment must have been removed
if (segment.getId() != id) {
// skipping all interrupted cells, and trying with a new one
RECEIVERS.compareAndSet(this, r, segment.getId() * Segment.SEGMENT_SIZE);
continue;
}
}
// we know that s > r
segment.cleanPrev();
if (hasValueToReceive(segment, i)) {
return true;
} else {
// nothing to receive, we can (try to, if not already done) bump the counter and try again
RECEIVERS.compareAndSet(this, r, r + 1);
}
}
}
private boolean hasValueToReceive(Segment segment, int i) {
while (true) {
var state = segment.getCell(i); // reading the current state of the cell
if (state == null || state == IN_BUFFER) {
// this can only happen if a sender is in progress (we checked before that s > r)
// waiting what the sender is going to do -> repeat
Thread.onSpinWait();
} else if (state instanceof Continuation c) {
// a receiver might have gotten suspended while hasValuesToReceive() is running - then, no value to receive here & the r counter is updated.
return c.isSender();
} else if (state instanceof StoredSelectClause ss) {
return ss.isSender(); // as above
} else if (state instanceof CellState) {
if (state == INTERRUPTED_SEND || state == INTERRUPTED_RECEIVE) {
// cell interrupted -> nothing to receive; in case of an interrupted receiver, the counter is already updated
return false;
} else if (state == RESUMING) {
// receive() or expandBuffer() is resuming the sender -> repeat
Thread.onSpinWait();
} else if (state == CLOSED) {
return false;
} else if (state == DONE || state == BROKEN) {
// a concurrent receiver already finished / poisoned the cell
return false;
} else {
throw new IllegalStateException("Unexpected state: " + state + " in channel: " + this);
}
} else {
// buffered value
return true;
}
}
}
// **************
// Select clauses
// **************
private static final Function