Українська
Спін-блокування та сигнальні засоби
ReaderWriterLockSlim, SpinLock і SpinWait
Замок читачів і письменників
Багато даних часто читають і рідко змінюють: довідники, кеші, налаштування. Звичайний lock пропускає по одному потоку навіть тоді, коли всі лише читають. Клас ReaderWriterLockSlim має три режими:
- читання (
EnterReadLock/ExitReadLock) – одночасно будь-яка кількість читачів; - запис (
EnterWriteLock/ExitWriteLock) – монопольний доступ одного письменника; - читання з можливістю оновлення (
EnterUpgradeableReadLock) – лише один потік, який читає разом з іншими читачами й за потреби переходить у режим запису.
cs
private readonly ReaderWriterLockSlim rw = new();
private readonly Dictionary<string, decimal> rates = [];
public decimal? GetRate(string code)
{
rw.EnterReadLock();
try { return rates.TryGetValue(code, out var r) ? r : null; }
finally { rw.ExitReadLock(); }
}
public void SetRate(string code, decimal rate)
{
rw.EnterWriteLock();
try { rates[code] = rate; }
finally { rw.ExitWriteLock(); }
}Типово рекурсивний вхід заборонено (LockRecursionPolicy.NoRecursion), а потік у режимі читання не може перейти в режим запису: два таких потоки заблокували б один одного, тому для цього існує режим оновлення. Коли письменник чекає, нові читачі теж чекають – так письменники не голодують. ReaderWriterLockSlim реалізує IDisposable. Він виграє лише тоді, коли читань значно більше, ніж записів, і читання тривають достатньо довго; для дуже коротких секцій його накладні витрати більші, ніж у lock.
SpinLock і SpinWait
Блокування потоку ядром ОС коштує мікросекунди: перемикання контексту, пробудження. Якщо замок звільниться за кілька наносекунд, дешевше покрутитися (spin) – виконати порожній цикл, перевіряючи замок. Структура SpinLock реалізує такий замок:
cs
private SpinLock spin = new(enableThreadOwnerTracking: false);
public void Add(int value)
{
bool taken = false;
try
{
spin.Enter(ref taken);
sum += value; // дуже коротка секція
}
finally
{
if (taken) spin.Exit();
}
}SpinLock – структура, тому її не можна копіювати й не можна оголошувати як readonly-поле: виклик методу для копії блокує копію, а не спільний замок. Замок нереентрантний; усередині секції не можна чекати чи виконувати введення-виведення. Документація рекомендує переходити на SpinLock лише тоді, коли профілювання показало, що lock чи Interlocked є вузьким місцем.
Структура SpinWait допомагає написати власне очікування: SpinOnce() спочатку крутиться, а після кількох ітерацій поступається процесором іншим потокам; SpinWait.SpinUntil(() => ready) чекає виконання умови. Сучасні lock, SemaphoreSlim і ManualResetEventSlim уже поєднують коротке обертання з подальшим блокуванням.
Сигнальні засоби
Сигнальні засоби не захищають дані, а координують потоки: один потік повідомляє іншим, що щось сталося (табл. 3.2).
ManualResetEventSlim– «ворота»:Set()відчиняє їх, і всі потоки, що чекають уWait(), проходять; ворота залишаються відчиненими доReset(). Приклад – сигнал «старт» для всіх робочих потоків або «зупинка» для всіх.AutoResetEvent– «турнікет»: коженSet()пропускає один потік і автоматично закривається. Якщо ніхто не чекає, сигнал запам’ятовується, але не накопичується (дваSet()поспіль = один).CountdownEvent– лічильник:Signal()зменшує його,Wait()чекає нуля. Зручно чекати завершення N робіт, запущених у пулі потоків.Barrier– точка зустрічі N учасників:SignalAndWait()блокує, доки всі учасники не дійдуть до бар’єра, після чого всі продовжують одночасно, а бар’єр готовий до наступного кола.
cs
using CountdownEvent pending = new(initialCount: 5);
for (int i = 1; i <= 5; i++)
{
int id = i;
ThreadPool.QueueUserWorkItem(_ =>
{
Console.WriteLine($"Звіт {id} готовий");
pending.Signal(); // одна робота завершена
});
}
pending.Wait(); // чекати всі п’ять
Console.WriteLine("Усі звіти готові");Таблиця 3.2. Засоби синхронізації .NET
| Засіб | Між процесами | Призначення |
|---|---|---|
lock / Lock / Monitor | ні | взаємне виключення; Monitor – ще й умовні змінні |
Interlocked | ні | атомарні операції над однією змінною |
Mutex | так (іменований) | взаємне виключення, зокрема між процесами |
SemaphoreSlim | ні | обмеження кількості потоків, асинхронне очікування |
Semaphore | так (Windows) | обмеження кількості потоків і процесів |
ReaderWriterLockSlim | ні | багато читачів або один письменник |
SpinLock, SpinWait | ні | дуже короткі секції без переходу в ядро |
ManualResetEventSlim | ні | сигнал для всіх очікувальних потоків |
AutoResetEvent | так (EventWaitHandle, Windows) | сигнал для одного потоку |
CountdownEvent | ні | очікування завершення N робіт |
Barrier | ні | одночасний перехід N потоків до наступного етапу |
Класи із суфіксом Slim працюють у межах процесу й швидші за «системні» аналоги, бо спочатку обходяться без ядра ОС. Огляд усіх засобів: https://learn.microsoft.com/dotnet/standard/threading/overview-of-synchronization-primitives.