System Internals Concurrency

Synchronization Primitives

Глибокий розбір примітивів синхронізації в .NET - lock, Monitor, Mutex, Semaphore, AutoResetEvent, Interlocked та Volatile

Synchronization Primitives (Примітиви Синхронізації)

Вступ та Контекст

Проблема: Чому Потрібна Синхронізація?

Уявіть, що ви розробляєте банківську систему. Два потоки одночасно обробляють транзакції для одного рахунку:

// ❌ НЕБЕЗПЕЧНИЙ КОД: Race Condition
public class BankAccount
{
    private decimal _balance = 1000;

    public void Withdraw(decimal amount)
    {
        if (_balance >= amount)           // Thread A: 1000 >= 500? Так!
        {                                  // Thread B: 1000 >= 600? Так!
            Thread.Sleep(1);               // Симуляція затримки
            _balance -= amount;            // Thread A: 1000 - 500 = 500
        }                                  // Thread B: 500 - 600 = -100 ❌
    }
}

Що сталось?

  1. Thread A перевіряє баланс: 1000 >= 500 ✓
  2. Thread B перевіряє баланс: 1000 >= 600 ✓ (ще не змінений!)
  3. Thread A знімає 500: баланс = 500
  4. Thread B знімає 600: баланс = -100 😱

Це класичний приклад Race Condition (стан гонки) — результат залежить від порядку виконання потоків, який недетермінований.

Статистика: За дослідженнями Microsoft, race conditions — одна з трьох найскладніших для діагностики категорій багів (разом з memory leaks та deadlocks). Вони часто з'являються лише під високим навантаженням у production.

Типи Проблем Багатопотоковості

ПроблемаОписНаслідок
Race ConditionКонкуренція за спільний ресурсData corruption, inconsistency
DeadlockВзаємне блокування потоківProgram hang (100% CPU або freeze)
LivelockПотоки активні, але не прогресуютьНескінченний цикл без користі
StarvationПотік не отримує ресурсЗатримки, timeout failures
Loading diagram...
graph TD
    subgraph "Race Condition"
        A1[Thread A: Read X] --> A2[Thread B: Read X]
        A2 --> A3[Thread A: Write X+1]
        A3 --> A4[Thread B: Write X+1]
        A4 --> A5[Expected: X+2<br/>Actual: X+1]
    end

    subgraph "Deadlock"
        B1[Thread A: Lock Resource 1] --> B2[Thread B: Lock Resource 2]
        B2 --> B3[Thread A: Wait for Resource 2]
        B3 --> B4[Thread B: Wait for Resource 1]
        B4 --> B5[Both Blocked Forever]
    end

    style A5 fill:#ef4444,stroke:#dc2626,color:#ffffff
    style B5 fill:#ef4444,stroke:#dc2626,color:#ffffff

Lock Statement: Найпростіший Захист

Базовий Синтаксис

Ключове слово lock — це найпоширеніший спосіб захисту критичних секцій:

public class ThreadSafeCounter
{
    private int _count = 0;
    private readonly object _lock = new object();  // Об'єкт для блокування

    public void Increment()
    {
        lock (_lock)          {
            // Критична секція: тільки один потік одночасно
            _count++;
        }
    }

    public int GetCount()
    {
        lock (_lock)
        {
            return _count;
        }
    }
}

Декомпозиція:

  • Рядок 3: _lock — reference type об'єкт, який слугує "замком"
  • Рядок 8: lock (_lock) — потік намагається "захопити" замок
  • Якщо замок вільний → потік входить у критичну секцію
  • Якщо замок зайнятий → потік блокується до звільнення

Під Капотом: Lock як Monitor

Компілятор перетворює lock у виклики Monitor:

lock (_lock)
{
    _count++;
}
Чому try-finally? Гарантує звільнення lock навіть при винятку. Без цього потік міг би "забути" звільнити замок, заблокувавши інші потоки назавжди.

Правила Вибору Lock Object

Правило✅ Правильно❌ Неправильно
Приватний об'єктprivate readonly object _lock = new();lock(this) — зовнішній код може заблокувати ваш об'єкт
Reference typeobject, class instancelock(5) — value types не працюють
Readonlyreadonly object _lockМожливе перепризначення
Не stringobjectlock("myLock") — string interning може спричинити unexpected sharing
Dedicated objectОкремий об'єкт для кожного ресурсуОдин lock для всього класу
Типова Помилка: lock(this)
public class BadExample
{
    public void DoWork()
    {
        lock (this)  // ❌ ПОГАНО!
        {
            // Зовнішній код може зробити:
            // lock(badExampleInstance) { ... }
            // і заблокувати ваш метод!
        }
    }
}

Monitor Class: Повний Контроль

System.Threading.Monitor надає більше flexibility ніж lock:

Monitor.TryEnter(): Timeout для Lock

using System;
using System.Threading;

public class ResourceManager
{
    private readonly object _resourceLock = new object();

    public bool TryAccessResource(TimeSpan timeout)
    {
        bool lockTaken = false;

        try
        {
            // Спробувати захопити lock з timeout
            Monitor.TryEnter(_resourceLock, timeout, ref lockTaken);  
            if (lockTaken)
            {
                // Успішно отримали lock
                ProcessResource();
                return true;
            }
            else
            {
                // Timeout — не вдалось отримати lock
                Console.WriteLine("Не вдалось отримати доступ до ресурсу");
                return false;
            }
        }
        finally
        {
            if (lockTaken)
            {
                Monitor.Exit(_resourceLock);
            }
        }
    }

    private void ProcessResource()
    {
        Console.WriteLine("Processing resource...");
        Thread.Sleep(1000);
    }
}
Use Case для TryEnter: Коли ви не хочете блокувати потік назавжди — наприклад, у real-time системах або при обробці HTTP requests з timeout.

Monitor.Wait() та Monitor.Pulse(): Сигналізація

Ці методи дозволяють потокам комунікувати через спільний lock:

using System;
using System.Collections.Generic;
using System.Threading;

public class BlockingQueue<T>
{
    private readonly Queue<T> _queue = new Queue<T>();
    private readonly object _lock = new object();

    public void Enqueue(T item)
    {
        lock (_lock)
        {
            _queue.Enqueue(item);
            Monitor.Pulse(_lock);  // Сигналізуємо одному очікуючому потоку        }
    }

    public T Dequeue()
    {
        lock (_lock)
        {
            // Чекаємо, поки черга не буде порожньою
            while (_queue.Count == 0)
            {
                Monitor.Wait(_lock);  // Звільняємо lock та засинаємо                // При пробудженні знову захоплюємо lock
            }

            return _queue.Dequeue();
        }
    }
}

class Program
{
    static BlockingQueue<int> _queue = new BlockingQueue<int>();

    static void Main()
    {
        // Consumer thread
        Thread consumer = new Thread(() =>
        {
            for (int i = 0; i < 5; i++)
            {
                int item = _queue.Dequeue();
                Console.WriteLine($"Consumed: {item}");
            }
        });

        // Producer thread
        Thread producer = new Thread(() =>
        {
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(500);
                _queue.Enqueue(i);
                Console.WriteLine($"Produced: {i}");
            }
        });

        consumer.Start();
        producer.Start();

        consumer.Join();
        producer.Join();
    }
}

Як працює Wait/Pulse:

Loading diagram...
sequenceDiagram
    participant Consumer
    participant Lock
    participant WaitQueue as Wait Queue
    participant Producer

    Consumer->>Lock: Enter lock
    Lock-->>Consumer: Lock acquired
    Consumer->>Consumer: Check: queue empty?
    Consumer->>WaitQueue: Monitor.Wait() - release lock & sleep

    Note over Consumer: Sleeping in wait queue

    Producer->>Lock: Enter lock
    Lock-->>Producer: Lock acquired
    Producer->>Producer: Enqueue item
    Producer->>WaitQueue: Monitor.Pulse()
    WaitQueue-->>Consumer: Wake up (ready queue)
    Producer->>Lock: Exit lock

    Consumer->>Lock: Re-acquire lock
    Lock-->>Consumer: Lock acquired
    Consumer->>Consumer: Dequeue item
    Consumer->>Lock: Exit lock
Важливо: Завжди перевіряйте умову у while, а не в if!
// ❌ ПОГАНО: spurious wakeup може порушити логіку
if (_queue.Count == 0) Monitor.Wait(_lock);

// ✅ ПРАВИЛЬНО: перевірка після кожного пробудження
while (_queue.Count == 0) Monitor.Wait(_lock);

System.Threading.Lock (C# 13)

Новий Тип у .NET 9

C# 13 вводить новий тип System.Threading.Lock, оптимізований для синхронізації:

using System;
using System.Threading;

public class Account
{
    private readonly Lock _balanceLock = new();  // Новий тип Lock    private decimal _balance;

    public Account(decimal initialBalance) => _balance = initialBalance;

    public decimal Debit(decimal amount)
    {
        lock (_balanceLock)  // Компілятор розпізнає Lock тип        {
            if (_balance >= amount)
            {
                _balance -= amount;
                return amount;
            }
            return 0;
        }
    }

    public void Credit(decimal amount)
    {
        lock (_balanceLock)
        {
            _balance += amount;
        }
    }
}

Lock.EnterScope(): Явний API

using System;
using System.Threading;

public class ResourceController
{
    private readonly Lock _lock = new();

    public void Process()
    {
        // EnterScope() повертає ref struct, що автоматично звільняється
        using (Lock.Scope scope = _lock.EnterScope())          {
            // Критична секція
            Console.WriteLine("Processing...");
        }
        // scope.Dispose() автоматично викликає Exit
    }
}

Переваги Lock над object

Аспектobject lockSystem.Threading.Lock
ПродуктивністьMonitor-based (older API)Оптимізований для сучасних CPU
СемантикаНеявна (будь-який object)Явна (dedicated type)
APIЧерез Monitor classВбудовані методи
BoxingНіНі (value-like semantics)
Code clarityМенш очевидноСамодокументований код
Рекомендація: Для нових проєктів на .NET 9+ використовуйте System.Threading.Lock замість object для lock objects.

Mutex: Cross-Process Synchronization

Що таке Mutex?

Mutex (Mutual Exclusion) — примітив синхронізації, що може використовуватись між різними процесами:

Характеристикаlock/MonitorMutex
ScopeОдин процесСистема (kernel-level)
ПродуктивністьШвидше (user-mode)Повільніше (kernel transition)
OwnershipThreadThread (але kernel-tracked)
NamedНіТак (system-wide)

Local Mutex (In-Process)

using System;
using System.Threading;

class Program
{
    private static Mutex _mutex = new Mutex();  // Локальний mutex

    static void Main()
    {
        Thread t1 = new Thread(AccessResource);
        Thread t2 = new Thread(AccessResource);

        t1.Start("Thread 1");
        t2.Start("Thread 2");

        t1.Join();
        t2.Join();
    }

    static void AccessResource(object? name)
    {
        Console.WriteLine($"{name}: Waiting for mutex...");

        _mutex.WaitOne();  // Захопити mutex        try
        {
            Console.WriteLine($"{name}: Entered critical section");
            Thread.Sleep(2000);
        }
        finally
        {
            _mutex.ReleaseMutex();  // Звільнити mutex            Console.WriteLine($"{name}: Released mutex");
        }
    }
}

Named Mutex: Single Instance Application

Класичний use case — забезпечити, що тільки один екземпляр програми запущений:

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        // Унікальне ім'я для mutex (часто GUID)
        const string mutexName = "Global\\MyApp_SingleInstance_12345";

        // CreatedNew = true, якщо mutex створено вперше
        using Mutex mutex = new Mutex(true, mutexName, out bool createdNew);  
        if (!createdNew)
        {
            Console.WriteLine("Інший екземпляр програми вже запущений!");
            return;
        }

        Console.WriteLine("Програма запущена. Натисніть Enter для виходу...");
        Console.ReadLine();
    }
}

Декомпозиція:

  • true (перший аргумент) — спробувати одразу захопити mutex
  • mutexName — системне ім'я (Global\ для всіх сесій, Local\ для поточної)
  • createdNewtrue якщо ми створили mutex, false якщо він вже існував
Важливо: Завжди звільняйте mutex! Якщо процес аварійно завершиться без ReleaseMutex(), система автоматично позначить mutex як abandoned, і наступний WaitOne() кине AbandonedMutexException.

Semaphore: Обмеження Concurrency

Концепція Семафора

Semaphore — дозволяє обмежену кількість потоків одночасно:

Loading diagram...
graph LR
    subgraph "Semaphore(3)"
        A[Slot 1: Thread A]
        B[Slot 2: Thread B]
        C[Slot 3: Thread C]
    end

    D[Thread D: Waiting] --> A
    E[Thread E: Waiting] --> B

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style C fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style D fill:#f59e0b,stroke:#b45309,color:#ffffff
    style E fill:#f59e0b,stroke:#b45309,color:#ffffff

SemaphoreSlim: Lightweight In-Process

using System;
using System.Threading;
using System.Threading.Tasks;

class ConnectionPool
{
    // Максимум 5 одночасних підключень
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5, 5);  
    public async Task<string> QueryDatabaseAsync(string query)
    {
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] Waiting for connection...");

        await _semaphore.WaitAsync();  // Async wait        try
        {
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] Got connection, executing: {query}");
            await Task.Delay(2000);  // Симуляція запиту
            return $"Result for: {query}";
        }
        finally
        {
            _semaphore.Release();  // Звільнити slot            Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] Released connection");
        }
    }
}

class Program
{
    static async Task Main()
    {
        var pool = new ConnectionPool();

        // Запускаємо 10 паралельних запитів
        var tasks = new Task<string>[10];
        for (int i = 0; i < 10; i++)
        {
            tasks[i] = pool.QueryDatabaseAsync($"SELECT * FROM Table{i}");
        }

        await Task.WhenAll(tasks);
    }
}

Параметри конструктора:

  • initialCount — скільки slots доступно на старті
  • maxCount — максимальна кількість (Release не може перевищити)

Порівняння Semaphore та SemaphoreSlim

АспектSemaphoreSemaphoreSlim
ScopeCross-process (named)In-process only
PerformanceKernel mode (slower)User mode (faster)
Async supportНі✅ WaitAsync()
CancellationНі✅ CancellationToken
Use caseIPC, system resourcesApplication-level limiting
Rate Limiting Example:
// Обмеження до 100 requests/second
private readonly SemaphoreSlim _rateLimiter = new SemaphoreSlim(100, 100);

public async Task HandleRequestAsync()
{
    await _rateLimiter.WaitAsync();
    try
    {
        await ProcessRequestAsync();
    }
    finally
    {
        // Повернути slot через 1 секунду
        _ = Task.Delay(1000).ContinueWith(_ => _rateLimiter.Release());
    }
}

AutoResetEvent та ManualResetEvent

Signaling Mechanism

Events — це механізм сигналізації між потоками:

EventПоведінка після SignalАналогія
AutoResetEventАвтоматично скидаєтьсяТурнікет (один пройшов — закрився)
ManualResetEventЗалишається встановленимВорота (відкрились — всі проходять)

AutoResetEvent

using System;
using System.Threading;

class Program
{
    // false = початковий стан "закритий"
    static AutoResetEvent _event = new AutoResetEvent(false);  
    static void Main()
    {
        Thread waiter = new Thread(() =>
        {
            Console.WriteLine("Waiter: Waiting for signal...");
            _event.WaitOne();  // Блокується до сигналу            Console.WriteLine("Waiter: Got signal!");

            // AutoReset: event автоматично скинувся
            Console.WriteLine("Waiter: Waiting again...");
            _event.WaitOne();  // Знову блокується            Console.WriteLine("Waiter: Got second signal!");
        });

        waiter.Start();
        Thread.Sleep(1000);

        Console.WriteLine("Main: Sending first signal");
        _event.Set();  
        Thread.Sleep(1000);

        Console.WriteLine("Main: Sending second signal");
        _event.Set();

        waiter.Join();
    }
}

ManualResetEvent (та ManualResetEventSlim)

using System;
using System.Threading;

class Program
{
    static ManualResetEventSlim _gate = new ManualResetEventSlim(false);  
    static void Main()
    {
        // Запускаємо 5 потоків, що чекають на "ворота"
        for (int i = 0; i < 5; i++)
        {
            int id = i;
            new Thread(() =>
            {
                Console.WriteLine($"Thread {id}: Waiting at gate...");
                _gate.Wait();  // Всі потоки чекають                Console.WriteLine($"Thread {id}: Passed through gate!");
            }).Start();
        }

        Thread.Sleep(2000);
        Console.WriteLine("Main: Opening gate...");
        _gate.Set();  // Всі 5 потоків проходять одночасно!
        Thread.Sleep(1000);
        Console.WriteLine("Main: Closing gate...");
        _gate.Reset();  // Закриваємо ворота    }
}
ManualResetEventSlim — легша альтернатива для in-process використання. Вона використовує spin-waiting для коротких очікувань, що ефективніше за kernel transition.

Use Case: Producer-Consumer з Signaling

using System;
using System.Collections.Concurrent;
using System.Threading;

class ProducerConsumer
{
    private readonly ConcurrentQueue<int> _queue = new();
    private readonly AutoResetEvent _itemAvailable = new(false);
    private volatile bool _running = true;

    public void Producer()
    {
        for (int i = 0; i < 10; i++)
        {
            Thread.Sleep(500);
            _queue.Enqueue(i);
            Console.WriteLine($"Produced: {i}");
            _itemAvailable.Set();  // Сигналізуємо consumer
        }
        _running = false;
        _itemAvailable.Set();  // Розблокувати consumer для завершення
    }

    public void Consumer()
    {
        while (_running || !_queue.IsEmpty)
        {
            _itemAvailable.WaitOne(1000);  // Timeout для перевірки _running

            while (_queue.TryDequeue(out int item))
            {
                Console.WriteLine($"Consumed: {item}");
            }
        }
    }
}

Interlocked Operations: Lock-Free Programming

Атомарні Операції

System.Threading.Interlocked надає атомарні операції, що не потребують lock:

using System;
using System.Threading;

class AtomicCounter
{
    private long _count = 0;

    // ❌ НЕ атомарно (read-modify-write)
    public void IncrementWrong()
    {
        _count++;  // Насправді: temp = _count; temp++; _count = temp;
    }

    // ✅ Атомарно
    public void IncrementRight()
    {
        Interlocked.Increment(ref _count);      }

    public long Value => Interlocked.Read(ref _count);  // Атомарне читання для long
}

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

МетодОписПриклад
Increment(ref int)+1, повертає нове значенняInterlocked.Increment(ref x)
Decrement(ref int)-1, повертає нове значенняInterlocked.Decrement(ref x)
Add(ref int, int)Додає значенняInterlocked.Add(ref x, 5)
Exchange(ref int, int)Заміна, повертає стареold = Interlocked.Exchange(ref x, 10)
CompareExchange(ref, new, comparand)CAS операціяДив. нижче
Read(ref long)Атомарне читання 64-bitInterlocked.Read(ref x)

CompareExchange: Compare-And-Swap (CAS)

CAS — фундаментальна операція для lock-free алгоритмів:

using System;
using System.Threading;

class SpinLock  // Спрощена реалізація
{
    private int _locked = 0;  // 0 = вільний, 1 = зайнятий

    public void Enter()
    {
        // Спробувати замінити 0 на 1
        while (Interlocked.CompareExchange(ref _locked, 1, 0) != 0)          {
            // Якщо не вдалось — хтось інший тримає lock
            Thread.SpinWait(10);  // Коротке очікування
        }
    }

    public void Exit()
    {
        Interlocked.Exchange(ref _locked, 0);
    }
}

Як працює CompareExchange:

CompareExchange(ref location, newValue, comparand):

1. Атомарно читає location
2. Якщо location == comparand:
   - Записує newValue в location
   - Повертає старе значення (comparand)
3. Якщо location != comparand:
   - Нічого не змінює
   - Повертає поточне значення location

Lock-Free Max Update

using System;
using System.Threading;

class ThreadSafeMax
{
    private int _max = int.MinValue;

    public void UpdateIfGreater(int value)
    {
        int current;
        do
        {
            current = _max;
            if (value <= current)
                return;  // Не потрібно оновлювати

        } while (Interlocked.CompareExchange(ref _max, value, current) != current);          // Якщо CAS не вдався — хтось змінив _max, пробуємо знову
    }

    public int Max => _max;
}
Коли використовувати Interlocked:
  • Прості лічильники та статистика
  • Flags та стани
  • Lock-free структури даних (advanced)
Коли НЕ використовувати:
  • Складні операції над кількома змінними
  • Коли потрібен "all-or-nothing" transaction

Volatile Keyword

Memory Visibility Problem

Сучасні процесори та компілятори оптимізують код, що може порушити видимість змін між потоками:

// ❌ МОЖЕ НЕ ПРАЦЮВАТИ без volatile
class Worker
{
    private bool _shouldStop = false;  // Не volatile

    public void DoWork()
    {
        while (!_shouldStop)  // Компілятор може закешувати _shouldStop!
        {
            // Робота...
        }
    }

    public void Stop() => _shouldStop = true;
}

Що може піти не так:

  1. JIT кешує _shouldStop в регістр CPU
  2. Інший потік змінює _shouldStop в пам'яті
  3. Worker thread не бачить зміни — нескінченний цикл!

Volatile: Memory Barrier

class Worker
{
    private volatile bool _shouldStop = false;  
    public void DoWork()
    {
        while (!_shouldStop)  // Завжди читає з пам'яті
        {
            // Робота...
        }
    }

    public void Stop() => _shouldStop = true;  // Негайно видима для інших потоків
}

Що робить volatile:

  • Read: Вставляє acquire barrier — читання не може бути переміщене до нього
  • Write: Вставляє release barrier — запис не може бути переміщений після нього

Volatile Class (Альтернатива)

using System.Threading;

class Counter
{
    private int _count = 0;

    public void Increment()
    {
        // Volatile.Write гарантує видимість
        Volatile.Write(ref _count, Volatile.Read(ref _count) + 1);
    }

    public int Count => Volatile.Read(ref _count);
}
Важливо: volatile НЕ робить операції атомарними!
private volatile int _count = 0;

// ❌ НЕ АТОМАРНО навіть з volatile!
_count++;  // Read + Increment + Write — три операції

// ✅ Для атомарності використовуйте Interlocked
Interlocked.Increment(ref _count);

Коли (Не) Використовувати Volatile

Використовуйте volatileНЕ використовуйте volatile
Прості boolean flags для зупинкиЛічильники, що інкрементуються
Status indicatorsСкладні об'єкти
Одноразові присвоєнняОперації на кількох змінних
Рекомендація: У більшості випадків замість volatile краще використовувати:
  1. lock — для комплексних операцій
  2. Interlocked — для атомарних операцій
  3. Monitor.Wait/Pulse — для сигналізації

Порівняльна Таблиця Примітивів

ПримітивScopePerformanceAsyncUse Case
lock / MonitorIn-process⚡⚡⚡ FastЗагальна синхронізація
System.Threading.LockIn-process⚡⚡⚡ Fast.NET 9+, явний API
MutexCross-process⚡ SlowSingle instance, IPC
SemaphoreCross-process⚡ SlowResource limiting
SemaphoreSlimIn-process⚡⚡⚡ FastConnection pools, rate limiting
AutoResetEventCross-process⚡⚡ MediumPoint-to-point signaling
ManualResetEventSlimIn-process⚡⚡⚡ FastBroadcast signaling
InterlockedN/A⚡⚡⚡⚡ FastestN/AAtomic operations
volatileN/A⚡⚡⚡⚡ FastestN/AVisibility only

Decision Flowchart

Loading diagram...
flowchart TD
    A[Потрібна синхронізація?] -->|Атомарна операція| B[Interlocked]
    A -->|Критична секція| C{Cross-process?}
    C -->|Ні| D[lock / Monitor]
    C -->|Так| E[Mutex]

    A -->|Обмежити concurrency| F{Async needed?}
    F -->|Так| G[SemaphoreSlim]
    F -->|Ні| H{Cross-process?}
    H -->|Так| I[Semaphore]
    H -->|Ні| G

    A -->|Signaling| J{Behavior?}
    J -->|One-at-a-time| K[AutoResetEvent]
    J -->|Broadcast| L[ManualResetEventSlim]

    style B fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style D fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style G fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style L fill:#3b82f6,stroke:#1d4ed8,color:#ffffff

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

Рівень 1: Початковий

Рівень 2: Середній

Рівень 3: Просунутий


Підсумки

У цьому розділі ми розглянули всі основні примітиви синхронізації .NET:

  • lock / Monitor — базова взаємовиключна синхронізація
  • System.Threading.Lock — новий оптимізований тип у C# 13
  • Mutex — cross-process взаємовиключення
  • Semaphore / SemaphoreSlim — обмеження кількості concurrent доступів
  • AutoResetEvent / ManualResetEvent — механізми сигналізації
  • Interlocked — атомарні lock-free операції
  • volatile — забезпечення memory visibility
Наступний крок: Для високорівневого асинхронного програмування вивчіть Task Parallel Library (TPL) та async/await pattern.

Корисні Посилання