У попередніх темах ми розглянули lock, Monitor, Mutex, SemaphoreSlim — всі вони базуються на блокуванні потоків. Коли ресурс зайнятий, потік переходить у стан очікування (wait state), операційна система забирає у нього CPU і віддає іншим потокам. Коли ресурс звільняється — ОС розбуджує потік і повертає йому CPU.
Це працює чудово для більшості сценаріїв, але має overhead:
1. Context Switch Cost — перемикання потоку між станами (running ↔ waiting) коштує ~1-10 мікросекунд. Це включає:
2. Kernel Transition — для kernel-mode примітивів (Mutex, базовий Semaphore) кожна операція = системний виклик у ядро ОС. Це додає ~200-1000 наносекунд overhead навіть без конкуренції.
3. Contention Overhead — при високій конкуренції потоки постійно блокуються/розблоковуються, витрачаючи більше часу на синхронізацію ніж на корисну роботу.
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
// Сценарій: 1 мільйон інкрементів лічильника через lock
int counter = 0;
object lockObj = new object();
var sw = Stopwatch.StartNew();
Parallel.For(0, 1_000_000, _ =>
{
lock (lockObj) // ← Критична секція: ОДНА інструкція (counter++)
{
counter++; // ~1 наносекунда на сучасному CPU
}
// Але lock/unlock коштує ~50-200 наносекунд при конкуренції!
});
sw.Stop();
Console.WriteLine($"Lock-based: {sw.ElapsedMilliseconds}ms, counter={counter}");
// Типовий результат: ~800-1200ms на 8-core CPU
Проблема: критична секція виконується за ~1ns, але lock overhead = ~50-200ns. Ми витрачаємо в 50-200 разів більше часу на синхронізацію ніж на корисну роботу!
Атомарні операції — спеціальні CPU інструкції що виконують операцію як єдину неподільну дію без блокування потоків. Вони працюють на рівні апаратури (CPU + cache coherency protocol) і не потребують kernel transition.
Переваги:
Обмеження:
Атомарна операція (від грецького "atomos" — неподільний) — операція що виконується як єдина неподільна дія з точки зору інших потоків. Ніхто не може "побачити" проміжний стан операції.
Аналогія: Уявіть банківський переказ 100₴ з рахунку A на рахунок B:
A -= 100; B += 100; — між цими двома інструкціями інший потік може побачити що гроші зникли з A але ще не з'явились у B (порушення інваріанту: сума = const)На рівні апаратури (x86/x64) атомарні операції реалізуються через:
1. LOCK Prefix — спеціальний префікс CPU інструкції що:
2. Cache Coherency Protocol — апаратний механізм синхронізації кешів між CPU cores:
3. Memory Ordering — атомарні операції мають вбудовані memory barriers:
CPU Core 1 CPU Core 2
─────────────────────────────────────────────────
counter = 10 (у L1 cache) counter = 10 (у L1 cache)
LOCK INC [counter]
↓
1. Блокує cache line
2. counter = 11
3. Invalidate signal → ← Отримує invalidation
4. Розблокує cache line counter у L1 = invalid
MOV EAX, [counter]
← Читає з RAM: 11
Interlocked — статичний клас у .NET що надає атомарні операції для примітивних типів. Під капотом він використовує CPU інструкції з LOCK префіксом.
using System.Threading;
int counter = 0;
// Атомарно: counter = counter + 1; return новеЗначення;
int newValue = Interlocked.Increment(ref counter);
Console.WriteLine($"After Increment: {newValue}"); // 1
// Атомарно: counter = counter - 1; return новеЗначення;
newValue = Interlocked.Decrement(ref counter);
Console.WriteLine($"After Decrement: {newValue}"); // 0
// ⚠️ ВАЖЛИВО: Increment/Decrement повертають НОВЕ значення (post-increment)
// Це НЕ те саме що counter++ (який повертає старе значення)
int x = 5;
int result1 = x++; // result1 = 5, x = 6 (post-increment)
int result2 = Interlocked.Increment(ref x); // result2 = 7, x = 7
// int (Int32)
int intCounter = 0;
Interlocked.Increment(ref intCounter);
Interlocked.Decrement(ref intCounter);
// long (Int64)
long longCounter = 0L;
Interlocked.Increment(ref longCounter);
Interlocked.Decrement(ref longCounter);
// uint та ulong — НЕ підтримуються напряму!
// Використовуйте Add з 1/-1 або CompareExchange
uint uintCounter = 0u;
// Interlocked.Increment(ref uintCounter); // ❌ Compile error
// Workaround для uint:
Interlocked.Add(ref uintCounter, 1u); // ✅ Працює (але Add підтримує тільки int/long)
// Для uint потрібен CompareExchange (розглянемо далі)
i++ НЕ АтомарнийЦе одна з найпоширеніших помилок у багатопотоковому програмуванні:
// ❌ НЕБЕЗПЕЧНО у багатопотоковому коді!
int counter = 0;
Parallel.For(0, 10_000, _ =>
{
counter++; // ← НЕ атомарна операція!
});
Console.WriteLine($"Expected: 10000, Actual: {counter}");
// Типовий результат: 8500-9800 (race condition!)
// Чому? Бо counter++ компілюється у ТРИ окремі інструкції:
// 1. Read: temp = counter (завантажити значення з пам'яті у регістр)
// 2. Modify: temp = temp + 1 (інкрементувати у регістрі)
// 3. Write: counter = temp (записати назад у пам'ять)
// Між цими інструкціями інший потік може втрутитись:
// Thread 1: Read (counter=100)
// Thread 2: Read (counter=100) ← Обидва прочитали 100!
// Thread 1: Modify (temp=101)
// Thread 2: Modify (temp=101)
// Thread 1: Write (counter=101)
// Thread 2: Write (counter=101) ← Втрачено один інкремент!
Візуалізація проблеми:
Правильне рішення:
// ✅ ПРАВИЛЬНО:
int counter = 0;
Parallel.For(0, 10_000, _ =>
{
Interlocked.Increment(ref counter); // Атомарна операція
});
Console.WriteLine($"Expected: 10000, Actual: {counter}");
// Завжди: 10000 ✓
using System.Threading;
// Атомарно: location = location + value; return новеЗначення;
int total = 100;
int result = Interlocked.Add(ref total, 50);
Console.WriteLine($"Result: {result}, Total: {total}"); // Result: 150, Total: 150
// Від'ємні значення (атомарне віднімання):
result = Interlocked.Add(ref total, -30);
Console.WriteLine($"Result: {result}, Total: {total}"); // Result: 120, Total: 120
// long (Int64):
long bigTotal = 1_000_000_000L;
Interlocked.Add(ref bigTotal, 500_000_000L);
Console.WriteLine($"BigTotal: {bigTotal}"); // 1500000000
// uint та ulong — НЕ підтримуються!
// uint x = 100;
// Interlocked.Add(ref x, 50); // ❌ Compile error
using System;
using System.Threading;
using System.Threading.Tasks;
public class ThreadSafeAccumulator
{
private long _total = 0;
private int _count = 0;
public void Add(int value)
{
Interlocked.Add(ref _total, value);
Interlocked.Increment(ref _count);
}
public double Average => _count == 0 ? 0 : (double)_total / _count;
public long Total => Interlocked.Read(ref _total); // Atomic read
public int Count => Interlocked.CompareExchange(ref _count, 0, 0); // Atomic read trick
}
// Використання: 1000 потоків додають випадкові числа
var accumulator = new ThreadSafeAccumulator();
Parallel.For(0, 1000, i =>
{
accumulator.Add(Random.Shared.Next(1, 100));
});
Console.WriteLine($"Total: {accumulator.Total}");
Console.WriteLine($"Count: {accumulator.Count}");
Console.WriteLine($"Average: {accumulator.Average:F2}");
Exchange — атомарно замінює значення змінної на нове і повертає попереднє значення. Це еквівалент:
// Неатомарна версія (небезпечна!):
T oldValue = location;
location = newValue;
return oldValue;
// Атомарна версія:
T oldValue = Interlocked.Exchange(ref location, newValue);
using System.Threading;
// ─── Примітивні типи ───────────────────────────────────────────
int value = 42;
int oldValue = Interlocked.Exchange(ref value, 100);
Console.WriteLine($"Old: {oldValue}, New: {value}"); // Old: 42, New: 100
long bigValue = 999L;
long oldBig = Interlocked.Exchange(ref bigValue, 1000L);
float floatValue = 3.14f;
float oldFloat = Interlocked.Exchange(ref floatValue, 2.71f);
double doubleValue = 1.23;
double oldDouble = Interlocked.Exchange(ref doubleValue, 4.56);
// ─── Reference Types ───────────────────────────────────────────
object? obj = new MyClass { Id = 1 };
object? oldObj = Interlocked.Exchange(ref obj, new MyClass { Id = 2 });
// oldObj тепер вказує на MyClass { Id = 1 }
// obj тепер вказує на MyClass { Id = 2 }
// Атомарне обнулення:
string? text = "Hello";
string? oldText = Interlocked.Exchange(ref text, null);
Console.WriteLine($"Old: {oldText}, New: {text}"); // Old: Hello, New: (null)
public class Worker
{
private int _isRunning = 0; // 0 = false, 1 = true
public bool TryStart()
{
// Спроба атомарно змінити 0 → 1
int wasRunning = Interlocked.Exchange(ref _isRunning, 1);
if (wasRunning == 1)
{
Console.WriteLine("Already running!");
return false; // Вже запущений
}
Console.WriteLine("Started!");
return true; // Успішно запустили
}
public void Stop()
{
Interlocked.Exchange(ref _isRunning, 0);
Console.WriteLine("Stopped!");
}
}
// Тест: 10 потоків намагаються запустити одночасно
var worker = new Worker();
Parallel.For(0, 10, i =>
{
bool started = worker.TryStart();
Console.WriteLine($"Thread {i}: {(started ? "SUCCESS" : "FAILED")}");
});
// Результат: тільки ОДИН потік отримає SUCCESS
public class ConfigManager
{
private Config? _currentConfig;
public void UpdateConfig(Config newConfig)
{
Config? oldConfig = Interlocked.Exchange(ref _currentConfig, newConfig);
Console.WriteLine($"Config updated: {oldConfig?.Version} → {newConfig.Version}");
// Cleanup старої конфігурації (якщо потрібно)
(oldConfig as IDisposable)?.Dispose();
}
public Config? GetConfig()
{
// Атомарне читання reference
return Interlocked.CompareExchange(ref _currentConfig, null, null);
}
}
public record Config(string Version, Dictionary<string, string> Settings);
// Використання: кілька потоків читають, один оновлює
var manager = new ConfigManager();
manager.UpdateConfig(new Config("1.0", new()));
// Reader threads
Parallel.For(0, 100, _ =>
{
var config = manager.GetConfig();
Console.WriteLine($"Read config: {config?.Version}");
});
// Writer thread
Task.Run(() =>
{
Thread.Sleep(50);
manager.UpdateConfig(new Config("2.0", new()));
});
CompareExchange — найпотужніший примітив Interlocked. Він реалізує операцію Compare-And-Swap (CAS) — фундамент lock-free програмування.
Логіка CAS:
// Псевдокод (неатомарна версія):
T CompareExchange(ref T location, T newValue, T comparand)
{
T original = location;
if (location == comparand) // Якщо значення досі те що очікували
{
location = newValue; // → Замінити на нове
}
return original; // Повернути оригінальне значення
}
Ключова ідея: "Замінити значення на нове, тільки якщо воно досі дорівнює очікуваному". Це дозволяє виявити чи хтось інший змінив значення між нашим читанням та записом.
using System.Threading;
int value = 10;
int comparand = 10; // Очікуване значення
int newValue = 20; // Нове значення
// Спроба 1: comparand співпадає → заміна відбувається
int original = Interlocked.CompareExchange(ref value, newValue, comparand);
Console.WriteLine($"Original: {original}, Value: {value}");
// Original: 10, Value: 20 ✓
// Спроба 2: comparand НЕ співпадає → заміна НЕ відбувається
value = 10;
original = Interlocked.CompareExchange(ref value, 99, 999);
Console.WriteLine($"Original: {original}, Value: {value}");
// Original: 10, Value: 10 (залишився без змін, бо 10 != 999)
Найважливіший патерн lock-free програмування — CAS loop:
// Задача: атомарно виконати складну операцію (counter = counter * 2 + 1)
// Interlocked.Add не підходить — операція нелінійна
int counter = 10;
int MultiplyAndAddOne(ref int location)
{
int current, newValue;
do
{
current = location; // 1. Читаємо поточне значення
newValue = current * 2 + 1; // 2. Обчислюємо нове (може бути дорога операція)
// 3. Спроба атомарно замінити:
// "Якщо location досі == current (ніхто не змінив) → записати newValue"
}
while (Interlocked.CompareExchange(ref location, newValue, current) != current);
// ↑ Якщо повернене значення != current → хтось змінив між кроками 1-3 → RETRY!
return newValue;
}
int result = MultiplyAndAddOne(ref counter);
Console.WriteLine($"Result: {result}"); // 21 (10 * 2 + 1)
Як це працює:
current = 10)newValue = 10 * 2 + 1 = 21)10 → замінити на 21"
10 (== current) → вихід з циклу ✓Тепер створимо повноцінний thread-safe лічильник без використання lock:
using System;
using System.Threading;
/// <summary>
/// Thread-safe лічильник без використання lock.
/// Всі операції — lock-free через Interlocked.
/// </summary>
public class LockFreeCounter
{
private int _value;
/// <summary>Поточне значення (atomic read).</summary>
public int Value => Interlocked.CompareExchange(ref _value, 0, 0);
// Трюк: CAS з comparand=0 і newValue=0 → нічого не змінює, але повертає поточне значення
/// <summary>Атомарно інкрементувати.</summary>
public int Increment() => Interlocked.Increment(ref _value);
/// <summary>Атомарно декрементувати.</summary>
public int Decrement() => Interlocked.Decrement(ref _value);
/// <summary>Атомарно додати delta.</summary>
public int Add(int delta) => Interlocked.Add(ref _value, delta);
/// <summary>Атомарно встановити значення.</summary>
public int Set(int newValue) => Interlocked.Exchange(ref _value, newValue);
/// <summary>Атомарно встановити значення якщо воно менше за newValue.</summary>
public int SetIfGreater(int newValue)
{
int current;
do
{
current = _value;
if (current >= newValue)
return current; // Вже більше або рівне → не змінюємо
}
while (Interlocked.CompareExchange(ref _value, newValue, current) != current);
return newValue;
}
/// <summary>Атомарно встановити значення якщо воно більше за newValue.</summary>
public int SetIfLess(int newValue)
{
int current;
do
{
current = _value;
if (current <= newValue)
return current;
}
while (Interlocked.CompareExchange(ref _value, newValue, current) != current);
return newValue;
}
/// <summary>Атомарно скинути у 0 і повернути попереднє значення.</summary>
public int Reset() => Interlocked.Exchange(ref _value, 0);
/// <summary>Атомарно виконати довільну операцію.</summary>
public int Transform(Func<int, int> transformation)
{
int current, newValue;
do
{
current = _value;
newValue = transformation(current);
}
while (Interlocked.CompareExchange(ref _value, newValue, current) != current);
return newValue;
}
}
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
const int Iterations = 10_000_000;
// ─── Test 1: Lock-Free Counter ────────────────────────────────
var lockFree = new LockFreeCounter();
var sw = Stopwatch.StartNew();
Parallel.For(0, Iterations, _ => lockFree.Increment());
sw.Stop();
Console.WriteLine($"Lock-Free: {sw.ElapsedMilliseconds}ms, Value={lockFree.Value}");
// ─── Test 2: Lock-Based Counter ───────────────────────────────
int lockBased = 0;
object lockObj = new object();
sw.Restart();
Parallel.For(0, Iterations, _ =>
{
lock (lockObj) { lockBased++; }
});
sw.Stop();
Console.WriteLine($"Lock-Based: {sw.ElapsedMilliseconds}ms, Value={lockBased}");
// ─── Test 3: Interlocked напряму (для порівняння) ─────────────
int direct = 0;
sw.Restart();
Parallel.For(0, Iterations, _ =>
{
Interlocked.Increment(ref direct);
});
sw.Stop();
Console.WriteLine($"Interlocked Direct: {sw.ElapsedMilliseconds}ms, Value={direct}");
// Типові результати (8 cores, високий contention):
// Lock-Free: ~80-120ms ✓
// Lock-Based: ~800-1200ms (в 10x повільніше!)
// Interlocked Direct: ~70-100ms (трохи швидше бо без wrapper)
Treiber Stack — класичний lock-free алгоритм для стеку (1986 рік). Ідея проста: head вказівник оновлюється атомарно через CompareExchange.
Структура:
head → [Node3] → [Node2] → [Node1] → null
(top)
Push операція:
Pop операція:
using System;
using System.Threading;
/// <summary>
/// Lock-free stack (Treiber algorithm, 1986).
/// Push та Pop — lock-free для будь-якої кількості потоків.
/// </summary>
public class LockFreeStack<T>
{
private class Node
{
public T Value;
public Node? Next;
public Node(T value) => Value = value;
}
private Node? _head; // Вершина стеку
/// <summary>Додати елемент на вершину (lock-free).</summary>
public void Push(T item)
{
var newNode = new Node(item);
do
{
newNode.Next = _head; // 1. Новий вузол вказує на поточну вершину
// 2. CAS: якщо _head досі той самий → замінити на newNode
}
while (Interlocked.CompareExchange(ref _head, newNode, newNode.Next) != newNode.Next);
// ↑ Якщо хтось інший змінив _head між кроками 1-2 → retry
}
/// <summary>Забрати елемент з вершини (lock-free). Повертає false якщо стек порожній.</summary>
public bool TryPop(out T? result)
{
Node? current;
do
{
current = _head; // 1. Читаємо поточну вершину
if (current == null) // Стек порожній
{
result = default;
return false;
}
// 2. CAS: якщо _head досі == current → замінити на current.Next
}
while (Interlocked.CompareExchange(ref _head, current.Next, current) != current);
result = current.Value;
return true;
}
/// <summary>Подивитись на вершину без видалення.</summary>
public bool TryPeek(out T? result)
{
Node? current = _head; // Snapshot read (може змінитись одразу після)
if (current == null)
{
result = default;
return false;
}
result = current.Value;
return true;
}
/// <summary>Перевірити чи стек порожній (snapshot).</summary>
public bool IsEmpty => _head == null;
/// <summary>Підрахувати кількість елементів (O(n), snapshot).</summary>
public int Count
{
get
{
int count = 0;
Node? current = _head;
while (current != null)
{
count++;
current = current.Next;
}
return count;
}
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
var stack = new LockFreeStack<int>();
// ─── Test 1: 10 потоків додають по 1000 елементів ────────────
Console.WriteLine("=== Push Test ===");
var sw = System.Diagnostics.Stopwatch.StartNew();
Parallel.For(0, 10, threadId =>
{
for (int i = 0; i < 1000; i++)
{
stack.Push(threadId * 1000 + i);
}
});
sw.Stop();
Console.WriteLine($"Pushed 10000 items in {sw.ElapsedMilliseconds}ms");
Console.WriteLine($"Stack count: {stack.Count}");
// ─── Test 2: 10 потоків забирають елементи ───────────────────
Console.WriteLine("\n=== Pop Test ===");
int totalPopped = 0;
sw.Restart();
Parallel.For(0, 10, _ =>
{
int localCount = 0;
while (stack.TryPop(out _))
{
localCount++;
}
Interlocked.Add(ref totalPopped, localCount);
});
sw.Stop();
Console.WriteLine($"Popped {totalPopped} items in {sw.ElapsedMilliseconds}ms");
Console.WriteLine($"Stack count: {stack.Count}");
Console.WriteLine($"Stack empty: {stack.IsEmpty}");
// Очікуваний результат:
// Pushed 10000 items in ~5-15ms
// Stack count: 10000
// Popped 10000 items in ~5-15ms
// Stack count: 0
// Stack empty: True
Michael-Scott Queue — lock-free черга (1996 рік). Складніша за стек бо потрібно підтримувати два вказівники: head (для dequeue) та tail (для enqueue).
Структура:
head → [Node1] → [Node2] → [Node3] ← tail
(front) (back)
Особливості:
Для навчання почнемо з простішої версії — SPSC (Single-Producer, Single-Consumer) queue:
using System;
using System.Threading;
/// <summary>
/// Lock-free queue для Single-Producer, Single-Consumer сценарію.
/// Використовує circular buffer + atomic indices.
/// </summary>
public class LockFreeSPSCQueue<T>
{
private readonly T[] _buffer;
private readonly int _mask; // Для швидкого modulo через bitwise AND
private int _head = 0; // Producer пише сюди
private int _tail = 0; // Consumer читає звідси
public LockFreeSPSCQueue(int capacity)
{
// Capacity має бути степенем 2 для bitwise modulo
if (capacity <= 0 || (capacity & (capacity - 1)) != 0)
throw new ArgumentException("Capacity must be power of 2");
_buffer = new T[capacity];
_mask = capacity - 1;
}
/// <summary>Додати елемент (викликає тільки Producer thread).</summary>
public bool TryEnqueue(T item)
{
int currentHead = _head;
int nextHead = (currentHead + 1) & _mask; // Wrap around через bitwise AND
// Перевірка чи черга повна
if (nextHead == Volatile.Read(ref _tail))
return false; // Черга повна
_buffer[currentHead] = item;
Volatile.Write(ref _head, nextHead); // Publish нового head
return true;
}
/// <summary>Забрати елемент (викликає тільки Consumer thread).</summary>
public bool TryDequeue(out T? result)
{
int currentTail = _tail;
// Перевірка чи черга порожня
if (currentTail == Volatile.Read(ref _head))
{
result = default;
return false; // Черга порожня
}
result = _buffer[currentTail];
_buffer[currentTail] = default!; // Cleanup для GC
int nextTail = (currentTail + 1) & _mask;
Volatile.Write(ref _tail, nextTail); // Publish нового tail
return true;
}
/// <summary>Кількість елементів (приблизна, snapshot).</summary>
public int Count
{
get
{
int head = Volatile.Read(ref _head);
int tail = Volatile.Read(ref _tail);
return (head - tail) & _mask;
}
}
public bool IsEmpty => _tail == Volatile.Read(ref _head);
public bool IsFull => ((_head + 1) & _mask) == Volatile.Read(ref _tail);
}
using System;
using System.Threading;
using System.Threading.Tasks;
var queue = new LockFreeSPSCQueue<int>(1024); // 1024 = 2^10
var cts = new CancellationTokenSource();
long totalProduced = 0;
long totalConsumed = 0;
// ─── Producer Thread ──────────────────────────────────────────
var producer = Task.Run(() =>
{
int value = 0;
while (!cts.Token.IsCancellationRequested)
{
if (queue.TryEnqueue(value++))
{
Interlocked.Increment(ref totalProduced);
}
else
{
Thread.SpinWait(10); // Черга повна, трохи почекати
}
}
});
// ─── Consumer Thread ──────────────────────────────────────────
var consumer = Task.Run(() =>
{
while (!cts.Token.IsCancellationRequested)
{
if (queue.TryDequeue(out int value))
{
Interlocked.Increment(ref totalConsumed);
// Process value...
}
else
{
Thread.SpinWait(10); // Черга порожня, трохи почекати
}
}
});
// ─── Benchmark: 5 секунд ──────────────────────────────────────
await Task.Delay(5000);
cts.Cancel();
await Task.WhenAll(producer, consumer);
Console.WriteLine($"Produced: {totalProduced:N0}");
Console.WriteLine($"Consumed: {totalConsumed:N0}");
Console.WriteLine($"Throughput: {totalConsumed / 5.0:N0} items/sec");
Console.WriteLine($"Queue count: {queue.Count}");
// Типовий результат:
// Produced: 50,000,000
// Consumed: 49,999,500
// Throughput: 10,000,000 items/sec
// Queue count: 500
На 32-bit системах (x86-32) читання 64-bit значень (long, double) НЕ атомарне:
// На x86-32 (32-bit система):
long value = 0x0123456789ABCDEF;
// Thread 1: Пише нове значення
value = 0xFEDCBA9876543210;
// Це компілюється у ДВІ 32-bit операції:
// 1. Write low 32 bits: 0x76543210
// 2. Write high 32 bits: 0xFEDCBA98
// Thread 2: Читає між цими двома операціями
long read = value;
// Може прочитати: 0xFEDCBA9889ABCDEF (torn read!)
// ^^^^^^^^ нові біти
// ^^^^^^^^ старі біти
Torn Read — часткове зчитування, коли потік бачить частину старого значення та частину нового.
using System.Threading;
long bigNumber = 123456789012345L;
// ❌ Небезпечно на x86-32:
long value1 = bigNumber; // Може бути torn read
// ✅ Безпечно на всіх платформах:
long value2 = Interlocked.Read(ref bigNumber); // Атомарне читання
// На x64 (64-bit система) — Interlocked.Read не потрібен,
// бо long read атомарний апаратно. Але для cross-platform коду — використовуйте.
long/double атомарне апаратно. Interlocked.Read потрібен тільки для сумісності з 32-bit системами або для явної документації намірів.using System;
using System.Threading;
public class LazyInitializer<T> where T : class
{
private T? _instance;
public T GetOrCreate(Func<T> factory)
{
// Швидкий шлях: якщо вже ініціалізовано
T? instance = Volatile.Read(ref _instance);
if (instance != null)
return instance;
// Повільний шлях: ініціалізація
T newInstance = factory();
// Спроба атомарно встановити (якщо ще null)
T? original = Interlocked.CompareExchange(ref _instance, newInstance, null);
if (original == null)
{
// Ми перші — наш екземпляр використовується
return newInstance;
}
else
{
// Хтось інший випередив — використовуємо їхній екземпляр
(newInstance as IDisposable)?.Dispose(); // Cleanup нашого
return original;
}
}
}
// Використання:
var lazy = new LazyInitializer<ExpensiveResource>();
// 100 потоків намагаються ініціалізувати одночасно
Parallel.For(0, 100, _ =>
{
var resource = lazy.GetOrCreate(() =>
{
Console.WriteLine($"[Thread {Environment.CurrentManagedThreadId}] Creating...");
return new ExpensiveResource();
});
Console.WriteLine($"[Thread {Environment.CurrentManagedThreadId}] Got: {resource.Id}");
});
// Результат: тільки ОДИН потік виведе "Creating..."
// Всі інші отримають той самий екземпляр
using System;
using System.Threading;
public class StatsCollector
{
private long _totalRequests = 0;
private long _successfulRequests = 0;
private long _failedRequests = 0;
private long _totalLatencyMs = 0;
private int _minLatencyMs = int.MaxValue;
private int _maxLatencyMs = int.MinValue;
public void RecordSuccess(int latencyMs)
{
Interlocked.Increment(ref _totalRequests);
Interlocked.Increment(ref _successfulRequests);
Interlocked.Add(ref _totalLatencyMs, latencyMs);
UpdateMin(ref _minLatencyMs, latencyMs);
UpdateMax(ref _maxLatencyMs, latencyMs);
}
public void RecordFailure()
{
Interlocked.Increment(ref _totalRequests);
Interlocked.Increment(ref _failedRequests);
}
private void UpdateMin(ref int location, int value)
{
int current;
do
{
current = location;
if (value >= current) return; // Не менше — не оновлюємо
}
while (Interlocked.CompareExchange(ref location, value, current) != current);
}
private void UpdateMax(ref int location, int value)
{
int current;
do
{
current = location;
if (value <= current) return; // Не більше — не оновлюємо
}
while (Interlocked.CompareExchange(ref location, value, current) != current);
}
public void PrintStats()
{
long total = Interlocked.Read(ref _totalRequests);
long success = Interlocked.Read(ref _successfulRequests);
long failed = Interlocked.Read(ref _failedRequests);
long totalLatency = Interlocked.Read(ref _totalLatencyMs);
int min = Interlocked.CompareExchange(ref _minLatencyMs, 0, 0);
int max = Interlocked.CompareExchange(ref _maxLatencyMs, 0, 0);
Console.WriteLine($"Total Requests: {total:N0}");
Console.WriteLine($"Success: {success:N0} ({success * 100.0 / total:F2}%)");
Console.WriteLine($"Failed: {failed:N0} ({failed * 100.0 / total:F2}%)");
if (success > 0)
{
Console.WriteLine($"Avg Latency: {totalLatency / (double)success:F2}ms");
Console.WriteLine($"Min Latency: {min}ms");
Console.WriteLine($"Max Latency: {max}ms");
}
}
}
// Демонстрація: 1000 потоків записують статистику
var stats = new StatsCollector();
Parallel.For(0, 100_000, i =>
{
if (Random.Shared.Next(100) < 95) // 95% success rate
{
int latency = Random.Shared.Next(10, 500);
stats.RecordSuccess(latency);
}
else
{
stats.RecordFailure();
}
});
stats.PrintStats();
using System;
using System.Collections.Generic;
using System.Threading;
public class EventCounter
{
private readonly Dictionary<string, int> _counters = new();
private readonly object _dictionaryLock = new(); // Тільки для додавання нових ключів
public void Increment(string eventName)
{
// Спроба знайти лічильник
if (!_counters.TryGetValue(eventName, out int counterRef))
{
// Лічильника немає — додаємо (потрібен lock для Dictionary)
lock (_dictionaryLock)
{
if (!_counters.ContainsKey(eventName))
{
_counters[eventName] = 0;
}
}
}
// Атомарний інкремент (без lock!)
ref int counter = ref System.Runtime.InteropServices.CollectionsMarshal
.GetValueRefOrNullRef(_counters, eventName);
if (!Unsafe.IsNullRef(ref counter))
{
Interlocked.Increment(ref counter);
}
}
public Dictionary<string, int> GetSnapshot()
{
lock (_dictionaryLock)
{
var snapshot = new Dictionary<string, int>();
foreach (var kvp in _counters)
{
// Атомарне читання
int value = Interlocked.CompareExchange(ref kvp.Value, 0, 0);
snapshot[kvp.Key] = value;
}
return snapshot;
}
}
}
Interlocked.Increment/Decrement
int та longi++ НЕ атомарний — завжди використовуйте Interlocked!Interlocked.Add
int та longuint/ulong використовуйте CompareExchangeInterlocked.Exchange
Interlocked.CompareExchange (CAS)
Реалізуйте 3 варіанти thread-safe counter та порівняйте:
lock statementInterlocked.IncrementInterlocked.CompareExchange з retry loopВимоги:
Очікуваний результат: Interlocked має бути в 5-10x швидше за lock.
Stopwatch.StartNew() для точного вимірювання. Запускайте у Release mode. Для CAS-based варіанту використовуйте pattern: do { current = value; newValue = current + 1; } while (CompareExchange(ref value, newValue, current) != current);Розширте LockFreeStack<T> додавши:
TotalPushes — кількість успішних PushTotalPops — кількість успішних PopTotalRetries — кількість CAS retriesClear() — атомарно очистити стек і повернути кількість видалених елементівTotalPushes == TotalPopsTotalRetries — скільки разів довелось retry через contentionInterlocked.Increment(ref _totalRetries) у циклі do-while перед повторною спробою CAS. Для Clear() використовуйте Interlocked.Exchange(ref _head, null) і пройдіться по linked list підраховуючи елементи.Реалізуйте спрощений LRU (Least Recently Used) cache:
Структура:
public class LockFreeLRUCache<TKey, TValue>
{
private readonly int _capacity;
private readonly ConcurrentDictionary<TKey, Node> _dict;
private Node? _head; // Most recently used
private Node? _tail; // Least recently used
private class Node
{
public TKey Key;
public TValue Value;
public Node? Prev;
public Node? Next;
public long AccessCount; // Для статистики
}
}
Методи:
bool TryGet(TKey key, out TValue value) — отримати значення, перемістити на початокvoid Set(TKey key, TValue value) — додати/оновити, видалити LRU якщо переповненняint Count — поточна кількість елементівВимоги:
Interlocked для оновлення AccessCountConcurrentDictionary для швидкого lookupСпрощення: Можна використати ReaderWriterLockSlim для linked list, але Interlocked для AccessCount.
Тест: 100 потоків × 10_000 операцій (50% Get, 50% Set), capacity=1000. Перевірте що cache не перевищує capacity.
ConcurrentDictionary для lookup (lock-free), ReaderWriterLockSlim для LRU list операцій, Interlocked.Increment для AccessCount. Це дає 90% переваг lock-free з 10% складності.Синхронізація — Interlocked, Volatile та Lock-Free Структури
Атомарні операції, Compare-And-Swap, volatile keyword, memory barriers та lock-free програмування в .NET. Детальний розбір Interlocked класу, SpinLock, SpinWait та реалізація lock-free stack через CAS pattern.
Volatile, Memory Model та Spinning
Детальний розбір volatile keyword, .NET memory model, happens-before relationship, acquire/release semantics, Thread.MemoryBarrier, SpinLock, SpinWait та ABA problem у lock-free структурах.