Ответ
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 часов. И да, синхронизируй доступ к общим данным, а то будет больно.