System Programming Windows

Синхронізація — Interlocked, Volatile та Lock-Free Структури

Атомарні операції, Compare-And-Swap, volatile keyword, memory barriers та lock-free програмування в .NET. Детальний розбір Interlocked класу, SpinLock, SpinWait та реалізація lock-free stack через CAS pattern.

Синхронізація: Atomic Operations та Lock-Free Programming

Чому Lock-Based Синхронізація Не Завжди Оптимальна

У темі 06 та 07 ми розглянули lock, Monitor, Mutex, SemaphoreSlim — всі вони базуються на блокуванні: якщо ресурс зайнятий, потік переходить у стан очікування (wait state), віддає CPU іншим потокам і чекає поки ОС його розбудить. Це працює чудово для більшості сценаріїв, але має overhead:

  1. Context Switch Cost — перемикання потоку у wait state та назад = ~1-10μs
  2. Kernel Transition — для kernel-mode примітивів (Mutex, Semaphore base) кожна операція = системний виклик
  3. Contention — при високій конкуренції потоки постійно блокуються/розблоковуються

Для дуже коротких критичних секцій (наприклад, інкремент лічильника, swap значення) — overhead блокування може перевищувати саму роботу!

Альтернатива: атомарні операції та lock-free структури — виконують синхронізацію без блокування потоків, використовуючи спеціальні CPU інструкції.


Interlocked: Атомарні Операції на Рівні CPU

Що Таке Атомарність

Атомарна операція — виконується як єдина неподільна дія з точки зору інших потоків. Ніхто не може "побачити" проміжний стан. Для CPU це означає спеціальну інструкцію з префіксом LOCK (x86/x64), яка:

  • Гарантує що операція виконається повністю без переривання
  • Забезпечує memory ordering (інші потоки побачать результат)
  • Працює на рівні cache line (не потребує kernel transition)

System.Threading.Interlocked — клас що надає атомарні операції для примітивних типів.

Основні Методи Interlocked

InterlockedBasics.cs
using System.Threading;

// ─── Increment / Decrement ────────────────────────────────────
int counter = 0;

// Атомарно: counter = counter + 1; return новеЗначення;
int newValue = Interlocked.Increment(ref counter);  // → 1

// Атомарно: counter = counter - 1; return новеЗначення;
newValue = Interlocked.Decrement(ref counter);      // → 0

// ⚠️ Важливо: Increment/Decrement повертають НОВЕ значення (post-increment)
// Це НЕ те саме що counter++ (який повертає старе)

// ─── Add ───────────────────────────────────────────────────────
long total = 100;
Interlocked.Add(ref total, 50);  // total = 150
// Підтримує: int, long, uint, ulong

// ─── Exchange (Atomic Swap) ────────────────────────────────────
int oldValue = 42;
int newVal = Interlocked.Exchange(ref oldValue, 100);
// oldValue тепер = 100, newVal = 42 (повертає попереднє значення)

// Працює з будь-якими reference types:
object? obj = new MyClass();
object? previous = Interlocked.Exchange(ref obj, null);

// ─── CompareExchange (CAS — Compare-And-Swap) ─────────────────
int value = 10;
int comparand = 10;  // очікуване значення
int newValue = 20;   // нове значення

int original = Interlocked.CompareExchange(ref value, newValue, comparand);
// Логіка: if (value == comparand) { value = newValue; } return originalValue;
// original = 10, value тепер = 20

// Якщо comparand НЕ співпадає — нічого не змінюється:
value = 10;
original = Interlocked.CompareExchange(ref value, 99, 999);
// original = 10, value залишився 10 (бо 10 != 999)

// ─── Read (Atomic Read для 64-bit на 32-bit системах) ─────────
long bigNumber = 123456789012345;
long snapshot = Interlocked.Read(ref bigNumber);
// На x64 — не потрібен (long read атомарний), але на x86-32 — критично!

Чому i++ НЕ Атомарний

NonAtomicIncrement.cs
// ❌ НЕБЕЗПЕЧНО у багатопотоковому коді:
int counter = 0;

Parallel.For(0, 1000, _ =>
{
    counter++;  // ← НЕ атомарна операція!
});

Console.WriteLine(counter);  // Очікуємо 1000, отримуємо ~850-990 (race condition)

// Чому? Бо counter++ компілюється у 3 інструкції:
// 1. Read:   temp = counter
// 2. Modify: temp = temp + 1
// 3. Write:  counter = temp
// Між цими інструкціями інший потік може втрутитись!

// ✅ ПРАВИЛЬНО:
Parallel.For(0, 1000, _ =>
{
    Interlocked.Increment(ref counter);  // Атомарна операція
});

Console.WriteLine(counter);  // Завжди 1000
Race Condition — counter++ без синхронізації
$ dotnet run
⚠ Небезпечний код: counter++ без Interlocked
Очікуваний результат: 1000
Фактичний результат: 873 # Race condition!
✓ Безпечний код: Interlocked.Increment
Очікуваний результат: 1000
Фактичний результат: 1000 # Завжди коректно

CompareExchange: Фундамент Lock-Free Програмування

CAS Pattern (Compare-And-Swap Loop)

CompareExchange — найпотужніший примітив Interlocked. Він дозволяє реалізувати будь-яку атомарну операцію через retry loop:

CASPattern.cs
using System.Threading;

// Задача: атомарно виконати складну операцію (наприклад, 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);  // 21 (10 * 2 + 1)

Як це працює:

  1. Читаємо значення (current)
  2. Обчислюємо нове (newValue) — може зайняти час
  3. Спроба CAS: "якщо значення досі current → замінити на newValue"
  4. Якщо хтось інший змінив між кроками 1-3 → CAS поверне старе значення → retry loop
CAS Loop — універсальний патерн для lock-free операцій. Він гарантує що складна операція виконається атомарно, навіть якщо потрібно кілька спроб.

Lock-Free Counter: Повна Реалізація

LockFreeCounter.cs
using System.Threading;

/// <summary>
/// Thread-safe лічильник без використання lock.
/// Всі операції — lock-free через Interlocked.
/// </summary>
public class LockFreeCounter
{
    private int _value;

    public int Value => Interlocked.CompareExchange(ref _value, 0, 0);  // Atomic read
    // Трюк: CAS з comparand=0 і newValue=0 → нічого не змінює, але повертає поточне значення

    public int Increment() => Interlocked.Increment(ref _value);
    public int Decrement() => Interlocked.Decrement(ref _value);
    public int Add(int delta) => Interlocked.Add(ref _value, delta);

    /// <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>Атомарно скинути у 0 і повернути попереднє значення.</summary>
    public int Reset() => Interlocked.Exchange(ref _value, 0);
}

// Benchmark: Lock-Free vs Lock-Based
var lockFree = new LockFreeCounter();
var lockBased = 0;
var lockObj = new object();

var sw = System.Diagnostics.Stopwatch.StartNew();
Parallel.For(0, 1_000_000, _ => lockFree.Increment());
sw.Stop();
Console.WriteLine($"Lock-Free: {sw.ElapsedMilliseconds}ms, Value={lockFree.Value}");

sw.Restart();
Parallel.For(0, 1_000_000, _ => { lock (lockObj) { lockBased++; } });
sw.Stop();
Console.WriteLine($"Lock-Based: {sw.ElapsedMilliseconds}ms, Value={lockBased}");

// Типовий результат (8 cores):
// Lock-Free:  ~50-80ms
// Lock-Based: ~200-400ms (в 3-5 разів повільніше при високій конкуренції)
Benchmark: Lock-Free vs Lock-Based Counter
$ dotnet run --configuration Release
Benchmarking 1,000,000 increments across 8 threads...
[Lock-Free Counter]
Elapsed: 67ms
Final value: 1,000,000
[Lock-Based Counter]
Elapsed: 312ms
Final value: 1,000,000
Performance gain: 4.7x faster with lock-free approach

volatile Keyword: Що Він Робить (і Чого НЕ Робить)

Проблема: Compiler та CPU Reordering

Сучасні компілятори та CPU переставляють інструкції для оптимізації. Це безпечно для однопотокового коду, але ламає багатопотоковий:

VisibilityProblem.cs
// ❌ БЕЗ volatile — може не працювати!
class Worker
{
    private bool _shouldStop = false;  // ← CPU може закешувати у регістрі
    private int _data = 0;

    public void ProducerThread()
    {
        _data = 42;           // 1. Записуємо дані
        _shouldStop = true;   // 2. Сигналізуємо що готово
    }

    public void ConsumerThread()
    {
        while (!_shouldStop)  // ← Може НІКОЛИ не побачити зміну!
        {
            Thread.SpinWait(1);
        }
        Console.WriteLine(_data);  // Може побачити 0 замість 42 (reordering!)
    }
}

// Проблема 1: Compiler може закешувати _shouldStop у регістрі → ніколи не перечитує з RAM
// Проблема 2: CPU може переставити інструкції → Consumer побачить _shouldStop=true ДО _data=42

Що Робить volatile

VolatileKeyword.cs
class Worker
{
    private volatile bool _shouldStop = false;  // ← volatile keyword
    private int _data = 0;

    public void ProducerThread()
    {
        _data = 42;
        _shouldStop = true;  // volatile write: гарантує що _data записаний ДО цього
    }

    public void ConsumerThread()
    {
        while (!_shouldStop) { }  // volatile read: завжди читає з пам'яті, не з кешу
        Console.WriteLine(_data);  // Гарантовано побачить 42
    }
}

// volatile гарантує:
// ✅ Заборона compiler optimization (завжди читати/писати з/у пам'ять)
// ✅ Acquire/Release semantics:
//    - volatile READ = acquire: всі операції ПІСЛЯ нього не можуть бути переставлені ПЕРЕД
//    - volatile WRITE = release: всі операції ПЕРЕД ним не можуть бути переставлені ПІСЛЯ

Що volatile НЕ Робить

Поширена помилка: volatile НЕ робить операції атомарними!
VolatileMisuse.cs
// ❌ НЕБЕЗПЕЧНО — volatile НЕ допомагає тут!
private volatile int _counter = 0;

Parallel.For(0, 1000, _ =>
{
    _counter++;  // ← Досі НЕ атомарна операція! (Read-Modify-Write)
});

Console.WriteLine(_counter);  // Race condition, результат < 1000

// ✅ ПРАВИЛЬНО:
private int _counter = 0;  // volatile не потрібен з Interlocked

Parallel.For(0, 1000, _ =>
{
    Interlocked.Increment(ref _counter);  // Interlocked вже має memory barriers
});

Коли використовувати volatile:

  • ✅ Boolean flags для сигналізації між потоками (read/write, не RMW)
  • ✅ Simple state variables (enum, reference)
  • ❌ Лічильники, складні операції → використовуйте Interlocked або lock

Thread.MemoryBarrier: Повний Контроль

Що Таке Memory Barrier

Memory Barrier (Fence) — інструкція CPU що забороняє reordering навколо себе. Thread.MemoryBarrier() = full fence: всі операції ДО нього завершаться перш ніж почнуться операції ПІСЛЯ.

MemoryBarrier.cs
using System.Threading;

class DataExchange
{
    private int _data;
    private bool _ready;

    public void Writer()
    {
        _data = 42;
        Thread.MemoryBarrier();  // ← Full fence: _data записаний ДО _ready
        _ready = true;
    }

    public int Reader()
    {
        while (!_ready) { }
        Thread.MemoryBarrier();  // ← Full fence: _ready прочитаний ДО _data
        return _data;  // Гарантовано 42
    }
}

// Еквівалент через volatile:
class DataExchangeVolatile
{
    private int _data;
    private volatile bool _ready;  // volatile write/read = implicit barriers

    public void Writer()
    {
        _data = 42;
        _ready = true;  // volatile write = release barrier
    }

    public int Reader()
    {
        while (!_ready) { }  // volatile read = acquire barrier
        return _data;
    }
}
У більшості прикладного коду Thread.MemoryBarrier() не потрібен — volatile, Interlocked, lock вже мають вбудовані barriers. Використовується у низькорівневих бібліотеках та lock-free структурах.

SpinLock та SpinWait: User-Mode Spinning

Коли Spinning Краще За Blocking

Spinning — потік активно чекає у циклі замість переходу у wait state. Це має сенс коли:

  • Критична секція дуже коротка (<100ns)
  • Очікування дуже коротке (lock звільниться за <1μs)
  • Є вільні CPU cores (інакше spinning = марна трата CPU)

Trade-off: Spinning витрачає CPU але уникає context switch (~1-10μs overhead).

SpinLock: Легковаговий Lock

SpinLockBasics.cs
using System.Threading;

// SpinLock — struct (не class!), передавати через ref
private SpinLock _spinLock = new SpinLock(enableThreadOwnerTracking: false);
// enableThreadOwnerTracking=true: додає перевірки (повільніше), для debugging

public void CriticalSection()
{
    bool lockTaken = false;
    try
    {
        _spinLock.Enter(ref lockTaken);  // ← Spinning поки не захопить

        // Критична секція — має бути ДУЖЕ короткою!
        _sharedCounter++;
    }
    finally
    {
        if (lockTaken)
            _spinLock.Exit();  // ← ОБОВ'ЯЗКОВО у finally
    }
}

// TryEnter з timeout:
public bool TryCriticalSection(int timeoutMs)
{
    bool lockTaken = false;
    try
    {
        _spinLock.TryEnter(timeoutMs, ref lockTaken);
        if (!lockTaken) return false;

        _sharedCounter++;
        return true;
    }
    finally
    {
        if (lockTaken) _spinLock.Exit();
    }
}
SpinLock небезпечний при неправильному використанні:
  • ❌ Довга критична секція → марна трата CPU
  • ❌ Вкладені SpinLock → deadlock (не рекурсивний)
  • ❌ Забули Exit() → інші потоки спінять вічно

SpinWait: Adaptive Spinning

SpinWait — "розумний" spinning що адаптується: спочатку швидкий spin, потім Thread.Yield(), потім Thread.Sleep(0).

SpinWaitPattern.cs
using System.Threading;

// Патерн: чекати поки умова стане true
private volatile bool _condition = false;

public void WaitForCondition()
{
    var spinner = new SpinWait();

    while (!_condition)
    {
        spinner.SpinOnce();  // ← Adaptive: spin → yield → sleep
    }
}

// SpinUntil: helper для простих умов
public void WaitForConditionSimple()
{
    SpinWait.SpinUntil(() => _condition);  // Еквівалент циклу вище

    // З timeout:
    bool success = SpinWait.SpinUntil(() => _condition, TimeSpan.FromSeconds(5));
    if (!success) throw new TimeoutException();
}

// Як працює SpinOnce():
// Ітерація 1-10:   Tight loop (CPU-bound spin)
// Ітерація 11-20:  Thread.Yield() (дати шанс іншим потокам на цьому core)
// Ітерація 21+:    Thread.Sleep(0) або Sleep(1) (kernel transition)

Benchmark: lock vs SpinLock

LockBenchmark.cs
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

const int Iterations = 10_000_000;
int counter = 0;

// ─── Test 1: lock (Monitor) ───────────────────────────────────
var lockObj = new object();
var sw = Stopwatch.StartNew();

Parallel.For(0, Iterations, _ =>
{
    lock (lockObj) { counter++; }
});

sw.Stop();
Console.WriteLine($"lock:     {sw.ElapsedMilliseconds}ms, counter={counter}");

// ─── Test 2: SpinLock ─────────────────────────────────────────
counter = 0;
var spinLock = new SpinLock(false);
sw.Restart();

Parallel.For(0, Iterations, _ =>
{
    bool taken = false;
    try
    {
        spinLock.Enter(ref taken);
        counter++;
    }
    finally { if (taken) spinLock.Exit(); }
});

sw.Stop();
Console.WriteLine($"SpinLock: {sw.ElapsedMilliseconds}ms, counter={counter}");

// ─── Test 3: Interlocked (для порівняння) ─────────────────────
counter = 0;
sw.Restart();

Parallel.For(0, Iterations, _ =>
{
    Interlocked.Increment(ref counter);
});

sw.Stop();
Console.WriteLine($"Interlocked: {sw.ElapsedMilliseconds}ms, counter={counter}");

// Типові результати (8 cores, дуже коротка критична секція):
// lock:        ~800-1200ms
// SpinLock:    ~400-600ms   (в 2x швидше)
// Interlocked: ~50-100ms    (в 10x швидше — найкраще для простих операцій)
Benchmark: lock vs SpinLock vs Interlocked
$ dotnet run --configuration Release
Testing 10,000,000 increments with 8 threads...
[lock (Monitor)]
Time: 1,047ms
Counter: 10,000,000
[SpinLock]
Time: 523ms
Counter: 10,000,000
Speedup: 2.0x faster than lock
[Interlocked]
Time: 78ms
Counter: 10,000,000
Speedup: 13.4x faster than lock, 6.7x faster than SpinLock

Lock-Free Stack: Практична Реалізація

Алгоритм Treiber Stack

Treiber Stack — класичний lock-free stack через CAS. Ідея: head вказівник оновлюється атомарно через CompareExchange.

LockFreeStack.cs
using System.Threading;

/// <summary>
/// Lock-free stack (Treiber algorithm).
/// Push та Pop — wait-free для single producer/consumer, lock-free для multi.
/// </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>Перевірити чи стек порожній (snapshot, може змінитись одразу після виклику).</summary>
    public bool IsEmpty => _head == null;  // volatile read
}

// Демонстрація:
var stack = new LockFreeStack<int>();

// 10 потоків додають по 1000 елементів
Parallel.For(0, 10, i =>
{
    for (int j = 0; j < 1000; j++)
        stack.Push(i * 1000 + j);
});

// 10 потоків забирають елементи
int totalPopped = 0;
Parallel.For(0, 10, _ =>
{
    int localCount = 0;
    while (stack.TryPop(out _))
        localCount++;

    Interlocked.Add(ref totalPopped, localCount);
});

Console.WriteLine($"Pushed: 10000, Popped: {totalPopped}");  // Має бути 10000
Lock-Free Stack — Concurrent Push/Pop
$ dotnet run
Testing lock-free stack with 10 threads...
[Phase 1: Pushing]
Thread 0: pushed 1000 items
Thread 1: pushed 1000 items
Thread 2: pushed 1000 items
... 7 more threads ...
Total pushed: 10,000
[Phase 2: Popping]
Thread 0: popped 987 items
Thread 1: popped 1024 items
Thread 2: popped 1011 items
... 7 more threads ...
Total popped: 10,000
✓ Success: All items accounted for, no data loss!

ABA Problem: Підводний Камінь Lock-Free

ABA Problem — класична проблема CAS-based структур:

ABAProblem.cs
// Сценарій:
// 1. Thread A: читає head = Node(A)
// 2. Thread B: Pop(A), Pop(B), Push(A)  ← head знову = Node(A) (але ІНШИЙ об'єкт!)
// 3. Thread A: CAS(head, newNode, A) → SUCCESS (бо адреса співпала)
//    Але структура змінилась між кроками 1-3 → corruption!

// Рішення 1: Version Counter (Tagged Pointer)
private class VersionedNode<T>
{
    public T Value;
    public VersionedNode<T>? Next;
    public long Version;  // ← Інкрементується при кожній зміні
}

// Рішення 2: Hazard Pointers (складно, для production використовуйте бібліотеки)

// Рішення 3: Garbage Collection (C# вирішує автоматично!)
// У C# ABA problem менш критичний бо GC не дозволяє reuse адрес одразу
// Але для критичних систем — використовуйте ConcurrentStack<T> з BCL
Для production коду використовуйте System.Collections.Concurrent.ConcurrentStack<T> — він вже має всі оптимізації та захист від ABA. Власні lock-free структури — тільки для навчання або специфічних потреб.

Підсумок

Interlocked

  • Атомарні операції: Increment, Add, Exchange, CompareExchange
  • CPU-level atomicity через LOCK prefix (x86/x64)
  • CompareExchange (CAS) — фундамент lock-free програмування
  • Швидше за lock для простих операцій (~10-50x)

volatile

  • Заборона compiler caching та reordering
  • Acquire/Release semantics для memory ordering
  • ❌ НЕ робить операції атомарними (i++ досі небезпечний)
  • Використання: boolean flags, simple state variables

SpinLock / SpinWait

  • User-mode spinning замість kernel blocking
  • Краще за lock для ДУЖЕ коротких критичних секцій (<100ns)
  • SpinWait — adaptive: spin → yield → sleep
  • ⚠️ Марна трата CPU якщо очікування довге

Lock-Free Structures

  • CAS loop pattern: read → compute → CompareExchange → retry
  • Treiber Stack — класичний lock-free алгоритм
  • ABA Problem — підводний камінь (версіонування або GC)
  • Production: використовуйте System.Collections.Concurrent.*

Практичні Завдання

Рівень 1: Atomic Counter Benchmarks

Реалізуйте 4 варіанти thread-safe counter та порівняйте:

  1. lock statement
  2. Interlocked.Increment
  3. SpinLock
  4. volatile int + Interlocked.CompareExchange (CAS loop)

Benchmark: 10 потоків × 1_000_000 інкрементів кожен. Виміряйте час та переконайтесь що всі дають результат 10_000_000.

Підказка: Використовуйте Stopwatch для вимірювання часу. Запускайте у Release mode (dotnet run -c Release) для реалістичних результатів. Очікуваний порядок швидкості: Interlocked > SpinLock > CAS loop > lock.

Рівень 2: Lock-Free Queue (Single-Producer, Single-Consumer)

Реалізуйте LockFreeSPSCQueue<T> (Single-Producer Single-Consumer):

  1. Circular buffer фіксованого розміру (масив + head/tail індекси)
  2. TryEnqueue(T item) — додати якщо є місце
  3. TryDequeue(out T item) — забрати якщо є елементи
  4. Використовуйте Interlocked для head/tail, volatile для синхронізації
  5. Тест: Producer генерує 1_000_000 чисел, Consumer їх споживає — сума має співпадати
Підказка: Для SPSC достатньо volatile для індексів (не потрібен CAS). Producer пише тільки _tail, Consumer читає тільки _head. Використовуйте (index + 1) % capacity для циклічності.

Рівень 3: Benchmark — lock vs Interlocked vs SpinLock

Створіть benchmark suite що порівнює 3 підходи для різних сценаріїв:

  1. Дуже коротка критична секція (1 інкремент)
  2. Коротка (10 операцій)
  3. Середня (100 операцій + Array access)
  4. Довга (1000 операцій + I/O simulation через Thread.SpinWait)

Виміряйте throughput (ops/sec) та CPU usage для кожного. Зробіть висновок: коли який підхід оптимальний залежно від довжини критичної секції та рівня конкуренції.

Підказка: Очікувані результати: для дуже коротких секцій Interlocked/SpinLock виграють, для довгих — lock краще (менше CPU waste). Використовуйте Process.GetCurrentProcess().TotalProcessorTime для вимірювання CPU usage.