Skip to content

Commit 11a2b8a

Browse files
committed
main
1 parent 1b4246d commit 11a2b8a

File tree

6 files changed

+473
-2
lines changed

6 files changed

+473
-2
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Monitor & lock Keyword
2+
3+
Locking an object meaning that only one thread can access the object at a time.
4+
Other threads have to wait until the `Monitor` exits.
5+
6+
```cs
7+
BankAccount account = new();
8+
9+
var deposits = Enumerable.Range(1, 100)
10+
.Select(_ => Task.Run(() => account.Deposit(100))); // [!code highlight]
11+
var withdraws = Enumerable.Range(1, 100)
12+
.Select(_ => Task.Run(() => account.Withdraw(100))); // [!code highlight]
13+
14+
Task.WaitAll([.. deposits, .. withdraws]);
15+
Console.WriteLine(account.Balance); // now always 0 // [!code highlight]
16+
17+
public class BankAccount
18+
{
19+
private readonly object _lock = new(); // [!code ++]
20+
public decimal Balance { get; private set; }
21+
public void Deposit(decimal amount)
22+
{
23+
lock (_lock) // [!code ++]
24+
Balance += amount;
25+
}
26+
public void Withdraw(decimal amount)
27+
{
28+
lock (_lock) // [!code ++]
29+
Balance -= amount;
30+
}
31+
}
32+
```
33+
34+
> [!NOTE]
35+
> `lock` statement is just a syntax sugar for `Monitor.Enter(obj)` and `Monitor.Exit(obj)`
36+
>```cs
37+
>public void Deposit(decimal amount)
38+
>{
39+
> Monitor.Enter(_lock); // [!code highlight]
40+
> Balance += amount;
41+
> Monitor.Exit(_lock); // [!code highlight]
42+
>}
43+
>public void Withdraw(decimal amount)
44+
>{
45+
> Monitor.Enter(_lock); // [!code highlight]
46+
> Balance -= amount;
47+
> Monitor.Exit(_lock); // [!code highlight]
48+
>}
49+
>```
50+
51+
## Do Not Lock on Self
52+
53+
Lock on a new object field is always the recommended way. `lock(this)` might work only when the locking happens inside the method body.
54+
If the containing object was locked outside, such operation might cause a deadlock.
55+
56+
Here, the Transfer method locks on the `from` and `to` instances.
57+
If the `BankAccount` class internally uses `lock(this)`, this could lead to a deadlock if two threads attempt to transfer money between the same accounts in opposite directions.
58+
59+
```cs
60+
public class BankAccount
61+
{
62+
public decimal Balance { get; private set; }
63+
public void Deposit(decimal amount)
64+
{
65+
lock (this) // [!code warning]
66+
Balance += amount;
67+
}
68+
public void Withdraw(decimal amount)
69+
{
70+
lock (this) // [!code warning]
71+
Balance -= amount;
72+
}
73+
public void Transfer(BankAccount from, BankAccount to, decimal amount)
74+
{
75+
lock (from) // [!code warning]
76+
{ // [!code warning]
77+
lock (to) // [!code warning]
78+
{ // [!code warning]
79+
from.Withdraw(amount); // [!code warning]
80+
to.Deposit(amount); // [!code warning]
81+
} // [!code warning]
82+
} // [!code warning]
83+
}
84+
}
85+
```
86+
87+
## Conclusion
88+
89+
- Use `lock` as shorthand for `Monitor.Enter(obj)` and `Monitor.Exit(obj)`
90+
- Always lock on a private object field to prevent deadlock.
91+
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# Mutex
2+
3+
The term "Mutex" is short for "Mutual Exclusion", which refers to the concept of exclusive access to a shared resource.
4+
A mutex is a synchronization primitive used to control access to a resource (or a critical section of code) by **multiple threads or processes**.
5+
It ensures that only one thread or process can access the resource at a time, preventing conflicts and race conditions.
6+
7+
> [!NOTE]
8+
> `Mutex` is a derived type of `WaitHandle`.
9+
10+
## Local Mutex
11+
12+
A local mutex is mutex used for threads. Such mutex only exists during current process, not visible across the operating system.
13+
14+
> [!NOTE]
15+
> Local mutex generally don't need a name on creation using constructor unless you need to get it back using `Mutex.OpenExisting(string name)`
16+
17+
- `mutex.WaitOne`: blocks current thread until a signal, possibly with a timeout
18+
- `mutex.ReleaseMutex`: re-enable access for other threads and processes
19+
20+
One could use `Mutex` as like `Monitor` but this can be more expensive since `Mutex` is heavier that `object`.
21+
A common way is manage access dedicately from external for a single object.
22+
23+
When a mutex is acquired with `mutex.WaitOne`, meaning that current thread has a *exclusive* access to the shared resource.
24+
So, it's like all threads are queuing until mutex signals.
25+
26+
And finally `mutex.ReleaseMutex` is required no matter what happens if mutex for current thread is already acquired, **otherwise other threads just waits forever**.
27+
28+
> [!IMPORTANT]
29+
> If you specified a timeout when waiting for ownership of mutex, the `bool` flag returned indicates whether current thread could have access to the resource, so you should have a conditional check on that value.
30+
31+
:::code-group
32+
33+
```cs[Use this]
34+
BankAccount account = new();
35+
36+
Mutex mutex = new(); // use one mutex for only one object
37+
38+
var deposits = Enumerable.Range(1, 1000).Select(_ => Task.Run(() =>
39+
{
40+
bool locked = mutex.WaitOne();
41+
try
42+
{
43+
if (locked) account.Deposit(100);
44+
}
45+
finally
46+
{
47+
if (locked) mutex.ReleaseMutex();
48+
}
49+
}));
50+
var withdraws = Enumerable.Range(1, 1000).Select(_ => Task.Run(() =>
51+
{
52+
bool locked = mutex.WaitOne();
53+
try
54+
{
55+
if (locked) account.Withdraw(100);
56+
}
57+
finally
58+
{
59+
if (locked) mutex.ReleaseMutex();
60+
}
61+
}));
62+
```
63+
64+
```cs[Like Monitor]
65+
public class BankAccount
66+
{
67+
public int Balance { get; private set; }
68+
// this is not recommeneded // [!code warning]
69+
private readonly Mutex _mutex = new(); // [!code ++]
70+
public void Deposit(int amount)
71+
{
72+
_mutex.WaitOne(); // [!code ++]
73+
Balance += amount;
74+
_mutex.ReleaseMutex(); // [!code ++]
75+
}
76+
public void Withdraw(int amount)
77+
{
78+
_mutex.WaitOne(); // [!code ++]
79+
Balance -= amount;
80+
_mutex.ReleaseMutex(); // [!code ++]
81+
}
82+
}
83+
```
84+
:::
85+
86+
> [!NOTE]
87+
> One can declare `Mutex` as `static` so it can be used to manage multiple resources, but this is generally not recommended.
88+
> Because only one thread can use the static `Mutex` at a time, other thread has to wait for the release of the mutex.
89+
90+
### Manage Multiple Resources
91+
92+
When one account needs to transfer money to another, two mutex would be required to control the exclusive access from each of them, because there might have deposit or withdraw at the same time.
93+
And the transfer have to wait until an instance when there's no any deposit and withdraw, this is where `Mutex.WaitAll` comes into play.
94+
95+
Key points to work with multiple mutex:
96+
97+
- if resources protected by multiple mutex were all involved, one has to wait all of these mutex involved before you perform the operation
98+
- remember to release all of them when the task was done.
99+
100+
101+
```cs
102+
BankAccount from = new();
103+
BankAccount to = new();
104+
// use two mutex to manage two shared objects
105+
Mutex mutexFrom = new(); // [!code highlight]
106+
Mutex mutexTo = new(); // [!code highlight]
107+
108+
var deposits = Enumerable.Range(1, 1000).Select(_ => Task.Run(() =>
109+
{
110+
bool locked = mutexFrom.WaitOne();
111+
try
112+
{
113+
if (locked) from.Deposit(100);
114+
}
115+
finally
116+
{
117+
if (locked) mutexFrom.ReleaseMutex();
118+
}
119+
}));
120+
121+
var withdraws = Enumerable.Range(1, 1000).Select(_ => Task.Run(() =>
122+
{
123+
bool locked = mutexTo.WaitOne();
124+
try
125+
{
126+
if (locked) to.Withdraw(100);
127+
}
128+
finally
129+
{
130+
if (locked) mutexTo.ReleaseMutex();
131+
}
132+
}));
133+
134+
Task transfer = Task.Run(() =>
135+
{
136+
Thread.Sleep(100); // just make sure transfer happens after Deposit and Withdraw
137+
bool locked = Mutex.WaitAll([mutexFrom, mutexTo]); // same as WaitHandle.WaitAll // [!code highlight]
138+
try
139+
{
140+
if (locked) BankAccount.Transfer(from, to, from.Balance);
141+
}
142+
finally
143+
{
144+
if (locked)
145+
{
146+
// release all of them
147+
mutexFrom.ReleaseMutex(); // [!code highlight]
148+
mutexTo.ReleaseMutex(); // [!code highlight]
149+
}
150+
}
151+
});
152+
153+
Task.WaitAll([.. deposits, .. withdraws, transfer]);
154+
155+
Console.WriteLine(from.Balance); // 0
156+
Console.WriteLine(to.Balance); // 0
157+
```
158+
159+
## Global Mutex
160+
161+
Global Mutex is created by Mutex constructor with a specified name.
162+
It's registered by it's name across the operating system.
163+
164+
- Use `Mutex.OpenExisting` to open a registered glbal mutex by its name.
165+
- This always returns a mutex represents the registered one.(the reference might be different)
166+
```cs
167+
_ = Mutex.OpenExisting("Global\\...");
168+
```
169+
170+
- `WaitHandleCannotBeOpenedException` can be thrown when there's not such global mutex been registered.
171+
172+
The following example shows how to prevent multiple instances being created on a same system.
173+
174+
```cs
175+
internal class Program
176+
{
177+
178+
const string MutexId = "Global\\149b89b4-3bc9-4df5-9064-5d28b4ae8ca4"; // must start with Global\ // [!code highlight]
179+
static Mutex? mutex = null;
180+
private static void Main(string[] args)
181+
{
182+
try
183+
{
184+
// might throw here when mutex not registered with the name
185+
_ = Mutex.OpenExisting(MutexId);
186+
Console.WriteLine($"{MutexId} is running, cannot start another instance.");
187+
}
188+
catch (WaitHandleCannotBeOpenedException)
189+
{
190+
mutex = new Mutex(false, MutexId); // register the mutex, `initiallyOwned` doesn't matter here
191+
Console.WriteLine("A unique instance is started");
192+
}
193+
194+
Console.ReadKey();
195+
mutex?.Dispose();
196+
}
197+
}
198+
```
199+
200+
> [!NOTE]
201+
> `Local\` is no required when creating a local mutex
202+
203+
## Abandoned Mutex
204+
205+
If a thread acquired a mutex was terminated without releasing the mutex, such **mutex is said to be abandoned**.
206+
207+
```cs
208+
Mutex mutex = new();
209+
BankAccount account = new();
210+
211+
new Thread(() => {
212+
bool locked = false;
213+
try {
214+
locked = mutex.WaitOne();
215+
if (locked) account.Deposit(100);
216+
} finally {
217+
// if (locked) mutex.ReleaseMutex(); // [!code highlight]
218+
}
219+
}).Start();
220+
221+
Thread.Sleep(1000);
222+
223+
try {
224+
_ = mutex.WaitOne(); // acquire again
225+
} catch (AbandonedMutexException) {
226+
// caught here
227+
Console.WriteLine($"{nameof(AbandonedMutexException)} was thrown"); // [!code highlight]
228+
}
229+
```
230+
231+
> [!IMPORTANT]
232+
> Tasks started were managed by `ThreadPool`, so they're not necessarily terminated since the corresponding thread is cached and still active.
233+
> So a unreleased mutex might not throw `AbandonedMutexException` from a completed task.
234+
235+
## Conclusion
236+
237+
- Use a dedicated `Mutex` for each resource/object.
238+
- `Mutex` is not generally needed as a field, one should use it outside the object.
239+
- `Mutex` can control access from threads and processes, use `Global\` prefix to register a global mutex.
240+
- `Mutex` is a `IDisposable`, one should dispose it after finishing the work.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Why Lock?
2+
3+
## What is Atomicity
4+
5+
An operation cannot be interrupted is atomic.
6+
7+
- Assignment to reference type
8+
- Direct assignment to value type
9+
- Reads and writes for <=32bit value types on 32bit-platform
10+
- Reads and writes for <=64bit value types on 64bit-platform
11+
12+
### Example
13+
14+
An write to a value type property calculates a temporary value and assigns it to the backing field.
15+
So there's a gap when you calculate such temporary value so other thread might come in to mess up the operation.
16+
17+
The follwing example demonstrates how a bank account deposit and withdraw 100 times with a same amount simultaneously which not always results a non-zero value.
18+
19+
```cs
20+
BankAccount account = new();
21+
22+
// modify the value concurrently // [!code highlight]
23+
var deposits = Enumerable.Range(1, 100)
24+
.Select(_ => Task.Run(() => account.Deposit(100))); // [!code highlight]
25+
var withdraws = Enumerable.Range(1, 100)
26+
.Select(_ => Task.Run(() => account.Withdraw(100))); // [!code highlight]
27+
28+
Task.WaitAll([.. deposits, .. withdraws]);
29+
30+
Console.WriteLine(account.Balance); // not always 0 // [!code warning]
31+
32+
public class BankAccount
33+
{
34+
public int Balance { get; private set; }
35+
public void Deposit(int amount) { Balance += amount; }
36+
public void Withdraw(int amount) { Balance -= amount; }
37+
}
38+
```
39+
40+
## Solution
41+
42+
- Use `Monitor` or `lock` keyword
43+
- `InterLocked` static utils dedicated for **integer types**, it's **more performant** than `lock`
44+
```cs
45+
public class BankAccount
46+
{
47+
private int _balance; // [!code ++]
48+
public int Balance { get => _balance; private set => _balance = value; }
49+
50+
public void Deposit(int amount)
51+
{
52+
Interlocked.Add(ref _balance, amount); // [!code ++]
53+
}
54+
public void Withdraw(int amount)
55+
{
56+
Interlocked.Add(ref _balance, -amount); // [!code ++]
57+
}
58+
}
59+
```
60+
- `Mutex` to handle synchronization for threads and processes

0 commit comments

Comments
 (0)