Процессы и потоки, контекстное переключение
↑ 1.2 ОС и Linux для разработчика · → Следующая
Зачем это на собесе
Основа для понимания многопоточности в .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-обёртки — нет).
Вопросы с ответами
Чем процесс отличается от потока?
Процесс — изолированная программа со своей памятью и ресурсами. Поток — единица выполнения внутри процесса. Потоки делят память процесса, но имеют свои стеки. Процессы тяжелее и изолированнее, потоки легче, но требуют синхронизации.
Что такое context switch и почему он дорогой?
Переключение CPU с одного потока на другой: сохранение и восстановление регистров и стека, сброс кэшей и TLB. При тысячах потоков CPU тратит заметную долю времени на переключения.
Конкурентность vs параллелизм?
Конкурентность — управление многими задачами, которые чередуются (может быть на одном ядре). Параллелизм — одновременное выполнение на нескольких ядрах.
Почему async/await повышает пропускную способность сервера?
Во время ожидания I/O поток не блокируется, а возвращается в пул и обслуживает другие запросы. Для той же нагрузки нужно гораздо меньше потоков.
Что такое thread pool starvation и как его обнаружить?
Нехватка свободных потоков пула из-за блокирующих вызовов. Видно по росту очереди пула (dotnet-counters: threadpool-queue-length, threadpool-thread-count) и латентности при низкой загрузке CPU.
Связанные темы
- Следующая: Память: стек, куча, виртуальная память
- Thread и ThreadPool: Thread и ThreadPool
- async/await изнутри: async∕await изнутри: state machine, SynchronizationContext, ConfigureAwait
- Event loop в Node.js: Event loop в Node.js и libuv
- Контейнеры, namespaces, cgroups: 2.1 Контейнеры и виртуальные машины — namespaces, cgroups