Skip to content

[API Proposal]: Add a Concurrent/Exclusive synchronization model with in-place transitions and pipeline orchestration #131460

Description

@WangHHB

Background and motivation

This proposal introduces a synchronous Concurrent/Exclusive synchronization model for fine-grained state objects and multi-stage state transitions.

Traditional reader/writer locks classify protected operations according to whether they read or write data. That model is useful when mutability determines whether operations may overlap, but many stateful operations do not fit cleanly into that distinction.

An operation may safely perform validation, preparation, bookkeeping, cache updates, or other non-conflicting state changes alongside similar operations, yet require exclusive permission only for a short commit step. In such cases, the important property is not whether the operation writes, but whether it is compatible with other operations executing at the same time.

The proposed model therefore describes execution permissions rather than data-access categories:

  • Concurrent permission allows execution alongside other Concurrent holders.
  • Exclusive permission requires execution without any other holder.

This distinction becomes concrete when an operation contains multiple permission stages:

Concurrent validation and preparation
    → Exclusive commit
    → Concurrent post-processing

Describing these stages as Read, Write, and Read would be misleading. The Concurrent stages may modify state, while the Exclusive stage is defined by its isolation requirement rather than by the mere presence of writes.

Three properties are central to the proposed model.

Fine-grained synchronization

The primitive is intended to be stored directly alongside independently synchronized state objects, such as players, rooms, orders, cache entries, sessions, or other entities.

Fine-grained synchronization is not only an organizational preference. It allows operations on unrelated entities to proceed independently instead of contending on one application-wide lock.

For this model to remain practical, the lock must have a small per-instance footprint and a low-cost ordinary Concurrent path. Otherwise, the cost of assigning synchronization state to each entity would offset the concurrency gained from doing so.

The design is intended to support both ends of the deployment spectrum:

  • one heavily contended lock protecting a large shared state;
  • many independent locks operating concurrently across fine-grained entities.

Preemptive Exclusive acquisition

Exclusive acquisition is not limited to waiting for the lock to become accidentally idle.

A preemptive Exclusive request enters the contention protocol, prevents new Concurrent operations from entering, and allows existing Concurrent holders to complete and leave.

This behavior is important under sustained Concurrent traffic. Without closing the admission path, an Exclusive operation may repeatedly lose the fully idle interval required to enter, even though each individual Concurrent operation is short.

Non-preemptive Exclusive attempts remain useful when the caller only wants to acquire an already idle lock without closing the Concurrent admission path.

Preemption is therefore an explicit operation-level semantic choice rather than one implicit global fairness policy for the entire lock.

In-place permission transitions

An ordinary Concurrent holder may discover during execution that one stage requires Exclusive permission.

The holder may upgrade within the current synchronization context, perform the isolated stage, and later downgrade directly to Concurrent.

This permits one logical operation to preserve synchronization continuity across multiple permission stages without releasing the lock, exposing an intermediate synchronization gap, and repeating state validation.

Unlike a traditional upgradeable-reader model, upgrade intent does not need to be declared before entering the operation or reserved through one dedicated upgradeable mode. The need for Exclusive permission may instead be determined from state observed during ordinary Concurrent execution.

Together, fine-grained synchronization, preemptive Exclusive acquisition, and in-place permission transitions provide the foundation for the Pipeline model.

The Pipeline applies fine-grained synchronization in two dimensions:

  1. different state entities may be protected by different lock instances;
  2. one operation may be divided into fine-grained permission stages within the same lock context.

Performance is therefore part of the engineering motivation rather than an independent claim.

The synchronization primitive must remain practical when used as one heavily contended lock and when many independent lock instances execute simultaneously. This allows the higher-level Pipeline abstraction to express detailed permission workflows without imposing coordination costs that overwhelm the protected work.

The reference implementation uses a hybrid contention strategy. Ordinary Concurrent acquisition primarily follows an atomic fast path. Contended Exclusive acquisition and upgrade arbitration use Monitor to serialize competing requests and reuse the runtime's mature blocking and wake-up mechanisms.

This avoids requiring every waiting Exclusive or upgrade request to remain in a custom active-wait loop and provides practical queueing behavior under write-heavy contention.

The design does not expose strict FIFO ordering as an API guarantee, but it aims to provide reasonable progress and relative fairness among contending Exclusive and upgrade requests.

The API is presented as one synchronization model with three layers:

  1. Core primitive

    Defines Concurrent and Exclusive acquisition, release, preemption, upgrade, downgrade, and contention semantics.

  2. Scope layer

    Tracks the permission currently held by an operation and provides reliable cleanup across permission transitions and exceptional control flow.

  3. Pipeline layer

    Composes ordered work Segments whose required permission may remain unchanged, be acquired independently, upgrade, downgrade, converge, or conditionally determine subsequent execution.

The Pipeline is not included merely as a convenience wrapper. It demonstrates the purpose of the Concurrent/Exclusive abstraction: permissions are composable states of an execution workflow rather than aliases for read and write locking.

Without the Pipeline, the core primitive may appear to be another reader/writer lock with different terminology. With the Pipeline, the intended model becomes explicit: a business operation is expressed as a sequence of permission requirements and transitions, while the synchronization implementation manages how those stages safely converge.

The three layers are proposed together because they form a complete engineering model, but they may be reviewed, revised, or accepted independently.

The current design is synchronous and non-recursive. It does not guarantee strict FIFO ordering, and an Exclusive permission context must not cross an await.

A working reference implementation, documentation, semantic tests, randomized stress tests, and benchmarks are available at:

https://github.com/WangHHB/ConcurrentExclusiveLock

API Proposal

The following API surface is based on the working reference implementation and is intentionally presented as a complete three-layer synchronization model.

The exact namespace, naming, type forms, and overload set are open to API review.

namespace System.Threading;

public enum ConcurrentExclusiveLockState : byte
{
    Idle = 0,
    Concurrent = 1,
    Exclusive = 2,
}

public readonly struct ConcurrentExclusiveLock
{
    public const int MaxConcurrent = int.MaxValue;

    public static ConcurrentExclusiveLock Create();

    public ConcurrentExclusiveLockState ObservedState { get; }

    public int ObservedContention { get; }

    public int ContextID { get; set; }

    public int EpochID { get; set; }

    public bool SwitchContextID(int newContextID);

    public bool RaiseEpochID(int newEpochID);

    public int AcquireConcurrent(int maxConcurrent = MaxConcurrent);

    public int TryAcquireConcurrent(int maxConcurrent = MaxConcurrent);

    public int TryAcquireConcurrent(int millisecondsTimeout, int maxConcurrent = MaxConcurrent);

    public void ReleaseConcurrent();

    public void AcquireExclusive();

    public bool TryAcquireExclusive(bool preemptConcurrent = true);

    public bool TryAcquireExclusive(int millisecondsTimeout);

    public void ReleaseExclusive();

    public void ConcurrentToExclusive();

    public void ExclusiveToConcurrent();

    public bool TryConcurrentToExclusiveWithSwitchContextID(int newContextID);

    public bool TryConcurrentToExclusiveWithRaiseEpochID(int newEpochID);
}

public struct ConcurrentExclusiveLockScope : IDisposable
{
    public ConcurrentExclusiveLockScope(ConcurrentExclusiveLock locker);

    public ConcurrentExclusiveLockState ObservedState { get; }

    public int ObservedContention { get; }

    public int ContextID { get; set; }

    public int EpochID { get; set; }

    public bool SwitchContextID(int newContextID);

    public bool RaiseEpochID(int newEpochID);

    public int AcquireConcurrent(int maxConcurrent = ConcurrentExclusiveLock.MaxConcurrent);

    public int TryAcquireConcurrent(int maxConcurrent = ConcurrentExclusiveLock.MaxConcurrent);

    public int TryAcquireConcurrent(int millisecondsTimeout, int maxConcurrent = ConcurrentExclusiveLock.MaxConcurrent);

    public void ReleaseConcurrent();

    public void AcquireExclusive();

    public bool TryAcquireExclusive(bool preemptConcurrent = true);

    public bool TryAcquireExclusive(int millisecondsTimeout);

    public void ReleaseExclusive();

    public void ConcurrentToExclusive();

    public void ExclusiveToConcurrent();

    public bool TryConcurrentToExclusiveWithSwitchContextID(int newContextID);

    public bool TryConcurrentToExclusiveWithRaiseEpochID(int newEpochID);

    public void Dispose();
}

public readonly struct ConcurrentExclusiveLockPipeline
{
    public readonly ConcurrentExclusiveLock Locker;

    public ConcurrentExclusiveLockPipeline(ConcurrentExclusiveLock locker);

    public void DoPipeline(params ConcurrentExclusiveLockSegment[] segments);

    public Task DoPipelineAsync(params ConcurrentExclusiveLockSegment[] segments);
}

public enum ConcurrentExclusiveAccessMode : byte
{
    None = 0,
    Concurrent = 1,
    TryConcurrent = 2,
    Exclusive = 3,
    TestExclusive = 4,
    TryExclusive = 5,
    ConvergeConcurrent = 6,
    ConvergeExclusive = 7,
    TryApplyIDConvergeExclusive = 8,
}

public readonly struct ConcurrentExclusiveLockSegment
{
    public enum IDType : byte
    {
        ContextID = 0,
        EpochID = 1,
    }

    public readonly Action Segment;

    public readonly int ContextOrEpochID;

    public readonly IDType IDKind;

    public readonly ConcurrentExclusiveAccessMode Access;

    public static ConcurrentExclusiveLockSegment None(Action segment);

    public static ConcurrentExclusiveLockSegment Concurrent(Action segment);

    public static ConcurrentExclusiveLockSegment TryConcurrent(Action segment);

    public static ConcurrentExclusiveLockSegment Exclusive(Action segment);

    public static ConcurrentExclusiveLockSegment TestExclusive(Action segment);

    public static ConcurrentExclusiveLockSegment TryExclusive(Action segment);

    public static ConcurrentExclusiveLockSegment ConvergeConcurrent(Action segment);

    public static ConcurrentExclusiveLockSegment ConvergeExclusive(Action segment);

    public static ConcurrentExclusiveLockSegment TryApplyIDConvergeExclusive(Action segment, int contextOrEpochID, IDType idType);

    [Obsolete("Pipeline segments must be synchronous. Async segments are not supported.", error: true)]
    public static ConcurrentExclusiveLockSegment None(Func<Task> segment);

    [Obsolete("Pipeline segments must be synchronous. Async segments are not supported.", error: true)]
    public static ConcurrentExclusiveLockSegment Concurrent(Func<Task> segment);

    [Obsolete("Pipeline segments must be synchronous. Async segments are not supported.", error: true)]
    public static ConcurrentExclusiveLockSegment TryConcurrent(Func<Task> segment);

    [Obsolete("Pipeline segments must be synchronous. Async segments are not supported.", error: true)]
    public static ConcurrentExclusiveLockSegment Exclusive(Func<Task> segment);

    [Obsolete("Pipeline segments must be synchronous. Async segments are not supported.", error: true)]
    public static ConcurrentExclusiveLockSegment TestExclusive(Func<Task> segment);

    [Obsolete("Pipeline segments must be synchronous. Async segments are not supported.", error: true)]
    public static ConcurrentExclusiveLockSegment TryExclusive(Func<Task> segment);

    [Obsolete("Pipeline segments must be synchronous. Async segments are not supported.", error: true)]
    public static ConcurrentExclusiveLockSegment ConvergeConcurrent(Func<Task> segment);

    [Obsolete("Pipeline segments must be synchronous. Async segments are not supported.", error: true)]
    public static ConcurrentExclusiveLockSegment ConvergeExclusive(Func<Task> segment);

    [Obsolete("Pipeline segments must be synchronous. Async segments are not supported.", error: true)]
    public static ConcurrentExclusiveLockSegment TryApplyIDConvergeExclusive(Func<Task> segment, int contextOrEpochID, IDType idType);
}

API Usage

The examples below show the same synchronization workflow at two abstraction levels.

Permission workflow orchestration

The Pipeline is the primary usage example because it makes the Concurrent/Exclusive abstraction concrete. Each Segment declares an execution-permission requirement rather than a read/write intent.

private readonly ConcurrentExclusiveLock _locker = ConcurrentExclusiveLock.Create();
private readonly ConcurrentExclusiveLockPipeline _pipeline;

public EntityCoordinator()
{
    _pipeline = new ConcurrentExclusiveLockPipeline(_locker);
}

public void ApplyEpoch(int targetEpoch)
{
    _pipeline.DoPipeline(
        ConcurrentExclusiveLockSegment.Concurrent(() =>
        {
            InspectCurrentState();
            PrepareNonConflictingState();
        }),

        ConcurrentExclusiveLockSegment.TryApplyIDConvergeExclusive(() =>
        {
            ApplyEpochUpdate();
        }, targetEpoch, ConcurrentExclusiveLockSegment.IDType.EpochID),

        ConcurrentExclusiveLockSegment.ConvergeConcurrent(() =>
        {
            PublishCurrentSnapshot();
        }),

        ConcurrentExclusiveLockSegment.None(() =>
        {
            NotifyOtherSystems();
        }));
}

This workflow means:

  1. inspect and prepare state while compatible operations may execute concurrently;
  2. apply targetEpoch and converge to Exclusive only when this operation becomes the unique updater for that epoch;
  3. continue from Exclusive by downgrading to Concurrent, or reacquire Concurrent when another operation has already applied the epoch;
  4. release the synchronization permission before notifying external systems.

The first stage is not required to be read-only. It may perform non-conflicting state changes. The Exclusive stage is not defined merely by writing data; it is defined by the requirement that the epoch update have one unique committer.

The final None Segment also expresses an intentional synchronization boundary. External notification executes only after the permission held by the preceding Segment has been released.

Equivalent explicit permission management

The same operation can be expressed directly through the Scope layer:

public void ApplyEpochWithoutPipeline(int targetEpoch)
{
    using(ConcurrentExclusiveLockScope scope = new ConcurrentExclusiveLockScope(_locker))
    {
        scope.AcquireConcurrent();

        InspectCurrentState();
        PrepareNonConflictingState();

        if (scope.TryConcurrentToExclusiveWithRaiseEpochID(targetEpoch))
        {
            ApplyEpochUpdate();
            scope.ExclusiveToConcurrent();
        }
        else
        {
            // A failed upgrade releases the original Concurrent permission.
            scope.AcquireConcurrent();
        }

        PublishCurrentSnapshot();

        scope.ReleaseConcurrent();

        NotifyOtherSystems();
    }
}

The Scope records the permission currently held by this operation. Its final permission may therefore differ from its initial permission because of a successful upgrade, failed upgrade, or downgrade.

Dispose() releases any permission still held by the Scope on exceptional or early-return paths. Explicit release remains useful when business code needs to establish a synchronization boundary before the Scope itself ends.

The direct form provides precise control and is appropriate for low-level code. The Pipeline form represents the same behavior declaratively and centralizes the following decisions:

  • whether the current permission can be retained;
  • whether Concurrent should be upgraded to Exclusive;
  • whether Exclusive should be downgraded to Concurrent;
  • whether a permission must be released and reacquired;
  • whether a conditional Segment should execute;
  • whether later work should continue from the None state.

Independent and converged Segments

Independent Segments deliberately establish a new permission boundary:

_pipeline.DoPipeline(
    ConcurrentExclusiveLockSegment.Concurrent(() =>
    {
        PerformFirstConcurrentOperation();
    }),

    ConcurrentExclusiveLockSegment.Concurrent(() =>
    {
        PerformSecondConcurrentOperation();
    }));

Although both Segments require Concurrent permission, the first permission is released and the second is acquired independently. This gives pending contenders an opportunity to enter between the two operations.

Converged Segments instead request continuity:

_pipeline.DoPipeline(
    ConcurrentExclusiveLockSegment.Concurrent(() =>
    {
        InspectState();
    }),

    ConcurrentExclusiveLockSegment.ConvergeExclusive(() =>
    {
        CommitState();
    }),

    ConcurrentExclusiveLockSegment.ConvergeConcurrent(() =>
    {
        PublishState();
    }));

Here, the Pipeline attempts to preserve one continuous synchronization context:

Concurrent
    → in-place upgrade to Exclusive
    → in-place downgrade to Concurrent

This distinction between independent and converged Segments is part of the synchronization semantics, not merely a code-organization convenience.

Synchronous execution boundary

Pipeline Segments are synchronous Action delegates. A Segment must finish before the Pipeline releases or transitions its permission.

An Exclusive permission context must therefore not cross an await. Async lambdas supplied directly as Segments are intentionally rejected by disabled Func<Task> overloads.

DoPipelineAsync executes one complete synchronous Pipeline on a thread-pool thread. It does not make individual Segments asynchronous or awaitable.

Alternative Designs

ReaderWriterLockSlim

ReaderWriterLockSlim is the closest existing BCL alternative. It provides read, write, and upgradeable-read modes and remains appropriate when operations naturally map to read/write access and upgrade intent is known before entering the protected region.

The proposed model differs in three central areas.

Fine-grained deployment

ConcurrentExclusiveLock is intended to be stored directly alongside independently synchronized state objects, such as players, rooms, orders, cache entries, sessions, or other entities.

This deployment model requires more than acceptable uncontended performance. It also requires:

  • a small per-instance footprint;
  • a low-cost ordinary Concurrent path;
  • practical behavior when one lock is heavily contended;
  • scalable aggregate behavior when many independent locks operate simultaneously.

A synchronization primitive that is acceptable as one application-wide lock may still be unsuitable when every state entity carries its own lock instance.

The proposed design therefore treats both single-lock and multi-lock performance as engineering requirements. This provides the foundation for using the Pipeline as a fine-grained permission workflow over independently protected entities.

Preemptive Exclusive acquisition

The proposed API explicitly distinguishes between preemptive and non-preemptive Exclusive acquisition.

A preemptive Exclusive request enters the contention protocol, prevents new Concurrent operations from entering, and allows existing Concurrent holders to drain. This gives the Exclusive operation a defined path toward isolation under sustained Concurrent traffic.

A non-preemptive attempt succeeds only when the lock is already in a suitable state. It does not close admission to subsequent Concurrent operations.

This distinction allows the caller to decide whether a particular Exclusive operation should affect Concurrent admission. It is therefore an operation-level semantic choice rather than one fixed reader-preference or writer-preference policy for the entire lock.

Under continuous Concurrent traffic, this capability is important because waiting for an accidental fully idle interval may otherwise delay an Exclusive operation indefinitely in practice.

In-place upgrade and downgrade

With ReaderWriterLockSlim, upgradeability is represented by a separately acquired upgradeable-read mode. Only one operation may hold that mode at a time, and an ordinary reader cannot later decide to upgrade directly.

In the proposed model, every ordinary Concurrent holder begins in the same permission mode. The need for Exclusive permission may be discovered from state observed during normal Concurrent execution.

The holder may then request an in-place transition to Exclusive. Competing upgrade requests are resolved by the synchronization protocol rather than by requiring one operation to reserve a unique upgradeable position before entering.

After completing the isolated stage, the successful holder may downgrade directly to Concurrent and continue within the same synchronization context.

This permits workflows such as:

Concurrent validation and preparation
    → preemptive or conditional Exclusive commit
    → Concurrent publication

The operation does not need to release its synchronization context, expose an intermediate gap, reacquire another mode, and repeat state validation between stages.

Different permission semantics

The proposed modes are not aliases for read and write.

A Concurrent operation may perform non-conflicting state changes. An Exclusive operation requires isolation because it must execute alone, not merely because it writes data.

This distinction becomes especially important in the Pipeline, where each Segment describes an execution-permission requirement and its relationship to the permission held by the preceding Segment.

For these reasons, the proposal is not intended as a universal replacement for ReaderWriterLockSlim. It addresses a different synchronization model centered on fine-grained state objects, explicit Exclusive preemption, and continuous permission transitions.

Monitor or lock

The complete business operation could be protected by one Monitor or lock statement.

This remains the simplest and often the best choice when the protected operation is small, when most work inherently requires isolation, or when concurrent execution provides no meaningful benefit.

However, protecting an entire multi-stage operation with one exclusive lock reduces:

Concurrent preparation
    → Exclusive commit
    → Concurrent post-processing

to one continuously exclusive region.

That prevents compatible preparation and post-processing stages from executing concurrently, and it encourages synchronization to be applied at a coarser level than the underlying state model requires.

The proposed primitive does not reject Monitor. The reference implementation deliberately uses a hybrid strategy:

  • the ordinary Concurrent path primarily uses atomic state transitions;
  • contended Exclusive acquisition and upgrade arbitration use Monitor.

This reuses the runtime's mature blocking, wake-up, and waiter-coordination behavior for the path where threads may need to sleep, while avoiding Monitor acquisition on the common Concurrent fast path.

Using Monitor for contended writers also provides practical queueing behavior and reasonable relative fairness among competing Exclusive and upgrade requests.

Strict FIFO ordering is not guaranteed by the proposal. The exact scheduling order remains an implementation characteristic rather than a permanent API contract.

A fixed reader-preference or writer-preference policy

The lock could apply one global admission policy whenever an Exclusive request exists.

A permanently reader-preferred policy maximizes admission of Concurrent operations but may make progress toward Exclusive difficult under sustained Concurrent traffic.

A permanently writer-preferred policy gives pending writers priority but may unnecessarily delay new Concurrent operations even when a caller only wanted a non-disruptive attempt.

The proposed API instead makes preemption explicit at the operation level.

A caller may request preemptive Exclusive acquisition when progress toward isolation is required, or use a non-preemptive attempt when it does not want to close the Concurrent admission path.

This makes the synchronization policy part of the business operation rather than a single irreversible setting attached to the lock instance.

Composing existing synchronization primitives

A similar protocol can be constructed from atomic counters, Monitor, SemaphoreSlim, events, condition variables, or other lower-level mechanisms.

Doing so requires each implementation to independently define and validate:

  • admission of new Concurrent operations;
  • detection and handling of a pending preemptive Exclusive request;
  • draining of existing Concurrent holders;
  • arbitration between ordinary Exclusive requests and upgrade requests;
  • arbitration between multiple Concurrent-to-Exclusive upgrades;
  • the state retained after a successful upgrade;
  • the state retained or released after a failed upgrade;
  • continuity during Exclusive-to-Concurrent downgrade;
  • timeout behavior;
  • waiter blocking and wake-up behavior;
  • cleanup after exceptions and early returns;
  • memory visibility and ordering of state transitions;
  • progress and relative fairness under multiple writers.

These rules are the synchronization protocol itself rather than incidental application code.

Reimplementing them for each project or call site makes correctness, performance, and failure behavior difficult to review and test consistently.

A reusable BCL primitive would provide one documented contract and one implementation that can be validated across supported runtimes and architectures.

A fully custom writer wait queue

The implementation could maintain its own explicit FIFO queue for Exclusive and upgrade waiters instead of using Monitor for contended arbitration.

This could provide tighter control over queue ordering, but it would also require the synchronization primitive to implement and maintain:

  • waiter registration and removal;
  • blocking and wake-up;
  • timeout removal;
  • abandoned or interrupted waits;
  • handoff between waiters;
  • queue cleanup;
  • interaction between upgrades and ordinary Exclusive requests;
  • platform-specific waiting behavior.

It would also make strict ordering part of either the API contract or a highly observable implementation detail.

The current hybrid design instead delegates blocking and wake-up to Monitor, while retaining control over the Concurrent/Exclusive state protocol.

This provides a smaller implementation and practical relative fairness without promising strict FIFO behavior.

Exposing only the core primitive

A smaller proposal could expose only ConcurrentExclusiveLock and leave Scope and Pipeline behavior to application code.

This would reduce the initial API surface, but it would omit two important parts of the engineering model.

The Scope layer tracks the permission actually held after:

  • successful acquisition;
  • successful upgrade;
  • failed upgrade;
  • downgrade;
  • explicit release;
  • exceptional control flow.

The Pipeline layer expresses how permissions are composed across ordered business stages. It centralizes decisions about whether a permission should be:

  • retained;
  • released;
  • acquired independently;
  • upgraded;
  • downgraded;
  • converged from the current state;
  • conditionally obtained for a unique operation.

Without the Pipeline, Concurrent and Exclusive may appear to be alternative names for reader and writer modes.

With the Pipeline, their intended meaning becomes concrete: they are composable execution permissions used to describe a multi-stage workflow.

The three layers may still be reviewed, revised, or accepted independently, but presenting them together exposes the complete synchronization model.

Naming the modes Read and Write

The modes could use the familiar names Read and Write.

This was rejected because mutability is not the compatibility rule of the proposed model.

A Concurrent Segment may update bookkeeping, prepare local state, update a non-conflicting portion of an entity, or perform another operation that is compatible with other Concurrent holders.

An Exclusive Segment may read, write, validate, commit, or perform any operation whose defining requirement is that no other holder execute at the same time.

Using Read and Write would encourage callers to infer restrictions that the protocol does not impose. It would also obscure the Pipeline model, whose Segments describe execution compatibility and permission transitions rather than data-access categories.

Releasing and reacquiring instead of transitioning in place

A caller could release Concurrent permission and then independently acquire Exclusive permission.

After the Exclusive stage, it could release Exclusive and independently reacquire Concurrent.

This creates synchronization gaps between stages.

During such a gap, another operation may change the state that justified the transition, requiring the caller to repeat validation or introduce additional version checks and retry logic.

An in-place upgrade provides a defined transition from the observed Concurrent state toward Exclusive isolation. An in-place downgrade preserves continuity between the isolated stage and subsequent Concurrent work.

Independent release and reacquisition remain available when a caller intentionally wants to establish a new synchronization boundary. They are not an equivalent replacement for continuous permission transitions.

Allowing asynchronous Pipeline Segments

Pipeline Segments could accept Func<Task> and permit a permission context to cross await.

This would require a substantially different synchronization contract covering:

  • suspended execution while permission remains held;
  • continuation ownership;
  • cancellation;
  • execution-context changes;
  • potentially long-lived Exclusive permissions;
  • failure after the original synchronous call has returned.

It would also make the lifetime of a Segment independent of the delegate invocation during which the Pipeline established its permission.

The current proposal therefore keeps individual Segments synchronous.

An asynchronous caller may execute a complete synchronous Pipeline on another thread, but a Segment itself does not suspend while retaining Concurrent or Exclusive permission.

A true asynchronous Concurrent/Exclusive primitive could be considered separately with an API and ownership model designed specifically for asynchronous execution.

Risks

Permission-model misuse

Concurrent permission means that an operation may execute alongside other Concurrent holders. It does not mean that arbitrary shared-state mutation is automatically safe.

Callers remain responsible for ensuring that operations classified as Concurrent are mutually compatible. Incorrect classification may introduce races or violate application invariants.

Documentation and examples must therefore describe Concurrent as an execution-compatibility contract rather than as unrestricted concurrent mutation.

Permission-transition semantics

Some operations may change the permission held by the caller.

In particular, successful upgrade, failed conditional upgrade, downgrade, explicit release, and Pipeline convergence may all produce different final permission states.

Each transition API must clearly document:

  • the required initial permission;
  • the permission held after success;
  • the permission held after failure;
  • whether synchronization continuity is preserved;
  • whether the operation may block.

The Scope and Pipeline layers reduce the amount of permission-state bookkeeping required in application code, but their behavior must remain explicit and predictable.

Upgrade contention

Unlike a traditional single upgradeable-reader mode, ordinary Concurrent holders may discover during execution that Exclusive permission is required.

When multiple holders attempt to upgrade, the implementation must prevent deadlock and define which request continues toward Exclusive.

The current design serializes upgrade competition and ensures that unsuccessful contenders do not indefinitely retain the Concurrent permission required by the successful contender to make progress.

These semantics require extensive stress testing under mixed Concurrent, Exclusive, and upgrade workloads.

Preemptive Exclusive behavior

A preemptive Exclusive request prevents new Concurrent operations from entering while existing holders drain.

This provides a defined path toward Exclusive progress under sustained Concurrent traffic, but temporarily reduces Concurrent throughput.

Preemption is therefore caller-selected rather than an unconditional lock-wide policy. Non-preemptive acquisition remains available when the caller does not want to close the Concurrent admission path.

Strict FIFO ordering is not guaranteed. Contended Exclusive and upgrade operations use runtime blocking and wake-up mechanisms to provide practical progress and relative fairness without exposing queue order as an API contract.

Scope ownership

ConcurrentExclusiveLockScope is a mutable value type that tracks the permission currently held by one logical operation.

The value-type design avoids an additional object allocation and is useful in allocation-sensitive environments, including Unity-oriented workloads and applications that maintain large numbers of fine-grained synchronization operations.

As with other mutable value types, callers should not copy a live Scope or pass it around by value after acquisition. It is intended to remain a local lifetime object:

using(ConcurrentExclusiveLockScope scope = new ConcurrentExclusiveLockScope(locker))
{
    scope.AcquireConcurrent();
    // Work and permission transitions.
}

API review may consider another ownership representation, but any alternative should preserve practical allocation behavior and broad runtime usability.

Synchronous execution and reentrancy

The proposed primitive is synchronous and non-recursive.

A permission context must not cross an await, and Pipeline Segments accept synchronous Action delegates only.

Code executed while permission is held should avoid unknown callbacks, re-entry into the same lock, long-running blocking operations, and external work whose synchronization behavior is not controlled by the caller.

The None Pipeline Segment provides an explicit boundary for work that should execute after releasing synchronization permission.

Fine-grained locking and multiple-lock ordering

Fine-grained synchronization reduces contention between unrelated entities, but an operation that acquires multiple independent lock instances can still deadlock if callers use inconsistent acquisition orders.

The proposal coordinates permission transitions within one lock instance. Applications that combine multiple locks must establish their own ordering or higher-level coordination strategy.

Performance variability

The design targets both one heavily contended lock and many independently active fine-grained locks.

The reference implementation uses an atomic-oriented ordinary Concurrent path and a serialized blocking path for contended Exclusive and upgrade operations. The Pipeline adds delegate, Segment, and params-array overhead beyond the core primitive.

Actual performance depends on workload, runtime, processor architecture, contention ratio, critical-section duration, and Pipeline usage.

Benchmarks are therefore supporting evidence for the intended engineering trade-offs rather than a universal guarantee that the proposed primitive will outperform every alternative in every workload.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions