-
-
Notifications
You must be signed in to change notification settings - Fork 231
Expand file tree
/
Copy pathScopedCountdownLock.cs
More file actions
162 lines (139 loc) · 5.9 KB
/
ScopedCountdownLock.cs
File metadata and controls
162 lines (139 loc) · 5.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
namespace Sentry.Threading;
/// <summary>
/// A synchronization primitive that tracks the amount of <see cref="CounterScope"/>s held,
/// and is signaled when the count reaches zero while a <see cref="LockScope"/> is held.
/// </summary>
/// <remarks>
/// Similar to <see cref="CountdownEvent"/>,
/// but allows to increment the current count after the count has reached zero without resetting to the initial count before a <see cref="LockScope"/> is entered.
/// Has a similar API shape to System.Threading.Lock.
/// </remarks>
[DebuggerDisplay("IsSet = {IsSet}, Count = {Count}, IsEngaged = {IsEngaged}")]
internal sealed class ScopedCountdownLock : IDisposable
{
private readonly CountdownEvent _event;
private volatile int _isEngaged;
internal ScopedCountdownLock()
{
_event = new CountdownEvent(1);
_isEngaged = 0;
}
/// <summary>
/// <see langword="true"/> if the event is set/signaled; otherwise, <see langword="false"/>.
/// When <see langword="true"/>, the active <see cref="LockScope"/> can <see cref="LockScope.Wait"/> until the <see cref="Count"/> reaches <see langword="0"/>.
/// </summary>
internal bool IsSet => _event.IsSet;
/// <summary>
/// Gets the number of remaining <see cref="CounterScope"/> required to exit in order to set/signal the event while a <see cref="LockScope"/> is active.
/// When <see langword="0"/> and while a <see cref="LockScope"/> is active, no more <see cref="CounterScope"/> can be entered.
/// </summary>
internal int Count => _isEngaged == 1 ? _event.CurrentCount : _event.CurrentCount - 1;
/// <summary>
/// Returns <see langword="true"/> when a <see cref="LockScope"/> is active and the event can be set/signaled by <see cref="Count"/> reaching <see langword="0"/>.
/// Returns <see langword="false"/> when the <see cref="Count"/> can only reach the initial count of <see langword="1"/> when no <see cref="CounterScope"/> is active any longer.
/// </summary>
internal bool IsEngaged => _isEngaged == 1;
/// <summary>
/// No <see cref="CounterScope"/> will be entered when the <see cref="Count"/> has reached <see langword="0"/>, or while the lock is engaged via an active <see cref="LockScope"/>.
/// Check via <see cref="CounterScope.IsEntered"/> whether the underlying <see cref="CountdownEvent"/> has not been set/signaled yet.
/// To signal the underlying <see cref="CountdownEvent"/>, ensure <see cref="CounterScope.Dispose"/> is called.
/// </summary>
/// <remarks>
/// Must be disposed to exit.
/// </remarks>
internal CounterScope TryEnterCounterScope()
{
if (IsEngaged)
{
return new CounterScope();
}
if (_event.TryAddCount(1))
{
return new CounterScope(this);
}
return new CounterScope();
}
private void ExitCounterScope()
{
Debug.Assert(_event.CurrentCount >= 1);
_ = _event.Signal();
}
/// <summary>
/// When successful, the lock <see cref="IsEngaged"/>, <see cref="Count"/> can reach <see langword="0"/> when no <see cref="CounterScope"/> is active, and the event can be set/signaled.
/// Check via <see cref="LockScope.IsEntered"/> whether the Lock <see cref="IsEngaged"/>.
/// Use <see cref="LockScope.Wait"/> to block until every active <see cref="CounterScope"/> has exited before performing the locked operation.
/// After the locked operation has completed, disengage the Lock via <see cref="LockScope.Dispose"/>.
/// </summary>
/// <remarks>
/// Must be disposed to exit.
/// </remarks>
internal LockScope TryEnterLockScope()
{
if (Interlocked.CompareExchange(ref _isEngaged, 1, 0) == 0)
{
Debug.Assert(_event.CurrentCount >= 1);
_ = _event.Signal(); // decrement the initial count of 1, so that the event can be set with the count reaching 0 when all entered 'CounterScope' instances have exited
return new LockScope(this);
}
return new LockScope();
}
private void ExitLockScope()
{
Debug.Assert(_event.IsSet);
_event.Reset(); // reset the signaled event to the initial count of 1, so that new 'CounterScope' instances can be entered again
if (Interlocked.CompareExchange(ref _isEngaged, 0, 1) != 1)
{
Debug.Fail("The Lock should have not been disengaged without being engaged first.");
}
}
/// <inheritdoc />
public void Dispose()
{
_event.Dispose();
}
internal ref struct CounterScope : IDisposable
{
private ScopedCountdownLock? _lockObj;
internal CounterScope(ScopedCountdownLock lockObj)
{
_lockObj = lockObj;
}
internal bool IsEntered => _lockObj is not null;
public void Dispose()
{
var lockObj = _lockObj;
if (lockObj is not null)
{
_lockObj = null;
lockObj.ExitCounterScope();
}
}
}
internal ref struct LockScope : IDisposable
{
private ScopedCountdownLock? _lockObj;
internal LockScope(ScopedCountdownLock lockObj)
{
_lockObj = lockObj;
}
internal bool IsEntered => _lockObj is not null;
/// <summary>
/// Blocks the current thread until the current <see cref="Count"/> reaches <see langword="0"/> and the event is set/signaled.
/// The caller will return immediately if the event is currently in a set/signaled state.
/// </summary>
internal void Wait()
{
var lockObj = _lockObj;
lockObj?._event.Wait(Timeout.Infinite, CancellationToken.None);
}
public void Dispose()
{
var lockObj = _lockObj;
if (lockObj is not null)
{
_lockObj = null;
lockObj.ExitLockScope();
}
}
}
}