Процессы и потоки, контекстное переключение

↑ 1.2 ОС и Linux для разработчика · → Следующая

Желательно~4 мин чтенияУровень: junior

Зачем это на собесе

Основа для понимания многопоточности в .NET, ThreadPool, async/await и контейнеров. Вопрос «чем процесс отличается от потока» задают почти всегда.

Подтемы

  • Процесс и IPC
  • Поток
  • Планировщик ОС и context switch
  • Модели параллелизма, конкурентность vs параллелизм
  • CPU-bound и I/O-bound
  • Как это видно в .NET

Объяснение

Процесс

Экземпляр запущенной программы со своим изолированным адресным пространством памяти, дескрипторами файлов и сокетов, переменными окружения. Один контейнер с сервисом ASP.NET Core — обычно один процесс dotnet.

  • Процессы изолированы: падение одного не роняет другой.
  • Общение между процессами (IPC): сокеты, пайпы, общая память, файлы, сигналы.
  • Создание процесса дорогое.

Поток (thread)

Единица выполнения внутри процесса. Потоки процесса разделяют память (кучу), но у каждого свой стек и регистры.

  • Создание и переключение дешевле, чем у процессов, но не бесплатно (~1 МБ стека на поток в .NET по умолчанию).
  • Общая память → гонки данных и необходимость синхронизации.

Планировщик ОС и переключение контекста

Ядро распределяет потоки по ядрам CPU квантами времени. Context switch — сохранение регистров и стека одного потока и загрузка другого: сотни наносекунд–микросекунды плюс «остывание» кэшей CPU. Тысячи активных потоков тратят время на переключения, а не на работу.

Модели параллелизма

  • Один поток на запрос (классика): просто, но не масштабируется на тысячи одновременных ожиданий I/O.
  • Асинхронный I/O + пул потоков (.NET async/await, Node.js event loop): поток не блокируется на ожидании сети или БД, а возвращается в пул. Ядро уведомляет о готовности (epoll в Linux, IOCP в Windows).
  • Конкурентность ≠ параллелизм: конкурентность — много задач «в процессе» одновременно (можно на одном ядре), параллелизм — реально одновременное выполнение на нескольких ядрах.

CPU-bound и I/O-bound

  • I/O-bound (запросы к БД, HTTP, файлы) — большая часть бэкенд-работы. Решение: async/await, чтобы не держать потоки.
  • CPU-bound (генерация PDF, парсинг, шифрование, расчёты аналитики) — нужны реальные потоки и ядра: Parallel, Task.Run в фоне, отдельные воркеры.

Как это видно в .NET

Environment.ProcessId
Environment.ProcessorCount                // сколько ядер видит процесс (в контейнере — с учётом лимитов CPU)
Thread.CurrentThread.ManagedThreadId
ThreadPool.GetMinThreads(out var worker, out var io)

В контейнере .NET учитывает cgroup-лимиты CPU и памяти (размер пула потоков, режим GC).

Нюансы и подводные камни

  • Thread pool starvation: синхронное ожидание (.Result, .Wait(), Thread.Sleep) в обработчиках запросов блокирует потоки пула. Новые запросы ждут, пока пул медленно добавляет потоки (~1–2 в секунду сверх минимума). Типичный симптом — резкий рост латентности под нагрузкой при низкой загрузке CPU.
  • Больше потоков ≠ быстрее: для CPU-bound оптимум около числа ядер.
  • В контейнере с лимитом 0.5 CPU ProcessorCount будет 1, и это влияет на параллелизм.
  • Зомби-процессы и PID 1 в контейнере: процесс с PID 1 должен корректно обрабатывать сигналы (dotnet делает это, shell-обёртки — нет).

Вопросы с ответами

Связанные темы