Runtime & concurrency
Reveal answer and reasoning
A successful wait creates one Lease holding the semaphore reference as its release responsibility. DisposeAsync atomically exchanges that reference for null before calling Release. Only the disposer that receives the original nonnull reference owns the release; later disposers see null and do nothing. The private constructor prevents ordinary callers from creating a lease without the acquisition path.
This is a logical ownership protocol, not monitor thread affinity or a fairness guarantee. A common wrong answer releases unconditionally every time DisposeAsync is called, or uses repeated disposal to recover from an underlying semaphore destroyed too early. The shared gate must outlive authorized leases and waiters. Release itself is synchronous here, so returning a completed ValueTask is honest; no artificial delay is needed to justify the IAsyncDisposable interface.
Cancellation added before successful acquisition must not create a lease or release a permit.
What a strong answer covers
- One atomic detachment selects the release owner.
- Repeated disposal sees null and does nothing.
- The shared semaphore has a separate outer lifetime.
Common pitfall
Calling Release on every disposal or treating gate disposal as the lease's responsibility.
Follow-up questions
- Why is the constructor private?
- Why does DisposeAsync not need a fake asynchronous delay?
Self-review
Choose what to revisit. These marks are your own assessment and are separate from lesson completion.