Class AsyncExchange<T>
- Namespace
- CryptoHives.Foundation.Threading.Async.Pooled
- Assembly
- CryptoHives.Foundation.Threading.dll
A two-party rendezvous that lets two tasks atomically swap values without allocations,
equivalent to Java's Exchanger<V> for async code.
public sealed class AsyncExchange<T> : IResettable
Type Parameters
TThe type of value exchanged.
- Inheritance
-
AsyncExchange<T>
- Implements
- Inherited Members
Remarks
Each caller supplies a value and receives the counterpart's value. The first caller to arrive suspends until a second caller arrives; the second caller completes the first immediately and both return synchronously with each other's value.
Exactly one "slot" is maintained at a time. If multiple callers arrive while a waiter is already pending, each new caller pairs with and wakes the pending waiter; the next caller then becomes the new pending waiter.
private readonly AsyncExchange<int> _exchange = new();
// Task A
int fromB = await _exchange.ExchangeAsync(42, ct);
// Task B (concurrent with A)
int fromA = await _exchange.ExchangeAsync(99, ct);
// fromA == 42, fromB == 99
Optional timeout and cancellation token parameters on ExchangeAsync(T, TimeSpan, CancellationToken). A timeout of Zero turns the call into a try-exchange: it pairs with a counterpart that is already waiting and throws TimeoutException otherwise, without ever occupying the slot. TryExchange(T, out T) answers the same question synchronously, without the exception or the ValueTask<TResult> allocation a failed zero-timeout attempt would otherwise need.
This implementation uses ValueTask<TResult> for waiters and provides allocation-free exchange by reusing a per-instance LocalManualResetValueTaskSource<T> for the common single-waiter case and falling back to a pooled IGetPooledManualResetValueTaskSource<T> for concurrent contention.
Important Usage Note: awaiting a ValueTask<TResult> has its own caveats, as
it is a struct that can only be awaited or converted with AsTask() ONE single time. Additional
attempts to await, to convert, or even to read IsCompleted after the first await throw an
InvalidOperationException.
The IResettable interface is implemented to allow resetting the state of the instance for reuse by an ObjectPool<T> using the DefaultObjectPool<T> implementation.
Constructors
AsyncExchange(bool, IGetPooledManualResetValueTaskSource<T>?)
Constructs a new AsyncExchange<T> instance.
public AsyncExchange(bool runContinuationAsynchronously = true, IGetPooledManualResetValueTaskSource<T>? pool = null)
Parameters
runContinuationAsynchronouslyboolWhen true (default), the waiting party's continuation is forced to the thread pool when the exchange completes, preventing the arriving party's thread from being hijacked.
poolIGetPooledManualResetValueTaskSource<T>Custom pool for waiter instances; uses a shared per-
Tpool if omitted.
Properties
HasWaiter
Gets whether a task is currently waiting for an exchange partner.
public bool HasWaiter { get; }
Property Value
RunContinuationAsynchronously
Gets or sets whether the waiting party's continuation is forced to run asynchronously when the exchange is completed by the arriving party.
public bool RunContinuationAsynchronously { get; set; }
Property Value
Methods
ExchangeAsync(T, CancellationToken)
Exchanges value with a counterpart task and returns that task's value.
If no counterpart is waiting, suspends until one arrives or cancellationToken fires.
If a counterpart is already waiting, completes both tasks immediately without suspending.
public ValueTask<T> ExchangeAsync(T value, CancellationToken cancellationToken = default)
Parameters
valueTThe value to offer to the counterpart.
cancellationTokenCancellationTokenToken to cancel the wait.
Returns
- ValueTask<T>
A ValueTask<TResult> that completes with the counterpart's offered value.
Exceptions
- OperationCanceledException
Thrown when
cancellationTokenis cancelled while waiting.
ExchangeAsync(T, TimeSpan, CancellationToken)
Exchanges value with a counterpart task and returns that task's value,
or fails once timeout has elapsed.
public ValueTask<T> ExchangeAsync(T value, TimeSpan timeout, CancellationToken cancellationToken = default)
Parameters
valueTThe value to offer to the counterpart.
timeoutTimeSpanThe maximum time to wait for a counterpart. Use InfiniteTimeSpan to wait indefinitely, or Zero to pair only with a counterpart that is already waiting - TryExchange(T, out T) does the same synchronously, without throwing on a miss.
cancellationTokenCancellationTokenToken to cancel the wait.
Returns
- ValueTask<T>
A ValueTask<TResult> that completes with the counterpart's offered value.
Remarks
If a counterpart is already waiting the exchange completes immediately without allocating any timeout infrastructure. A timer is allocated only when this caller has to wait and a finite positive timeout is requested; it is disposed automatically when the returned ValueTask<TResult> is awaited.
Exceptions
- ArgumentOutOfRangeException
Thrown when
timeoutis negative and not equal to InfiniteTimeSpan.- TimeoutException
Thrown when the timeout elapses before a counterpart arrives.
- OperationCanceledException
Thrown when
cancellationTokenis cancelled while waiting.
TryExchange(T, out T)
Attempts to exchange value immediately, without waiting.
public bool TryExchange(T value, out T result)
Parameters
valueTThe value to offer to the counterpart.
resultTThe counterpart's value, if this method returns true. Undefined if this method returns false.
Returns
- bool
true if a counterpart was already waiting and the exchange completed; false if nobody was waiting.
Remarks
Synchronous and non-throwing by design: unlike ExchangeAsync(T, TimeSpan, CancellationToken) with a zero timeout, a failed attempt here never allocates an exception or a faulted ValueTask<TResult> - there is nothing to await in the first place, since this either pairs immediately or doesn't. A failed attempt never occupies the slot, exactly like the zero-timeout try-exchange.
TryReset()
Reset the object to a neutral state, semantically similar to when the object was first constructed.
public bool TryReset()
Returns
Remarks
In general, this method is not expected to be thread-safe.