Как реализована многопоточность в 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 операций, чтобы эффективно использовать пул потоков.

Ответ 18+ 🔞

Давай разберём эту многоэтажную хрень, которую C# подсовывает для многопоточности. Тут слоёв, как в говне мамонта, но если по полочкам, то всё встаёт на свои места.

1. Потоки операционки (System.Threading.Thread) Это прям голые кости, ручное управление. Создаёшь поток — он сразу хавает свой мегабайт памяти под стек, даже если делать нихуя не будет. Ресурсов жрёт дохуя, как танк на даче.

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);
});

Но тут загвоздка: пул сам решает, когда создавать новые потоки, а когда ждать. Можешь закинуть 100 задач, а он первые 10 будет выполнять, остальные в очереди повесят. Для долгих CPU-bound операций это может быть не очень.

3. Задачи (System.Threading.Tasks.Task) Это уже высший пилотаж, обёртка над пулом, но с мозгом. Task — это не поток, это обещание, что работа когда-нибудь сделается. У неё есть состояние, результат, исключения и возможность подписаться на завершение.

// Кинули в пул, не паримся
Task.Run(() => Console.WriteLine("Выполнено задачей"));

// А тут красота — цепочка действий
Task.Run(() => 42)
    .ContinueWith(prevTask => Console.WriteLine("Результат: " + prevTask.Result));

Главное — Task можно отменять через CancellationToken, ждать с таймаутом и комбинировать как угодно.

4. Асинхронность (async/await) Вот это вообще магия, ёпта. С виду код как синхронный, а по факту — неблокирующий. Ключевой момент: await не блокирует текущий поток. Он говорит: "окей, я тут подожду, а ты, поток, иди делай что-то полезное". Когда операция готова, выполнение продолжится (часто на другом потоке из пула).

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

Этот подход — святое дело для I/O операций: сеть, файлы, базы данных. Поток не висит впустую.

5. Синхронизация, или как не подраться за данные Когда несколько потоков лезут в одну переменную — начинается пиздец. Для этого есть целый арсенал:

  • lock (Monitor) — классика для одного процесса. Всё, что внутри lock, выполняется только одним потоком в момент времени.
  • Mutex — тот же lock, но между процессами. Медленный, как черепаха в смоле.
  • SemaphoreSlim — шлагбаум. Пускает только N потоков одновременно, остальные ждут.
  • ConcurrentCollections — готовые потокобезопасные коллекции. Внутри них уже вся синхронизация спрятана, используй не задумываясь.
  • CancellationToken — цивилизованный способ сказать задаче "всё, отмена, завязывай".

Итоговая мудрость: Не лезь напрямую в Thread, если не понимаешь зачем. Для фоновых вычислений (CPU-bound) — Task.Run. Для операций с диском, сетью или базой (I/O-bound) — async/await. А пул потоков под капотом сам всё оптимизирует, если его не ебашить глупыми задачами на 10 часов. И да, синхронизируй доступ к общим данным, а то будет больно.