diff --git a/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Monitor & lock Keyword.md b/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Monitor & lock Keyword.md new file mode 100644 index 0000000..7202925 --- /dev/null +++ b/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Monitor & lock Keyword.md @@ -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. + diff --git a/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Mutex.md b/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Mutex.md new file mode 100644 index 0000000..299b3ef --- /dev/null +++ b/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Mutex.md @@ -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. diff --git a/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Problem of Atomicity.md b/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Problem of Atomicity.md new file mode 100644 index 0000000..6686497 --- /dev/null +++ b/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Problem of Atomicity.md @@ -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 diff --git a/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Reader & Writer Lock.md b/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Reader & Writer Lock.md new file mode 100644 index 0000000..6b73872 --- /dev/null +++ b/docs/document/Modern CSharp/docs/Parallel Programming/Synchronization/Reader & Writer Lock.md @@ -0,0 +1,80 @@ +# Reader & Writer Lock + +A Reader-Writer Lock (often implemented as `ReaderWriterLock` in .NET) is a synchronization primitive that allows multiple threads to read a shared resource simultaneously but ensures exclusive access when writing to the resource. +The primary advantage of a reader-writer lock is that it allows for higher concurrency when multiple threads are reading the resource, while still ensuring mutual exclusion for write operations. + +## Usage + +- `EnterWriteLock` and `EnterReadLock` +- `ExitWriteLock` and `ExitReadLock` + +> [!TIP] +> Use `TryEnter*` and `TryExit*` if a timeout is required. + +```cs +internal class Program +{ + private static readonly ReaderWriterLockSlim _lock = new(); + private static void Main(string[] args) + { + var collection = Enumerable.Range(1, 10).ToArray(); + var tasks = Enumerable.Range(1, 10).Select(_ => + { + return Task.Run(() => + { + try + { + _lock.EnterWriteLock(); + collection[Random.Shared.Next(collection.Length)] = 111; + } + finally + { + _lock.ExitWriteLock(); + } + }); + }); + + Task.WaitAll(tasks); + Console.WriteLine(string.Join(", ", collection)); + } +} +``` + +## Upgradable Lock + +Since write lock and read lock are atomic, you can't combine the two operation as one atomic, it can be interrupted by another thread when before you enter write/read lock on current thread. +So Upgradable Lock simply provides a way to perform reading and writing as one atomic operation. + +```cs +internal class Program { + private static readonly ReaderWriterLockSlim _lock = new(); + private static void Main(string[] args) { + var collection = Enumerable.Range(1, 10).ToArray(); + var tasks = Enumerable.Range(1, 10).Select(_ => { + return Task.Run(() => { + try { + _lock.EnterUpgradeableReadLock(); // [!code highlight] + Console.WriteLine(string.Join(", ", collection)); + try { + _lock.EnterWriteLock(); // allows you to enter write lock // [!code highlight] + collection[Random.Shared.Next(collection.Length)] = 111; + } finally { + _lock.ExitWriteLock(); // [!code highlight] + } + } finally { + _lock.ExitUpgradeableReadLock(); // [!code highlight] + } + }); + }); + + Task.WaitAll(tasks); + } +} +``` + +## Conclusion + +- Use `ReaderWriterLockSlim` for a more performant experience instead of old `ReaderWriterLock`. +- Reader & Writer Lock allows **exclusive access for writing** but allows **multiple threads for reading**. +- Do not use `ReaderWriterLockSlim` on dotnet framework projects, use old `ReaderWriterLock`. +- Use `EnterUpgradeableReadLock` if you need to combine read and write as one atomic operation. diff --git a/docs/document/Modern CSharp/docs/Parallel Programming/Task/Exception Handling.md b/docs/document/Modern CSharp/docs/Parallel Programming/Task/Exception Handling.md index 5b5f4ce..efc786e 100644 --- a/docs/document/Modern CSharp/docs/Parallel Programming/Task/Exception Handling.md +++ b/docs/document/Modern CSharp/docs/Parallel Programming/Task/Exception Handling.md @@ -196,7 +196,7 @@ Child tasks are created in a detached way by default, the detached must rethrow ```cs var task = Task.Run(() => { - var child = Task.Run( () => throw new Exception("Detached child task faulted.")); + var child = Task.Run(() => throw new Exception("Detached child task faulted.")); child.Wait(); // throw here // [!code highlight] }); diff --git a/docs/services/DocumentService.ts b/docs/services/DocumentService.ts index 9726b0b..8dc9857 100644 --- a/docs/services/DocumentService.ts +++ b/docs/services/DocumentService.ts @@ -28,7 +28,7 @@ export const documentMap = { Nix: { icon: '❄', description: 'Reproduce freedom' }, 'Entity Framework Core': { icon: '🗿', description: '' }, 'HTML & CSS': { icon: '😬', description: '' }, - PowerShell: { icon: '🐚', description: 'The first strongly-typed shell' }, + PowerShell: { icon: '🐚', description: '' }, } as const satisfies DocumentInfo; export type DocumentName = keyof typeof documentMap;