@@ -44,21 +44,29 @@ public IContext Context
44
44
/// </summary>
45
45
public bool IsSpinning
46
46
{
47
- get { return ! this . IsIdle . IsSet ; }
47
+ get => this . _IsSpinning ;
48
+ private set => this . _IsSpinning = value ;
48
49
}
49
50
51
+ private volatile bool _IsSpinning = false ;
52
+
50
53
/// <summary>
51
54
/// Whether a rescan is scheduled.
52
55
/// </summary>
53
56
public bool RescanScheduled
54
57
{
55
- get { return this . _RescanScheduled ; }
56
- private set { this . _RescanScheduled = value ; }
58
+ get => this . _RescanScheduled ;
59
+ private set => this . _RescanScheduled = value ;
57
60
}
58
61
59
62
// volatile since it may be changed by multiple threads
60
63
private volatile bool _RescanScheduled = false ;
61
64
65
+ /// <summary>
66
+ /// To prevent <see cref="TryWait"/> from being starved by multiple spins.
67
+ /// </summary>
68
+ private long SpinId = 0 ;
69
+
62
70
/// <inheritdoc/>
63
71
public bool IsDisposed
64
72
{
@@ -92,6 +100,9 @@ public bool IsReadOnly
92
100
/// <summary>
93
101
/// Wait set used while spinning.
94
102
/// </summary>
103
+ /// <remarks>
104
+ /// Is also used to notify <see cref="TryWait"/> when the executor finished spinning.
105
+ /// </remarks>
95
106
private readonly WaitSet WaitSet ;
96
107
97
108
/// <summary>
@@ -104,11 +115,6 @@ public bool IsReadOnly
104
115
/// </summary>
105
116
private readonly HashSet < INode > Nodes = new HashSet < INode > ( ) ;
106
117
107
- /// <summary>
108
- /// Event signaling whether the executor is not spinning.
109
- /// </summary>
110
- private readonly ManualResetEventSlim IsIdle = new ManualResetEventSlim ( true ) ;
111
-
112
118
/// <summary>
113
119
/// Create a new instance.
114
120
/// </summary>
@@ -286,21 +292,92 @@ public bool TryScheduleRescan(INode node)
286
292
/// <inheritdoc/>
287
293
public void Wait ( )
288
294
{
289
- bool success = this . TryWait ( TimeSpan . FromMilliseconds ( - 1 ) ) ;
290
- Debug . Assert ( success , "infinite wait timed out" ) ;
295
+ if ( this . RescanScheduled )
296
+ {
297
+ lock ( this . WaitSet )
298
+ {
299
+ this . WaitUntilDone ( this . SpinId ) ;
300
+ }
301
+ }
291
302
}
292
303
293
304
/// <remarks>
294
305
/// This method is thread safe.
295
306
/// </remarks>
296
307
/// <exception cref="ObjectDisposedException"> If the executor was disposed. </exception>
308
+ /// <exception cref="ArgumentOutOfRangeException"> If the timeout is negative or too big. </exception>
297
309
/// <inheritdoc/>
298
310
public bool TryWait ( TimeSpan timeout )
299
311
{
300
- if ( this . RescanScheduled && this . IsSpinning )
312
+ if ( timeout . Ticks < 0 )
313
+ {
314
+ throw new ArgumentOutOfRangeException ( "timeout is negative" ) ;
315
+ }
316
+ if ( this . RescanScheduled )
317
+ {
318
+ lock ( this . WaitSet )
319
+ {
320
+ // read id inside the lock to prevent an outdated id from being copied
321
+ return this . WaitUntilDone ( this . SpinId , timeout ) ;
322
+ }
323
+ }
324
+ return true ;
325
+ }
326
+
327
+ /// <summary>
328
+ /// Utility method to wait until the current spin has finished.
329
+ /// </summary>
330
+ /// <remarks>
331
+ /// This replaces a <see cref="ManualResetEventSlim"/> which did starve waiters
332
+ /// when spinning multiple times.
333
+ /// </remarks>
334
+ /// <param name="spinId"> Current spin id. </param>
335
+ private void WaitUntilDone ( long spinId )
336
+ {
337
+ // the condition is checked with the lock held to prevent
338
+ // a the spin from pulsing before the wait can be started
339
+ while ( this . IsSpinning && this . SpinId == spinId )
340
+ {
341
+ try
342
+ {
343
+ // stop a possible current spin
344
+ this . Interrupt ( ) ;
345
+ }
346
+ catch ( ObjectDisposedException )
347
+ {
348
+ // if the context is shut down then the
349
+ // guard condition might be disposed but
350
+ // nodes still have to be removed
351
+ }
352
+ Monitor . Wait ( this . WaitSet ) ;
353
+ }
354
+ }
355
+
356
+ /// <summary>
357
+ /// Utility method to wait until the current spin has finished.
358
+ /// </summary>
359
+ /// <param name="spinId"> Current spin id. </param>
360
+ /// <param name="timeout"> Timeout when waiting </param>
361
+ /// <returns> Whether the wait did not time out. </returns>
362
+ /// <exception cref="ArgumentOutOfRangeException"> Timeout is too big. </exception>
363
+ private bool WaitUntilDone ( long spinId , TimeSpan timeout )
364
+ {
365
+ int milliSeconds ;
366
+ try
367
+ {
368
+ milliSeconds = Convert . ToInt32 ( timeout . TotalMilliseconds ) ;
369
+ }
370
+ catch ( OverflowException e )
371
+ {
372
+ throw new ArgumentOutOfRangeException ( "timeout too big" , e ) ;
373
+ }
374
+ int remainingTimeout = milliSeconds ;
375
+ uint startTime = ( uint ) Environment . TickCount ;
376
+ while ( this . IsSpinning && this . SpinId == spinId )
301
377
{
302
378
try
303
379
{
380
+ // stop a possible current spin
304
381
this . Interrupt ( ) ;
305
382
}
306
383
catch ( ObjectDisposedException )
@@ -309,7 +386,22 @@ public bool TryWait(TimeSpan timeout)
309
386
// guard condition might be disposed but
310
387
// nodes still have to be removed
311
388
}
312
- return this . IsIdle . Wait ( timeout ) ;
389
+ if ( ! Monitor . Wait ( this . WaitSet , remainingTimeout ) )
390
+ {
391
+ // if the wait timed out return immediately
392
+ return false ;
393
+ }
394
+ // update the timeout for the next wait
395
+ uint elapsed = ( uint ) Environment . TickCount - startTime ;
396
+ if ( elapsed > int . MaxValue )
397
+ {
398
+ return false ;
399
+ }
400
+ remainingTimeout = milliSeconds - ( int ) elapsed ;
401
+ if ( remainingTimeout <= 0 )
402
+ {
403
+ return false ;
404
+ }
313
405
}
314
406
return true ;
315
407
}
@@ -338,10 +430,10 @@ public void Interrupt()
338
430
/// <returns> Whether work could be processed since no rescan was scheduled. </returns>
339
431
public bool TrySpin ( TimeSpan timeout )
340
432
{
341
- this . IsIdle . Reset ( ) ;
433
+ this . IsSpinning = true ;
342
434
try
343
435
{
344
- // check after resetting IsIdle to
436
+ // check after setting IsSpinning to
345
437
// prevent race condition
346
438
if ( this . RescanScheduled )
347
439
{
@@ -357,7 +449,16 @@ public bool TrySpin(TimeSpan timeout)
357
449
}
358
450
finally
359
451
{
360
- this . IsIdle . Set ( ) ;
452
+ // update flag before waking threads
453
+ this . IsSpinning = false ;
454
+ lock ( this . WaitSet )
455
+ {
456
+ // prevent other threads from reading stale result
457
+ // overflow is acceptable
458
+ unchecked { this . SpinId ++ ; }
459
+ // notify other threads that we finished spinning
460
+ Monitor . PulseAll ( this . WaitSet ) ;
461
+ }
361
462
}
362
463
return true ;
363
464
}
@@ -477,7 +578,6 @@ public void Dispose()
477
578
}
478
579
this . WaitSet . Dispose ( ) ;
479
580
this . InterruptCondition . Dispose ( ) ;
480
- this . IsIdle . Dispose ( ) ;
481
581
}
482
582
}
483
583
}
0 commit comments