Конкурентность и многопоточность в C#: Полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Конкурентность и многопоточность в C#: Полное руководство

Современные приложения требуют высокой производительности и отзывчивости. Конкурентность (concurrency) и многопоточность (multithreading) в C# — это ключевые инструменты, позволяющие выполнять несколько задач одновременно, эффективно использовать многоядерные процессоры и улучшать пользовательский опыт. В этой статье мы разберем основные концепции, начиная от классических потоков и заканчивая современным async/await и Parallel LINQ.

Основы многопоточности: класс Thread и пул потоков

Многопоточность в C# начинается с класса System.Threading.Thread. Каждый поток представляет собой отдельный путь выполнения внутри процесса. Однако создание потоков вручную — дорогостоящая операция. Для оптимизации используется пул потоков (ThreadPool), который управляет повторным использованием потоков.

Пример создания потока вручную:

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread thread = new Thread(DoWork);
        thread.Start();
        Console.WriteLine("Главный поток работает...");
        thread.Join(); // Ожидаем завершения
    }

    static void DoWork()
    {
        Console.WriteLine("Поток выполняет работу.");
    }
}

Для более простого использования рекомендуется ThreadPool:

ThreadPool.QueueUserWorkItem(state => {
    Console.WriteLine("Задача в пуле потоков.");
});

Важно: Класс Thread имеет низкоуровневый API и не поддерживает возврат результата. Для современных приложений используйте Task.

Task и TPL (Task Parallel Library)

Библиотека TPL (System.Threading.Tasks) предоставляет высокоуровневую абстракцию — Task. Task представляет асинхронную операцию, которая может возвращать значение. Это основа для современной конкурентности в C#.

Пример Task без возврата значения:

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Task task = Task.Run(() => DoWork());
        await task;
        Console.WriteLine("Завершено.");
    }

    static void DoWork()
    {
        Console.WriteLine("Работа в Task.");
    }
}

Task с возвратом значения:

Task<int> task = Task.Run(() => {
    int result = 0;
    for (int i = 0; i < 1000000; i++) result += i;
    return result;
});
int sum = await task;
Console.WriteLine($"Сумма: {sum}");

TPL также включает Parallel.For и Parallel.ForEach для параллельной обработки коллекций:

Parallel.For(0, 10, i => {
    Console.WriteLine($"Итерация {i} на потоке {Thread.CurrentThread.ManagedThreadId}");
});

Async/await: асинхронное программирование

Ключевые слова async и await упрощают написание асинхронного кода, делая его похожим на синхронный. Они не создают новые потоки, а эффективно переключают контекст выполнения, освобождая поток для других задач.

Пример асинхронного метода:

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string data = await FetchDataAsync("https://example.com");
        Console.WriteLine(data);
    }

    static async Task<string> FetchDataAsync(string url)
    {
        using HttpClient client = new HttpClient();
        string result = await client.GetStringAsync(url);
        return result;
    }
}

Правила использования:

  • Метод с async должен возвращать Task, Task<T> или void (только для обработчиков событий).
  • await можно использовать только внутри async метода.
  • Избегайте блокирующих вызовов (.Result, .Wait()) — они могут вызвать deadlock.

Для параллельного выполнения нескольких асинхронных операций используйте Task.WhenAll:

Task task1 = DoWorkAsync(1);
Task task2 = DoWorkAsync(2);
await Task.WhenAll(task1, task2);

Синхронизация потоков и параллельные коллекции

Когда несколько потоков обращаются к общим данным, возникает состояние гонки (race condition). Для синхронизации используйте lock, Monitor, Mutex или Semaphore.

Пример с lock:

private static readonly object _lock = new object();
private static int _counter = 0;

static void Increment()
{
    lock (_lock)
    {
        _counter++;
        Console.WriteLine($"Счетчик: {_counter}");
    }
}

Для более сложных сценариев используйте ConcurrentDictionary, ConcurrentQueue и BlockingCollection из пространства имен System.Collections.Concurrent. Они обеспечивают потокобезопасность без явных блокировок.

Пример ConcurrentDictionary:

var dict = new ConcurrentDictionary<int, string>();
dict.TryAdd(1, "Value1");
dict.TryAdd(2, "Value2");
string value = dict.GetOrAdd(1, "NewValue"); // Получит "Value1"

Также стоит упомянуть Parallel LINQ (PLINQ), который автоматически распараллеливает запросы LINQ:

var numbers = Enumerable.Range(1, 100);
var evenNumbers = numbers.AsParallel()
                         .Where(n => n % 2 == 0)
                         .ToList();

Заключение

Конкурентность и многопоточность в C# — мощный инструментарий для создания отзывчивых и производительных приложений. Начиная с классических потоков и заканчивая современным async/await и Parallel LINQ, .NET предоставляет все необходимые средства. Главные рекомендации:

  • Для асинхронных операций I/O используйте async/await.
  • Для CPU-интенсивных задач используйте Task и TPL.
  • Избегайте ручного управления потоками — используйте пул потоков или Task.
  • Всегда синхронизируйте доступ к общим данным с помощью lock или потокобезопасных коллекций.

Освоив эти концепции, вы сможете создавать эффективные приложения, полностью использующие возможности современных многоядерных процессоров.

Блоги

Рекомендаций по книгам