Pull to refresh
8K+
4

User

16,1
Rating
2
Subscribers
Send message

Ну почти :)

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. Поэтому некоторые вещи не расписаны подробно, чтобы не раздувать статью.

Information

Rating
465-th
Registered
Activity

Specialization

Бэкенд разработчик
Старший
From 9,000 $
Python
ООП
PostgreSQL
Базы данных
SQL
Git
Kubernetes
REST