|
| 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. |
0 commit comments