Обновить

Комментарии 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 print

2. Главный поток программы выполняет свой 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 из коробки, пишет в очередь и не создаёт таких проблем.

Надеюсь, теперь понятно, как это работает :)

Огромное спасибо тебе, добрый человек!

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации