@@ -27,8 +27,7 @@ public sealed partial class MutexSlim
27
27
* (I'm looking at you, SemaphoreSlim... you know what you did)
28
28
* - must be low allocation
29
29
* - should allow control of the threading model for async callback
30
- * - *reasonable* "fairness" is needed (we won't get fussy if there
31
- * are rare scenarios that don't guarantee absolute fairness)
30
+ * - fairness is desirable; see note "FAIRNESS" below
32
31
* - timeout support is required
33
32
* value can be per mutex - doesn't need to be per-Wait[Async]
34
33
* - a "using"-style API is a nice-to-have, to avoid try/finally
@@ -39,6 +38,31 @@ public sealed partial class MutexSlim
39
38
* - async path uses custom awaitable with zero-alloc on immediate win
40
39
*/
41
40
41
+ /* Note on FAIRNESS
42
+
43
+ in particular, consider the following scenario with two
44
+ *incomplete* async calls from the same execution path:
45
+
46
+ // ** note not awaited yet
47
+ var a = obj.DoSomethingAsync("a"); // that uses MutexSlim
48
+ var b = obj.DoSomethingAsync("b"); // that uses MutexSlim
49
+
50
+ await a;
51
+ await b;
52
+
53
+ Now: which runs first? "a"? or "b"? Without fairness, it can
54
+ be either; consider that the lock is owned when "a" starts
55
+ so a continuation is queued and the execution flow of
56
+ DoSomethingAsync is suspended at the TryWaitAsync; now
57
+ imagine a rare race when the thing releasing the lock
58
+ happens at *exactly* the same time as our call into "b";
59
+ MutexSlim allows callers to interlocked-take the token,
60
+ so "b" can sneak in and win; so "b" runs, and activates
61
+ "a" *on the way out*. The fairness of MutexSlim has
62
+ been modified to prevent this problem.
63
+
64
+ */
65
+
42
66
/* usage:
43
67
44
68
using (var token = mutex.TryWait())
@@ -117,64 +141,62 @@ private PendingLockItem DequeueInsideLock()
117
141
[ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
118
142
private void Release ( int token , bool demandMatch = true )
119
143
{
120
- // release the token (we can check for wrongness without needing the lock, note)
121
- Log ( $ "attempting to release token { LockState . ToString ( token ) } ") ;
122
- if ( Interlocked . CompareExchange ( ref _token , LockState . ChangeState ( token , LockState . Pending ) , token ) != token )
144
+ // validate the token
145
+ if ( Volatile . Read ( ref _token ) != token )
123
146
{
124
147
Log ( $ "token { LockState . ToString ( token ) } is invalid") ;
125
148
if ( demandMatch ) Throw . InvalidLockHolder ( ) ;
126
149
return ;
127
150
}
128
151
129
- ActivateNextQueueItem ( ) ;
152
+ ActivateNextQueueItemWithValidatedToken ( token ) ;
130
153
}
131
154
132
- private void ActivateNextQueueItem ( )
155
+ private void ActivateNextQueueItemWithValidatedToken ( int token )
133
156
{
134
157
// see if we can nudge the next waiter
135
158
lock ( _queue )
136
159
{
137
- if ( _queue . Count == 0 )
138
- {
139
- Log ( $ "no pending items to activate") ;
140
- _uncontested = true ;
141
- return ;
142
- }
143
160
try
144
161
{
145
- // there's work to do; try and get a new token
146
- Log ( $ "pending items: { _queue . Count } ") ;
147
- int token = TryTakeLoopIfChanges ( ) ;
148
- if ( token == 0 )
162
+ if ( _queue . Count == 0 )
149
163
{
150
- // despite it being contested, somebody else
151
- // won the lock while we were busy thinking;
152
- // this is staggeringly rare, and means the
153
- // mutex isn't *absolteuly* fair, but it is
154
- // "fair enough" to be useful and practical
164
+ Log ( $ "no pending items to activate") ;
165
+ _uncontested = true ;
155
166
return ;
156
167
}
157
168
169
+ // there's work to do; get a new token
170
+ Log ( $ "pending items: { _queue . Count } ") ;
171
+
172
+ // we're expecting to activate; get a new token speculatively
173
+ // (needs to be new so the old caller can't double-dispose and
174
+ // release someone else's lock)
175
+ Volatile . Write ( ref _token , token = LockState . GetNextToken ( token ) ) ;
176
+
177
+ // try to give the new token to someone
158
178
while ( _queue . Count != 0 )
159
179
{
160
180
var next = DequeueInsideLock ( ) ;
161
181
if ( next . TrySetResult ( token ) )
162
182
{
163
183
Log ( $ "handed lock to { next } ") ;
164
- return ; // so we don't release the token
184
+ token = 0 ; // so we don't release the token
185
+ return ;
165
186
}
166
187
else
167
188
{
168
189
Log ( $ "lock rejected by { next } ") ;
169
190
}
170
191
}
171
-
172
- // nobody actually wanted it; return it
173
- Log ( "returning unwanted lock" ) ;
174
- Volatile . Write ( ref _token , LockState . ChangeState ( token , LockState . Pending ) ) ;
175
192
}
176
193
finally
177
194
{
195
+ if ( token != 0 ) // nobody actually wanted it; return it
196
+ { // (this could be the original token, or a new speculative token)
197
+ Log ( "returning unwanted lock" ) ;
198
+ Volatile . Write ( ref _token , LockState . ChangeState ( token , LockState . Pending ) ) ;
199
+ }
178
200
_uncontested = _queue . Count == 0 ;
179
201
SetNextAsyncTimeoutInsideLock ( ) ;
180
202
}
0 commit comments