Что такое конкурентность (concurrency) в программировании?

Ответ

Конкурентность — это свойство системы, при котором несколько задач (потоков, процессов, корутин) выполняются в перекрывающиеся промежутки времени, создавая иллюзию параллельного выполнения. Ключевой момент — управление выполнением нескольких задач в условиях, когда для их реального параллельного выполнения может не хватать физических ресурсов (ядер CPU).

Конкурентность vs. Параллелизм:

  • Конкурентность — это дизайн системы, структура программы, способная работать над несколькими задачами одновременно. Она возможна даже на одном ядре CPU за счет переключения контекста.
  • Параллелизм — это реальное одновременное выполнение задач на нескольких ядрах или процессорах. Параллелизм — это один из способов реализации конкурентности.

Пример конкурентности на одном ядре (асинхронный код C#):

public async Task ServeMultipleClientsAsync()
{
    // Запускаем три "долгих" I/O задачи конкурентно (например, запросы к БД или API).
    // Они не выполняют вычисления на CPU, а ждут ответа от внешних ресурсов.
    Task<int> dbQueryTask = QueryDatabaseAsync();
    Task<string> apiCallTask = CallExternalApiAsync();
    Task<FileStream> fileReadTask = ReadLargeFileAsync();

    // Ожидаем завершения всех задач. Пока одна задача "ждет", поток может быть
    // использован для обработки других задач или запросов.
    await Task.WhenAll(dbQueryTask, apiCallTask, fileReadTask);

    Console.WriteLine($"Results: {dbQueryTask.Result}, {apiCallTask.Result}");
}

private async Task<int> QueryDatabaseAsync()
{
    await Task.Delay(1000); // Имитация долгого I/O-операции
    return 42;
}

Основные проблемы конкурентности и их решения:

  1. Состояние гонки (Race Condition): Неопределенный результат из-за порядка выполнения потоков.
    • Решение: Использование блокировок (lock, Mutex), потокобезопасных коллекций, неизменяемых (immutable) структур данных.
  2. Взаимная блокировка (Deadlock): Два или более потока бесконечно ждут друг друга.
    • Решение: Упорядочивание захвата блокировок, использование Monitor.TryEnter с таймаутом, CancellationToken.
  3. Голодание (Starvation): Поток не может получить доступ к общему ресурсу.
    • Решение: Справедливые (fair) примитивы синхронизации, правильный дизайн приоритетов.

Модели реализации конкурентности в .NET:

  • Многопоточность (Threads): Низкоуровневая модель. ThreadPool для эффективного управления потоками.
  • Асинхронное программирование (async/await): Модель, идеально подходящая для I/O-операций, позволяющая освобождать потоки во время ожидания.
  • Параллельные циклы (Parallel.For, PLINQ): Для распараллеливания CPU-связанных задач.
  • Каналы (Channels, System.Threading.Channels): Для реализации шаблона Producer-Consumer с высокой пропускной способностью.

Ответ 18+ 🔞

Смотри, объясню на пальцах, а то ты, я смотрю, уже глаза стеклянные делаешь. Конкурентность — это когда твоя программа делает вид, что она может жонглировать пятью мячами сразу, хотя на самом деле у неё одна рука. Она быстро-быстро перекидывает мячики, и со стороны кажется, что они все в воздухе. Иллюзия, блядь, но какая!

Конкурентность и Параллелизм: в чём разница, ёпта?

  • Конкурентность — это план, чертёж. Ты спроектировал бар так, чтобы три бухающих мужика могли одновременно орать на бармена, толкаться и заказывать. Это дизайн системы, готовой к такому пиздецу.
  • Параллелизм — это реальность. Это когда в баре на самом деле три реальных, живых бармена, и каждый ебёт мозг своему клиенту одновременно. Параллелизм — это если у тебя много ядер в процессоре, и они реально в разные стороны пашут.

Вот тебе пример, как это выглядит в коде на одном ядре (C# async):

public async Task ServeMultipleClientsAsync()
{
    // Запускаем три долбанные задачи, которые в основном тупо ждут
    Task<int> dbQueryTask = QueryDatabaseAsync(); // Ждёт ответа от базы
    Task<string> apiCallTask = CallExternalApiAsync(); // Тянется за данными в интернет
    Task<FileStream> fileReadTask = ReadLargeFileAsync(); // Читает файл с диска

    // Сидим ждём, пока все эти тормоза отработают. Пока одна ждёт, поток не тупит, а может другую задачу подхватить.
    await Task.WhenAll(dbQueryTask, apiCallTask, fileReadTask);

    Console.WriteLine($"Results: {dbQueryTask.Result}, {apiCallTask.Result}");
}

private async Task<int> QueryDatabaseAsync()
{
    await Task.Delay(1000); // Представь, что это запрос к базе, который идёт хуёво долго
    return 42; // Ответ на главный вопрос жизни, вселенной и всего такого
}

Суть в чём? Пока код ждёт ответа от базы (это I/O операция, а не вычисления), он не занимает поток попусту. Он говорит: "Окей, я тут посплю, разбуди, как придёт ответ", а освободившийся поток в это время может чайник вскипятить, то есть другую полезную работу сделать. Это и есть магия async/await.

А теперь про проблемы, потому что без них никуда, пиздец:

  1. Состояние гонки (Race Condition): Представь, что два потока лезут в один и тот же общий кошелёк (общую переменную) за деньгами. Оба видят, что там 100 рублей. Оба хотят снять 100. И оба, сука, снимают. В итоге на счету -100 рублей, а оба потока думают, что они молодцы. Хаос, а не жизнь.
    • Что делать: Ставить замки (lock), использовать специальные потокобезопасные штуки или делать данные неизменяемыми — чтобы их можно было только читать, но не портить.
  2. Взаимная блокировка (Deadlock): Классика. Два потока обнимаются на смерть. Первый поток держит замок А и хочет получить замок Б. Второй поток держит замок Б и хочет получить замок А. И стоят они так до скончания времён, оба упёрлись рогом. Красота.
    • Что делать: Захватывать замки всегда в одном и том же порядке. Или ставить таймауты — "попробую взять замок 50 мс, а если не выйдет — пойду другим путём, не буду тут как лох торчать".
  3. Голодание (Starvation): Как самый мелкий и застенчивый мужик в баре. Он всё тянет руку, чтобы заказать, но бармен постоянно обслуживает тех, кто громче орёт или у кого купюры хрустят больше. Бедолага так и умрёт у стойки с пустым стаканом.
    • Что делать: Использовать справедливые механизмы, чтобы у каждого потока был шанс, и не давать одним задачам жрать все ресурсы.

Чем это всё делают в .NET:

  • Потоки (Threads): Старая, добрая, но грубая сила. Сами по себе — тяжёлые. Умные люди используют ThreadPool, чтобы не создавать их на каждый чих, а брать из пула, как инструмент.
  • Async/Await: Любовь моя. Для всего, что много ждёт (сеть, диск, база данных). Не блокирует потоки, экономит ресурсы.
  • Параллельные циклы (Parallel.For, PLINQ): Для задач, где надо реально нагрузить процессор — перемножить огромные матрицы, обработать кучу изображений. Дробит работу на куски и пихает на все доступные ядра.
  • Каналы (Channels): Это как конвейер на заводе. Одна часть кода (производитель) кидает в трубу данные, другая часть (потребитель) вынимает с другого конца. Очень эффективно для обработки потоков данных, когда один пишет, несколько читают, и всё летит с бешеной скоростью.

Короче, конкурентность — это не про то, чтобы всё делать одновременно. Это про то, чтобы делать много чего, не простаивая как идиот в ожидании. Главное — не нагородить таких костылей, что всё встанет колом или начнёт выдавать результаты, от которых сам потом офигеешь.