print() — это не атомарная операция. Она состоит из нескольких системных вызовов:
write("Number: 0") -> write("\n") -> flush буфера
Между любыми из этих операций планировщик ОС может переключить исполнение на другой поток.
Что и произошло в твоём случае
Number: 0Поток запущен!
Попробую расписать хронологию событий:
[thread] write("Number: 0") <- записал текст, но ещё не '\n'
планировщик переключился
[main] write("Поток запущен!")
write("\n")
планировщик вернулся
[thread] write("\n") <- вот этот '\n' от Number: 0
В итоге в stdout попало: Number: 0 + Поток запущен! + \n + \n
Почему планировщик переключился именно там?
Ты не можешь это контролировать без явной синхронизации. Планировщик ОС переключает потоки по своему усмотрению — по истечению кванта времени, при системном вызове, при освобождении GIL. write() в системный буфер — это как раз системный вызов, точка возможного переключения.
В CPython есть GIL — в каждый момент времени байткод выполняет только один поток. Но GIL снимается при I/O операциях (запись в stdout — это I/O), поэтому потоки могут чередоваться именно на write.
[thread] -> захватил GIL -> пишет "Number: 0" -> I/O syscall -> GIL снялся
[main] -> захватил GIL -> пишет "Поток запущен!\n" -> ...
[thread] -> захватил GIL -> пишет "\n" -> ...
По поводу твоих вопросов:
По идее вот тут print должен передвинуть каретку на следующую строку
\n — это просто символ в буфере вывода, а не механическое действие. Терминал его интерпретирует когда читает, а не когда он записан.
Главный поток программы выполняет свой print . print("Поток запущен!")Затем он "узнаёт" о существовании другого потока и переключается в режим ожидания? Или как это работает?
Нет. Главный поток понятия не имеет что делает thread до вызова join(). Он просто выполняет свой print независимо. join() — это явная точка синхронизации, где main грубо говорит "стоп, жду пока thread не закончит".
Как это можно пофиксить:
import threading
import time
# Lock — мьютекс для синхронизации доступа к stdout
print_lock = threading.Lock()
def print_numbers():
for i in range(10):
with print_lock:
print(f"Number: {i}")
time.sleep(0.5)
thread = threading.Thread(target=print_numbers)
thread.start()
with print_lock:
print("Поток запущен!")
thread.join()
print("Поток завершен!")
Но честно говоря — в реальном хайлоад сервисе print() из потоков вообще не используют. Есть logging модуль, который thread-safe из коробки, пишет в очередь и не создаёт таких проблем.
Статья скорее про внутренности CPython и рассчитана на читателей, которые уже знакомы с базовой моделью объектов в Python. Поэтому некоторые вещи не расписаны подробно, чтобы не раздувать статью.
Ну почти :)
print()— это не атомарная операция. Она состоит из нескольких системных вызовов:Между любыми из этих операций планировщик ОС может переключить исполнение на другой поток.
Что и произошло в твоём случае
Попробую расписать хронологию событий:
В итоге в stdout попало:
Number: 0+Поток запущен!+\n+\nПочему планировщик переключился именно там?
Ты не можешь это контролировать без явной синхронизации. Планировщик ОС переключает потоки по своему усмотрению — по истечению кванта времени, при системном вызове, при освобождении GIL.
write()в системный буфер — это как раз системный вызов, точка возможного переключения.В CPython есть GIL — в каждый момент времени байткод выполняет только один поток. Но GIL снимается при I/O операциях (запись в stdout — это I/O), поэтому потоки могут чередоваться именно на
write.По поводу твоих вопросов:
\n— это просто символ в буфере вывода, а не механическое действие. Терминал его интерпретирует когда читает, а не когда он записан.Нет. Главный поток понятия не имеет что делает
threadдо вызоваjoin(). Он просто выполняет свойprintнезависимо.join()— это явная точка синхронизации, где main грубо говорит "стоп, жду пока thread не закончит".Как это можно пофиксить:
Но честно говоря — в реальном хайлоад сервисе
print()из потоков вообще не используют. Естьloggingмодуль, который thread-safe из коробки, пишет в очередь и не создаёт таких проблем.Надеюсь, теперь понятно, как это работает :)
Статья скорее про внутренности CPython и рассчитана на читателей, которые уже знакомы с базовой моделью объектов в Python. Поэтому некоторые вещи не расписаны подробно, чтобы не раздувать статью.