Как реализована многопоточность в C#?

«Как реализована многопоточность в C#?» — вопрос из категории Многопоточность, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

C# предоставляет многоуровневую модель многопоточности, от низкоуровневых потоков ОС до высокоуровневых абстракций.

1. Классические потоки (System.Threading.Thread) Прямое создание и управление потоками операционной системы. Ресурсоёмко, так как каждый поток имеет свой стек (обычно 1 МБ).

var thread = new Thread(() =>
{
    Thread.Sleep(1000);
    Console.WriteLine("Работа из потока: " + Thread.CurrentThread.ManagedThreadId);
});
thread.IsBackground = true; // Фоновый поток не помешает завершению приложения
thread.Start();

2. Пул потоков (System.Threading.ThreadPool) Ключевой компонент для эффективного управления потоками. CLR создаёт и переиспользует набор рабочих потоков, избегая накладных расходов на их создание/уничтожение.

ThreadPool.QueueUserWorkItem(state =>
{
    // Эта работа будет выполнена одним из потоков пула
    Console.WriteLine("Пул потоков, ID: " + Thread.CurrentThread.ManagedThreadId);
});

3. Задачи (System.Threading.Tasks.Task) Рекомендуемая высокоуровневая абстракция. Task представляет собой асинхронную операцию, которая обычно выполняется в потоке из пула.

// Запуск фоновой задачи
Task.Run(() => Console.WriteLine("Выполнено задачей"));

// Задачи с продолжением (continuation)
Task.Run(() => 42)
    .ContinueWith(prevTask => Console.WriteLine("Результат: " + prevTask.Result));

4. Асинхронное программирование (async/await) Модель, позволяющая писать неблокирующий код, который выглядит как синхронный. Ключевое отличие — await не блокирует текущий поток, а возвращает управление вызывающему коду.

public async Task<string> DownloadDataAsync(string url)
{
    using var httpClient = new HttpClient();
    // При await поток не блокируется. Когда ответ готов,
    // выполнение продолжается (возможно, в другом потоке из пула).
    string data = await httpClient.GetStringAsync(url);
    return data.ToUpper();
}

5. Средства синхронизации Для безопасного доступа к общим данным используются:

  • lock (Monitor) — для взаимного исключения в рамках одного процесса.
  • Mutex — для синхронизации между процессами.
  • Semaphore/SemaphoreSlim — для ограничения числа одновременных доступов к ресурсу.
  • ConcurrentCollections (ConcurrentDictionary, ConcurrentQueue) — потокобезопасные коллекции.
  • CancellationToken — для кооперативной отмены операций.

Практическое правило: Используйте Task и async/await для операций ввода-вывода (I/O-bound) и Task.Run для распараллеливания CPU-bound операций, чтобы эффективно использовать пул потоков.