Комментарии 3
Только начинаю разбираться с потоками в python, но думаю спросить тут будет уместно)
Допустим у нас такой вот код есть:
import threading
import time
def print_numbers():
for i in range(10):
print(f"Number: {i}")
time.sleep(0.5)
# Создаем поток
thread = threading.Thread(target=print_numbers)
thread.start()
# Основной поток продолжает выполнять свои задачи
print("Поток запущен!")
# Ждем, пока поток завершится
thread.join()
print("Поток завершен!")Сколько бы я ни запускал, получаю такой вывод:
Number: 0Поток запущен!
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5
Number: 6
Number: 7
Number: 8
Number: 9
Поток завершен!
Процесс завершился с кодом выхода 0
Почему первая строка вывода такая?
Теоретичски, что происходит в консоли:
0. Создаётся главный поток программы, который "выполняет" код построчно, на виртуальной машине. (тут если я не прав в выражениях, прошу меня поправить)
1. Создаётся поток thread. Он сразу же вызывает функцию print_numbers(), которая печатает в консоль f-строку Number: 0. По идее вот тут print должен передвинуть каретку на следующую строку, в параметрах print есть end, со значением по умолчанию '\n':
def print(self, *args, sep=' ', end='\n', file=None): # known special case of print2. Главный поток программы выполняет свой print . print("Поток запущен!")
Затем он "узнаёт" о существовании другого потока и переключается в режим ожидания? Или как это работает?
Уже на этом этапе в консоли мы получаем какую-то мешанину в потоках и видим что у нас есть '\n' в консоли
Получается главный поток не знал о существовании другого потока и вывел свой принт? А потом получается поток thread вывел остаток своей строки или как?
Ну почти :)
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 из коробки, пишет в очередь и не создаёт таких проблем.
Надеюсь, теперь понятно, как это работает :)

Асинхронность в Python для senior interview: от asyncio до выбора правильной реализации под задачу