Skip to content

Commit

Permalink
main
Browse files Browse the repository at this point in the history
  • Loading branch information
sharpchen committed Feb 23, 2025
1 parent 1b4246d commit 11a2b8a
Show file tree
Hide file tree
Showing 6 changed files with 473 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Monitor & lock Keyword

Locking an object meaning that only one thread can access the object at a time.
Other threads have to wait until the `Monitor` exits.

```cs
BankAccount account = new();

var deposits = Enumerable.Range(1, 100)
.Select(_ => Task.Run(() => account.Deposit(100))); // [!code highlight]
var withdraws = Enumerable.Range(1, 100)
.Select(_ => Task.Run(() => account.Withdraw(100))); // [!code highlight]
Task.WaitAll([.. deposits, .. withdraws]);
Console.WriteLine(account.Balance); // now always 0 // [!code highlight]
public class BankAccount
{
private readonly object _lock = new(); // [!code ++]
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
lock (_lock) // [!code ++]
Balance += amount;
}
public void Withdraw(decimal amount)
{
lock (_lock) // [!code ++]
Balance -= amount;
}
}
```

> [!NOTE]
> `lock` statement is just a syntax sugar for `Monitor.Enter(obj)` and `Monitor.Exit(obj)`
>```cs
>public void Deposit(decimal amount)
>{
> Monitor.Enter(_lock); // [!code highlight]
> Balance += amount;
> Monitor.Exit(_lock); // [!code highlight]
>}
>public void Withdraw(decimal amount)
>{
> Monitor.Enter(_lock); // [!code highlight]
> Balance -= amount;
> Monitor.Exit(_lock); // [!code highlight]
>}
>```
## Do Not Lock on Self
Lock on a new object field is always the recommended way. `lock(this)` might work only when the locking happens inside the method body.
If the containing object was locked outside, such operation might cause a deadlock.
Here, the Transfer method locks on the `from` and `to` instances.
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.
```cs
public class BankAccount
{
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
lock (this) // [!code warning]
Balance += amount;
}
public void Withdraw(decimal amount)
{
lock (this) // [!code warning]
Balance -= amount;
}
public void Transfer(BankAccount from, BankAccount to, decimal amount)
{
lock (from) // [!code warning]
{ // [!code warning]
lock (to) // [!code warning]
{ // [!code warning]
from.Withdraw(amount); // [!code warning]
to.Deposit(amount); // [!code warning]
} // [!code warning]
} // [!code warning]
}
}
```
## Conclusion

- Use `lock` as shorthand for `Monitor.Enter(obj)` and `Monitor.Exit(obj)`
- Always lock on a private object field to prevent deadlock.

Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# Mutex

The term "Mutex" is short for "Mutual Exclusion", which refers to the concept of exclusive access to a shared resource.
A mutex is a synchronization primitive used to control access to a resource (or a critical section of code) by **multiple threads or processes**.
It ensures that only one thread or process can access the resource at a time, preventing conflicts and race conditions.

> [!NOTE]
> `Mutex` is a derived type of `WaitHandle`.
## Local Mutex

A local mutex is mutex used for threads. Such mutex only exists during current process, not visible across the operating system.

> [!NOTE]
> Local mutex generally don't need a name on creation using constructor unless you need to get it back using `Mutex.OpenExisting(string name)`
- `mutex.WaitOne`: blocks current thread until a signal, possibly with a timeout
- `mutex.ReleaseMutex`: re-enable access for other threads and processes

One could use `Mutex` as like `Monitor` but this can be more expensive since `Mutex` is heavier that `object`.
A common way is manage access dedicately from external for a single object.

When a mutex is acquired with `mutex.WaitOne`, meaning that current thread has a *exclusive* access to the shared resource.
So, it's like all threads are queuing until mutex signals.

And finally `mutex.ReleaseMutex` is required no matter what happens if mutex for current thread is already acquired, **otherwise other threads just waits forever**.

> [!IMPORTANT]
> 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.
:::code-group

```cs[Use this]
BankAccount account = new();
Mutex mutex = new(); // use one mutex for only one object
var deposits = Enumerable.Range(1, 1000).Select(_ => Task.Run(() =>
{
bool locked = mutex.WaitOne();
try
{
if (locked) account.Deposit(100);
}
finally
{
if (locked) mutex.ReleaseMutex();
}
}));
var withdraws = Enumerable.Range(1, 1000).Select(_ => Task.Run(() =>
{
bool locked = mutex.WaitOne();
try
{
if (locked) account.Withdraw(100);
}
finally
{
if (locked) mutex.ReleaseMutex();
}
}));
```

```cs[Like Monitor]
public class BankAccount
{
public int Balance { get; private set; }
// this is not recommeneded // [!code warning]
private readonly Mutex _mutex = new(); // [!code ++]
public void Deposit(int amount)
{
_mutex.WaitOne(); // [!code ++]
Balance += amount;
_mutex.ReleaseMutex(); // [!code ++]
}
public void Withdraw(int amount)
{
_mutex.WaitOne(); // [!code ++]
Balance -= amount;
_mutex.ReleaseMutex(); // [!code ++]
}
}
```
:::

> [!NOTE]
> One can declare `Mutex` as `static` so it can be used to manage multiple resources, but this is generally not recommended.
> Because only one thread can use the static `Mutex` at a time, other thread has to wait for the release of the mutex.
### Manage Multiple Resources

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.
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.

Key points to work with multiple mutex:

- if resources protected by multiple mutex were all involved, one has to wait all of these mutex involved before you perform the operation
- remember to release all of them when the task was done.


```cs
BankAccount from = new();
BankAccount to = new();
// use two mutex to manage two shared objects
Mutex mutexFrom = new(); // [!code highlight]
Mutex mutexTo = new(); // [!code highlight]
var deposits = Enumerable.Range(1, 1000).Select(_ => Task.Run(() =>
{
bool locked = mutexFrom.WaitOne();
try
{
if (locked) from.Deposit(100);
}
finally
{
if (locked) mutexFrom.ReleaseMutex();
}
}));

var withdraws = Enumerable.Range(1, 1000).Select(_ => Task.Run(() =>
{
bool locked = mutexTo.WaitOne();
try
{
if (locked) to.Withdraw(100);
}
finally
{
if (locked) mutexTo.ReleaseMutex();
}
}));

Task transfer = Task.Run(() =>
{
Thread.Sleep(100); // just make sure transfer happens after Deposit and Withdraw
bool locked = Mutex.WaitAll([mutexFrom, mutexTo]); // same as WaitHandle.WaitAll // [!code highlight]
try
{
if (locked) BankAccount.Transfer(from, to, from.Balance);
}
finally
{
if (locked)
{
// release all of them
mutexFrom.ReleaseMutex(); // [!code highlight]
mutexTo.ReleaseMutex(); // [!code highlight]
}
}
});

Task.WaitAll([.. deposits, .. withdraws, transfer]);

Console.WriteLine(from.Balance); // 0
Console.WriteLine(to.Balance); // 0
```

## Global Mutex

Global Mutex is created by Mutex constructor with a specified name.
It's registered by it's name across the operating system.

- Use `Mutex.OpenExisting` to open a registered glbal mutex by its name.
- This always returns a mutex represents the registered one.(the reference might be different)
```cs
_ = Mutex.OpenExisting("Global\\...");
```

- `WaitHandleCannotBeOpenedException` can be thrown when there's not such global mutex been registered.

The following example shows how to prevent multiple instances being created on a same system.

```cs
internal class Program
{

const string MutexId = "Global\\149b89b4-3bc9-4df5-9064-5d28b4ae8ca4"; // must start with Global\ // [!code highlight]
static Mutex? mutex = null;
private static void Main(string[] args)
{
try
{
// might throw here when mutex not registered with the name
_ = Mutex.OpenExisting(MutexId);
Console.WriteLine($"{MutexId} is running, cannot start another instance.");
}
catch (WaitHandleCannotBeOpenedException)
{
mutex = new Mutex(false, MutexId); // register the mutex, `initiallyOwned` doesn't matter here
Console.WriteLine("A unique instance is started");
}

Console.ReadKey();
mutex?.Dispose();
}
}
```

> [!NOTE]
> `Local\` is no required when creating a local mutex
## Abandoned Mutex

If a thread acquired a mutex was terminated without releasing the mutex, such **mutex is said to be abandoned**.

```cs
Mutex mutex = new();
BankAccount account = new();

new Thread(() => {
bool locked = false;
try {
locked = mutex.WaitOne();
if (locked) account.Deposit(100);
} finally {
// if (locked) mutex.ReleaseMutex(); // [!code highlight]
}
}).Start();

Thread.Sleep(1000);

try {
_ = mutex.WaitOne(); // acquire again
} catch (AbandonedMutexException) {
// caught here
Console.WriteLine($"{nameof(AbandonedMutexException)} was thrown"); // [!code highlight]
}
```

> [!IMPORTANT]
> Tasks started were managed by `ThreadPool`, so they're not necessarily terminated since the corresponding thread is cached and still active.
> So a unreleased mutex might not throw `AbandonedMutexException` from a completed task.
## Conclusion

- Use a dedicated `Mutex` for each resource/object.
- `Mutex` is not generally needed as a field, one should use it outside the object.
- `Mutex` can control access from threads and processes, use `Global\` prefix to register a global mutex.
- `Mutex` is a `IDisposable`, one should dispose it after finishing the work.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Why Lock?

## What is Atomicity

An operation cannot be interrupted is atomic.

- Assignment to reference type
- Direct assignment to value type
- Reads and writes for <=32bit value types on 32bit-platform
- Reads and writes for <=64bit value types on 64bit-platform

### Example

An write to a value type property calculates a temporary value and assigns it to the backing field.
So there's a gap when you calculate such temporary value so other thread might come in to mess up the operation.

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.

```cs
BankAccount account = new();

// modify the value concurrently // [!code highlight]
var deposits = Enumerable.Range(1, 100)
.Select(_ => Task.Run(() => account.Deposit(100))); // [!code highlight]
var withdraws = Enumerable.Range(1, 100)
.Select(_ => Task.Run(() => account.Withdraw(100))); // [!code highlight]
Task.WaitAll([.. deposits, .. withdraws]);

Console.WriteLine(account.Balance); // not always 0 // [!code warning]
public class BankAccount
{
public int Balance { get; private set; }
public void Deposit(int amount) { Balance += amount; }
public void Withdraw(int amount) { Balance -= amount; }
}
```

## Solution

- Use `Monitor` or `lock` keyword
- `InterLocked` static utils dedicated for **integer types**, it's **more performant** than `lock`
```cs
public class BankAccount
{
private int _balance; // [!code ++]
public int Balance { get => _balance; private set => _balance = value; }

public void Deposit(int amount)
{
Interlocked.Add(ref _balance, amount); // [!code ++]
}
public void Withdraw(int amount)
{
Interlocked.Add(ref _balance, -amount); // [!code ++]
}
}
```
- `Mutex` to handle synchronization for threads and processes
Loading

0 comments on commit 11a2b8a

Please sign in to comment.