Как работает переключение контекста на одноядерной системе
Что проверяют
Базу операционных систем: откуда берётся иллюзия одновременности, что сохраняется при переключении и сколько оно стоит. Для iOS-разработчика следом почти всегда спрашивают про GCD и Swift Concurrency: зачем ограничивать число потоков.
Как отвечать
На одном ядре в каждый момент исполняется один поток, а многозадачность это быстрое чередование:
- поводы: прерывание таймера по истечении кванта (вытеснение), блокирующий системный вызов (ввод-вывод, ожидание мьютекса), пробуждение потока с более высоким приоритетом, добровольная уступка;
- что сохраняется: регистры общего назначения, счётчик команд, указатель стека, флаги, состояние FP/SIMD-регистров. Ядро кладёт их в структуру потока, планировщик выбирает следующий и восстанавливает его состояние;
- поток или процесс: потоки одного процесса делят адресное пространство. При смене процесса меняются таблицы страниц, а теги ASID на ARM64 позволяют не сбрасывать TLB целиком;
- стоимость: прямая часть порядка микросекунд, но дороже косвенная: остывшие кэши, промахи TLB, сброшенный конвейер.
В iOS это ядро XNU с Mach-потоками. Классы QoS (userInteractive, userInitiated, utility, background) превращаются в приоритеты планировщика. GCD при блокирующих задачах плодит потоки, у каждого свой стек (у вторичных 512 КБ), и на сотнях потоков издержки переключений заметны. Swift Concurrency держит кооперативный пул не больше числа ядер, а задачи уступают друг другу на await без переключения потоков ядром. Поэтому блокировать поток внутри Task семафором нельзя.
Где спотыкаются
Говорят, что на одном ядре гонок нет. Поток вытесняется между чтением и записью переменной в любой момент, так что без синхронизации данные портятся и там.
Путают конкурентность и параллелизм: конкурентная очередь GCD на одном ядре не ускорит вычисления, выигрыш будет только на задачах, которые ждут ввод-вывод.
Забывают про инверсию приоритетов. os_unfair_lock поднимает приоритет владельца, а у DispatchSemaphore владельца нет, и поток с userInteractive может надолго застрять в ожидании background.