Конкурентность и GIL
GIL, потоки, процессы и асинхронность в Python.
17 вопросов
JuniorТеорияОчень частоЧто такое GIL в CPython?
Что такое GIL в CPython?
GIL (Global Interpreter Lock) — мьютекс, позволяющий лишь одному потоку выполнять байткод Python одновременно и защищающий внутренности интерпретатора, например счётчики ссылок. Отпускается на I/O.
Типичные ошибки
- ✗Думать, что
GILблокирует каждый объект отдельно, а не весь интерпретатор - ✗Считать, что
GILделает общие данные потокобезопасными и блокировки не нужны - ✗Предполагать, что
GILмешаетmultiprocessingработать параллельно на ядрах
Уточняющие вопросы
- →Как
sys.setswitchintervalменяет частоту освобожденияGIL? - →Какие операции позволяют потоку отпустить
GILво время работы?
MiddleТеорияОчень частоКак работают async/await и событийный цикл?
Как работают async/await и событийный цикл?
async def определяет корутину; await приостанавливает её до завершения awaitable, отдавая управление однопоточному событийному циклу, который тем временем запускает другие готовые корутины. Без лишних потоков.
Типичные ошибки
- ✗Думать, что
awaitблокирует всю программу, а не уступает циклу - ✗Считать, что каждая корутина работает на своём потоке ОС
- ✗Полагать, что событийный цикл вытесняет корутины по таймеру, а не на
await
Уточняющие вопросы
- →Как
asyncio.gatherпозволяет несколькимawaitпересекаться, а не идти подряд? - →Какие типы awaitable можно законно поставить после ключевого слова
await?
JuniorТеорияЧастоЧто такое корутина?
Что такое корутина?
Корутина обобщает подпрограмму: у неё несколько точек входа, она приостанавливается и возобновляется, сохраняя состояние. В Python их пишут через async def/await (или yield), давая кооперативность.
Типичные ошибки
- ✗Приравнивать корутину к потоку ОС, который вытесняет планировщик
- ✗Считать, что корутины работают параллельно на ядрах, а не кооперативно
- ✗Думать, что корутина теряет локальное состояние при приостановке на
await
Уточняющие вопросы
- →Чем корутина на генераторе с
yieldотличается от корутины наasync def? - →Какой объект возвращает вызов
async defфункции до еёawait?
JuniorТеорияЧастоЯвляются ли потоки Python настоящими потоками ОС?
Являются ли потоки Python настоящими потоками ОС?
Да — модуль threading создаёт нативные потоки ОС (POSIX/Windows), которые планирует операционная система, а не зелёные потоки уровня интерпретатора. GIL всё равно сериализует исполнение их байткода.
Типичные ошибки
- ✗Называть потоки Python зелёными, хотя
threadingиспользует нативные потоки ОС - ✗Путать
threading.Threadс отдельным процессом с изолированной памятью - ✗Считать, что выбирает поток интерпретатор, а не планировщик ОС
Уточняющие вопросы
- →Если это настоящие потоки ОС, почему они не распараллеливают CPU-нагрузку?
- →Как планировщик ОС взаимодействует с
GILпри переключении потока?
JuniorТеорияЧастоЧем потоки и процессы различаются по памяти?
Чем потоки и процессы различаются по памяти?
Потоки делят одно адресное пространство — ту же память, переменные и модули — поэтому общаются дёшево, но требуют синхронизации. Процессы независимы, имеют раздельные пространства и используют IPC.
Типичные ошибки
- ✗Менять роли местами — считать потоки изолированными, а процессы делящими память
- ✗Думать, что процессы делят глобальные переменные так же, как потоки одного процесса
- ✗Считать, что потокам нужен IPC вроде каналов, а не общая память и блокировки
Уточняющие вопросы
- →Почему потокам с общей памятью всё равно нужен
Lockвокруг изменяемого состояния? - →Что
os.forkкопирует, а что разделяет при запуске дочернего процесса?
MiddleКодЧастоЗапустите три I/O-задачи конкурентно через asyncio
Запустите три I/O-задачи конкурентно через asyncio
Используйте asyncio.gather для конкурентного запуска корутин на одном потоке: await asyncio.gather(fetch(1), fetch(2), fetch(3)). await asyncio.sleep уступает управление, поэтому все три перекрываются и завершаются за ~1с, а не 3с. Блокирующий time.sleep застопорил бы цикл.
Типичные ошибки
- ✗Ожидать каждую корутину последовательно, что сериализует их в ~3с
- ✗Использовать блокирующий
time.sleepвнутри корутины и стопорить цикл - ✗Вызывать
asyncio.runна каждую корутину вместо одногоgather
Уточняющие вопросы
- →Почему блокирующий вызов вроде
time.sleepломает конкурентностьasyncio? - →Чем
asyncio.gatherотличается отasyncio.as_completed?
MiddleКодЧастоПочему этот многопоточный CPU-цикл не ускоряется?
Почему этот многопоточный CPU-цикл не ускоряется?
Два потока не быстрее последовательного запуска — часто медленнее, из-за борьбы за GIL — потому что count это чистая Python-нагрузка на CPU, а GIL пускает лишь один поток исполнять байткод одновременно. Чтобы распараллелить, используйте multiprocessing.Process или ProcessPoolExecutor.
Типичные ошибки
- ✗Считать, что потоки дают CPU-параллелизм в CPython
- ✗Думать, что GIL влияет лишь на I/O, но не на CPU-циклы
- ✗Ожидать, что более длинный цикл преодолеет GIL
Уточняющие вопросы
- →Почему GIL не вредит I/O-нагруженному многопоточному коду так же?
- →Как
multiprocessingдостигает реального CPU-параллелизма, недоступного потокам?
MiddleТеорияЧастоКакие задачи выигрывают от потоков — I/O-bound или CPU-bound?
Какие задачи выигрывают от потоков — I/O-bound или CPU-bound?
Выигрывают I/O-bound задачи: поток отпускает GIL пока ждёт сеть или диск, давая другим работать, и конкурентность скрывает задержки. CPU-bound задачи нет — GIL сериализует их.
Типичные ошибки
- ✗Ожидать, что потоки распараллелят CPU-bound циклы по ядрам
- ✗Делать вывод, что потоки бесполезны везде из-за
GIL - ✗Думать, что поток держит
GIL, пока заблокирован на I/O
Уточняющие вопросы
- →Как
multiprocessingизменит ответ для CPU-bound работы? - →Почему
asyncioможет заменить потоки для множества I/O-ожиданий?
MiddleТеорияЧастоЗачем использовать multiprocessing для CPU-bound работы?
Зачем использовать multiprocessing для CPU-bound работы?
У каждого процесса свой интерпретатор и свой GIL, поэтому процессы исполняют байткод Python в настоящем параллелизме по ядрам, обходя сериализацию единым GIL, что тормозит потоки на CPU-bound.
Типичные ошибки
- ✗Считать, что рабочие процессы делят один глобальный
GIL, как потоки - ✗Воспринимать
multiprocessingкак простоthreadingс удобным синтаксисом - ✗Думать, что оно помогает только I/O-bound, а не CPU-bound работе
Уточняющие вопросы
- →Почему аргументы и возвращаемые значения должны быть picklable через границу процесса?
- →Когда накладные расходы на запуск и IPC перевешивают выигрыш от параллелизма?
MiddleДебаггингЧастоПочему этот многопоточный счётчик неверен и как его исправить?
Почему этот многопоточный счётчик неверен и как его исправить?
Гонка данных: counter += 1 — это «чтение-изменение-запись», не атомарная на уровне байткода, поэтому потоки чередуются и теряют обновления; GIL гарантирует атомарность байткода, но не многошаговых операций. Решение: защитить инкремент через threading.Lock.
Типичные ошибки
- ✗Считать, что GIL делает
+=атомарным между потоками - ✗Винить тайминг print, а не потерянные обновления
- ✗Думать, что один
globalделает инкремент потокобезопасным
Уточняющие вопросы
- →Почему GIL гарантирует атомарность байткода, но не атомарность оператора?
- →Когда
itertools.countилиqueue.Queueчище решения с блокировкой?
MiddleДебаггингИногдаПочему эти две блокировки могут привести к дедлоку?
Почему эти две блокировки могут привести к дедлоку?
Инверсия порядка блокировок: t1 берёт a, затем b, а t2 — b, затем a. Если каждый захватит свою первую блокировку одновременно, второй не достанется никому → дедлок. Решение: захватывать блокировки в едином глобальном порядке или использовать таймаут (lock.acquire(timeout=...)).
Типичные ошибки
- ✗Считать, что короткие критические секции не могут привести к дедлоку
- ✗Путать это с повторным захватом одной нереентерабельной блокировки
- ✗Винить GIL, а не инвертированный порядок блокировок
Уточняющие вопросы
- →Почему единый согласованный глобальный порядок блокировок предотвращает дедлок?
- →Как захват с таймаутом позволяет потоку выйти из возможного дедлока?
MiddleТеорияИногдаЧто такое greenlets / зелёные потоки?
Что такое greenlets / зелёные потоки?
Лёгкие потоки в пространстве пользователя, которые планирует среда или библиотека (greenlet, gevent), невидимые для ОС — для неё процесс выглядит однопоточным. Библиотека переключает их кооперативно.
Типичные ошибки
- ✗Называть greenlets потоками ОС, которые планирует ядро
- ✗Считать, что greenlets обходят
GILради параллельной CPU-работы - ✗Думать, что планировщик ОС вытесняет greenlets, а не кооперативное переключение
Уточняющие вопросы
- →Как
geventпатчит блокирующие вызовы, чтобы уступать на I/O-точках? - →Что станет с другими greenlets, если один крутит CPU-цикл без уступок?
MiddleКодИногдаРаспараллельте CPU-задачу через пул процессов
Распараллельте CPU-задачу через пул процессов
Используйте ProcessPoolExecutor, чтобы обойти GIL: with ProcessPoolExecutor() as pool: results = list(pool.map(square, range(10))). У каждого процесса свой интерпретатор и GIL, поэтому работа идёт по-настоящему параллельно. Функция и аргументы должны быть picklable.
Типичные ошибки
- ✗Использовать потоки для CPU-нагрузки (GIL блокирует параллелизм)
- ✗Передавать через процессы non-picklable функцию или аргумент
- ✗Подавать весь итерируемый объект одним аргументом вместо map по элементам
Уточняющие вопросы
- →Почему функция и аргументы должны быть picklable для
ProcessPoolExecutor? - →Когда
ThreadPoolExecutor— правильный выбор вместоProcessPoolExecutor?
SeniorТеорияИногдаЧто произойдёт, если выполнить блокирующий код внутри корутины?
Что произойдёт, если выполнить блокирующий код внутри корутины?
Это блокирует весь событийный цикл: один поток исполняет все корутины кооперативно, и управление уступает лишь await, поэтому синхронный блокирующий вызов тормозит все прочие корутины до возврата.
Типичные ошибки
- ✗Считать, что цикл вытесняет блокирующую корутину по тайм-ауту
- ✗Думать, что приостановится лишь блокирующая корутина, а соседние идут
- ✗Полагать, что синхронные вызовы авто-выносятся в поток внутри
async def
Уточняющие вопросы
- →Как
loop.run_in_executorубирает блокирующий вызов с событийного цикла? - →Почему
async-нативный клиент лучше, чем обёртка синхронного в поток?
SeniorДизайнИногдаУ вас есть CSV из топ-100 000 сайтов (ранг, url). Спроектируйте асинхронную Python-программу, которая опрашивает каждый сайт и подсчитывает, какой веб-сервер он использует (nginx, apache, IIS, unknown). Объясните примитивы конкурентности, как ограничить число одновременных запросов, как обработать таймауты и сбои и какой ресурс (память, CPU или сокеты) кончится первым и почему.
У вас есть CSV из топ-100 000 сайтов (ранг, url). Спроектируйте асинхронную Python-программу, которая опрашивает каждый сайт и подсчитывает, какой веб-сервер он использует (nginx, apache, IIS, unknown). Объясните примитивы конкурентности, как ограничить число одновременных запросов, как обработать таймауты и сбои и какой ресурс (память, CPU или сокеты) кончится первым и почему.
Используйте asyncio с асинхронным HTTP-клиентом (например aiohttp), читая заголовок ответа Server на каждый сайт. Ограничьте конкурентность через asyncio.Semaphore — без него исчерпаете сокеты/дескрипторы. Задайте таймаут на запрос и оберните каждый запрос в try/except, чтобы один сбой не утопил пакет (считая его unknown). Стримьте CSV, а не грузите целиком. Сокеты/дескрипторы кончатся первыми: работа I/O-bound, поэтому CPU и память низки, пока открытые соединения копятся.
Типичные ошибки
- ✗Запускать все запросы сразу без семафора, исчерпывая сокеты/дескрипторы
- ✗Хвататься за процессы или потоки на I/O-задаче
- ✗Давать одному сбойному запросу прервать весь пакет вместо его подсчёта
Уточняющие вопросы
- →Почему
asyncio.Semaphoreпредотвращает исчерпание сокетов/дескрипторов? - →Как добавить ограниченные повторы для временных сетевых сбоев?
SeniorТеорияИногдаМожет ли добавление потоков сделать CPU-bound цикл в CPython медленнее, чем один поток, и почему?
Может ли добавление потоков сделать CPU-bound цикл в CPython медленнее, чем один поток, и почему?
Да. GIL и так сериализует байткод, поэтому ускорения нет; вдобавок рантайм постоянно гоняет захват/освобождение GIL между конкурирующими потоками, и эти чистые накладные расходы на переключение могут утянуть пропускную способность ниже однопоточной.
Типичные ошибки
- ✗Считать, что лишние потоки в худшем случае сравняются с одним, но не проиграют
- ✗Винить в замедлении конкуренцию за кеш, а не передачи
GIL - ✗Ждать, что настройка
sys.setswitchintervalвернёт ускорение CPU-bound работы
Уточняющие вопросы
- →Как C-расширение, отпускающее
GIL, обеспечивает реальное параллельное вычисление? - →Почему
multiprocessingмасштабирует CPU-работу там, где потоки не могут?
MiddleДизайнРедкоНужно опросить 1 000 000 URL, затем для каждого результата вызвать три независимых сетевых сервиса и объединить их уже готовой business_logic(s1, s2, s3), наконец сохранив каждый результат. Функция делает только сетевые вызовы (без CPU-работы). Какая модель конкурентности — однопоточная, потоки, процессы или asyncio — подходит лучше всего и почему? Объясните, как выбор масштабируется на миллион сетевых запросов с высоким фан-аутом, почему остальные хуже, и как ограничить число одновременных соединений.
Нужно опросить 1 000 000 URL, затем для каждого результата вызвать три независимых сетевых сервиса и объединить их уже готовой business_logic(s1, s2, s3), наконец сохранив каждый результат. Функция делает только сетевые вызовы (без CPU-работы). Какая модель конкурентности — однопоточная, потоки, процессы или asyncio — подходит лучше всего и почему? Объясните, как выбор масштабируется на миллион сетевых запросов с высоким фан-аутом, почему остальные хуже, и как ограничить число одновременных соединений.
Используйте asyncio: однопоточный событийный цикл дёшево обслуживает огромное число конкурентных ожиданий I/O — именно эта нагрузка. Потоки добавляют накладные расходы OS-потоков и переключений, не масштабируясь на миллион; процессы — для CPU-задач и тратят память зря; однопоточный вариант слишком медленный. Ограничьте конкурентность через asyncio.Semaphore, чтобы не исчерпать сокеты и дескрипторы, и собирайте результаты по URL.
Типичные ошибки
- ✗Хвататься за
multiprocessingна I/O-задаче, а не CPU-задаче - ✗Верить, что миллион OS-потоков масштабируется без накладных расходов
- ✗Опускать семафор, из-за чего соединения исчерпывают сокеты или дескрипторы
Уточняющие вопросы
- →Почему событийный цикл масштабируется на миллион ожиданий I/O, а потоки нет?
- →Если три сервиса сбоят с вероятностью 1% и вызовы идемпотентны, как добавить ограниченные повторы?