1+ // Copyright (c) Microsoft Corporation.
2+ // Licensed under the MIT license.
3+
4+ using System ;
5+ using System . Threading ;
6+
7+ namespace Garnet . common
8+ {
9+ /// <summary>
10+ /// Reader/writer lock backed by semaphores.
11+ /// Supports a single writer or multiple concurrent readers.
12+ /// </summary>
13+ public sealed class ReaderWriterLock : IDisposable
14+ {
15+ enum LockOperation
16+ {
17+ Reader ,
18+ Writer
19+ }
20+
21+ #if NET9_0_OR_GREATER
22+ readonly Lock mutex ;
23+ #else
24+ readonly object mutex ;
25+ #endif
26+ readonly SemaphoreSlim readerSemaphore ;
27+ readonly SemaphoreSlim writerSemaphore ;
28+
29+ // -1: writer holds the lock, 0: free, >0: number of active readers.
30+ int lockHeld ;
31+ int waitingReaders ;
32+ int waitingWriters ;
33+ int grantedReaders ;
34+ int grantedWriters ;
35+ bool disposed ;
36+
37+ public ReaderWriterLock ( )
38+ {
39+ mutex = new ( ) ;
40+ readerSemaphore = new ( 0 ) ;
41+ writerSemaphore = new ( 0 ) ;
42+ }
43+
44+ public void Dispose ( )
45+ {
46+ lock ( mutex )
47+ {
48+ if ( disposed )
49+ return ;
50+ disposed = true ;
51+ }
52+
53+ readerSemaphore . Dispose ( ) ;
54+ writerSemaphore . Dispose ( ) ;
55+ }
56+
57+ /// <summary>
58+ /// Acquires an exclusive write lock, granting sole access to the resource for write operations.
59+ /// </summary>
60+ public void WriteLock ( )
61+ => WriteLock ( default ) ;
62+
63+ /// <summary>
64+ /// Acquires an exclusive write lock, granting sole access to the resource for write operations.
65+ /// </summary>
66+ /// <param name="token">A cancellation token that can be used to cancel the lock acquisition process.</param>
67+ public void WriteLock ( CancellationToken token )
68+ => Acquire ( LockOperation . Writer , token ) ;
69+
70+ /// <summary>
71+ /// Release writer lock and wake either one writer or all waiting readers.
72+ /// </summary>
73+ public void WriteUnlock ( )
74+ => Release ( ) ;
75+
76+ /// <summary>
77+ /// Acquires a reader lock, allowing concurrent read access to the resource.
78+ /// </summary>
79+ public void ReadLock ( )
80+ => ReaderLock ( default ) ;
81+
82+ /// <summary>
83+ /// Acquires a reader lock, allowing concurrent read access to the resource.
84+ /// </summary>
85+ /// <param name="token">The cancellation token used to signal the operation's cancellation.</param>
86+ public void ReaderLock ( CancellationToken token )
87+ => Acquire ( LockOperation . Reader , token ) ;
88+
89+ /// <summary>
90+ /// Release reader lock and wake one writer when this was the last active reader.
91+ /// </summary>
92+ public void ReadUnlock ( )
93+ => Release ( ) ;
94+
95+ void Acquire ( LockOperation operation , CancellationToken cancellationToken )
96+ {
97+ SemaphoreSlim waitSemaphore ;
98+ lock ( mutex )
99+ {
100+ ObjectDisposedException . ThrowIf ( disposed , nameof ( ReaderWriterLock ) ) ;
101+
102+ switch ( operation )
103+ {
104+ case LockOperation . Reader :
105+ // Acquire reader lock immediately
106+ if ( lockHeld >= 0 && waitingWriters == 0 )
107+ {
108+ lockHeld ++ ;
109+ return ;
110+ }
111+
112+ // Prepare to wait because lock was not available for readers
113+ waitingReaders ++ ;
114+ waitSemaphore = readerSemaphore ;
115+ break ;
116+ case LockOperation . Writer :
117+ // Acquire writer lock immediately
118+ if ( lockHeld == 0 )
119+ {
120+ lockHeld = - 1 ;
121+ return ;
122+ }
123+
124+ // Prepare to wait because lock was not available for writers
125+ waitingWriters ++ ;
126+ waitSemaphore = writerSemaphore ;
127+ break ;
128+ default :
129+ throw new InvalidOperationException ( ) ;
130+ }
131+ }
132+
133+ WaitForGrant ( waitSemaphore , operation , cancellationToken ) ;
134+ }
135+
136+ void WaitForGrant ( SemaphoreSlim waitSemaphore , LockOperation operation , CancellationToken cancellationToken )
137+ {
138+ try
139+ {
140+ waitSemaphore . Wait ( cancellationToken ) ;
141+ }
142+ catch ( OperationCanceledException )
143+ {
144+ var shouldRelease = false ;
145+ lock ( mutex )
146+ {
147+ ObjectDisposedException . ThrowIf ( disposed , nameof ( ReaderWriterLock ) ) ;
148+
149+ switch ( operation )
150+ {
151+ case LockOperation . Reader :
152+ if ( grantedReaders > 0 )
153+ {
154+ // lock was granted before cancellation
155+ grantedReaders -- ;
156+ shouldRelease = true ;
157+ }
158+ else
159+ {
160+ // lock was not granted yet
161+ waitingReaders -- ;
162+ }
163+ break ;
164+ case LockOperation . Writer :
165+ if ( grantedWriters > 0 )
166+ {
167+ // lock was granted before cancellation
168+ grantedWriters -- ;
169+ shouldRelease = true ;
170+ }
171+ else
172+ {
173+ // lock was not granted yet
174+ waitingWriters -- ;
175+ }
176+ break ;
177+ default :
178+ throw new InvalidOperationException ( ) ;
179+ }
180+ }
181+
182+ if ( shouldRelease )
183+ Release ( ) ;
184+
185+ throw ;
186+ }
187+
188+ lock ( mutex )
189+ {
190+ ObjectDisposedException . ThrowIf ( disposed , nameof ( ReaderWriterLock ) ) ;
191+
192+ switch ( operation )
193+ {
194+ case LockOperation . Reader :
195+ if ( grantedReaders <= 0 )
196+ throw new SynchronizationLockException ( "Reader wakeup observed without matching grant." ) ;
197+ // Release granted counters and not waiting counters since they have been decremented at release that granted the lock.
198+ grantedReaders -- ;
199+ break ;
200+ case LockOperation . Writer :
201+ if ( grantedWriters <= 0 )
202+ throw new SynchronizationLockException ( "Writer wakeup observed without matching grant." ) ;
203+ // Release granted counters and not waiting counters since they have been decremented at release that granted the lock.
204+ grantedWriters -- ;
205+ break ;
206+ default :
207+ throw new InvalidOperationException ( ) ;
208+ }
209+ }
210+ }
211+
212+ void Release ( )
213+ {
214+ lock ( mutex )
215+ {
216+ ObjectDisposedException . ThrowIf ( disposed , nameof ( ReaderWriterLock ) ) ;
217+
218+ if ( lockHeld == 0 )
219+ throw new SynchronizationLockException ( "Unlock called when lock is not held." ) ;
220+
221+ // Update lock state first.
222+ if ( lockHeld == - 1 )
223+ lockHeld = 0 ;
224+ else
225+ lockHeld -- ;
226+
227+ // Still held by active readers.
228+ if ( lockHeld != 0 )
229+ return ;
230+
231+ // Writers have priority.
232+ if ( waitingWriters > 0 )
233+ {
234+ waitingWriters -- ;
235+ lockHeld = - 1 ;
236+ grantedWriters ++ ;
237+ _ = writerSemaphore . Release ( ) ;
238+ return ;
239+ }
240+
241+ // Consume readers next
242+ if ( waitingReaders > 0 )
243+ {
244+ var toRelease = waitingReaders ;
245+ waitingReaders = 0 ;
246+ lockHeld = toRelease ;
247+ grantedReaders += toRelease ;
248+ _ = readerSemaphore . Release ( toRelease ) ;
249+ }
250+ }
251+ }
252+ }
253+ }
0 commit comments