Python: как сделать многопоточную программу
Делаем свой таймер на Python
Python: как сделать многопоточную программу

Когда-то дав­но мы дела­ли про­стой тай­мер с напо­ми­на­ни­ем на Python. Он рабо­тал так:

  1. Мы спра­ши­ва­ли поль­зо­ва­те­ля, о чём ему напом­нить и через сколь­ко минут.
  2. Про­грам­ма на это вре­мя засы­па­ла и ниче­го не делала.
  3. Как толь­ко вре­мя сна закан­чи­ва­лось, про­грам­ма про­сы­па­лась и выво­ди­ла напоминание.

У такой схе­мы есть минус: мы не можем поль­зо­вать­ся про­грам­мой и выде­лен­ны­ми на неё ресур­са­ми до тех пор, пока она не проснёт­ся. Про­цес­сор по кру­гу гоня­ет пустые коман­ды и ждёт, когда мож­но будет про­дол­жить полез­ную рабо­ту. Что­бы про­цес­сор и про­грам­ма мог­ли во вре­мя рабо­ты тай­ме­ра делать что-то ещё, исполь­зу­ют потоки.

Что такое поток

В упро­щён­ном виде пото­ки — это парал­лель­но выпол­ня­е­мые зада­чи. По умол­ча­нию исполь­зу­ет­ся один поток — это зна­чит, что про­грам­ма дела­ет всё по оче­ре­ди, линей­но, без воз­мож­но­сти делать несколь­ко дел одновременно. 

Но если мы сде­ла­ем в про­грам­ме два пото­ка задач, то они будут рабо­тать парал­лель­но и неза­ви­си­мо друг от дру­га. Одно­му пото­ку не нуж­но будет ста­но­вить­ся на пау­зу, когда в дру­гом что-то происходит.

👉 Важ­но пони­мать, что поток — это высо­ко­уров­не­вое поня­тие из обла­сти про­грам­ми­ро­ва­ния. На уровне ваше­го «желе­за» эти пото­ки всё ещё могут обсчи­ты­вать­ся после­до­ва­тель­но. Но бла­го­да­ря тому, что они будут обсчи­ты­вать­ся быст­ро, вам может пока­зать­ся, что они рабо­та­ют параллельно. 

Многопоточность

Пред­ста­вим такую ситуацию: 

  • У вас на руке смарт-часы, кото­рые соби­ра­ют дан­ные о вашем пуль­се, УФ-излучении и дви­же­ни­ях. На смарт-часах рабо­та­ет про­грам­ма, кото­рая обра­ба­ты­ва­ет эти данные.
  • Про­грам­ма состо­ит из четы­рёх функ­ций. Пер­вая соби­ра­ет дан­ные с дат­чи­ков. Три дру­гие обра­ба­ты­ва­ют эти дан­ные и дела­ют выводы. 
  • Пока пер­вая функ­ция не собра­ла нуж­ные дан­ные, ниче­го дру­го­го не происходит.
  • Как толь­ко дан­ные вве­де­ны, запус­ка­ют­ся три остав­ши­е­ся функ­ции. Они не зави­сят друг от дру­га и каж­дая счи­та­ет своё.
  • Как толь­ко все три функ­ции закон­чат рабо­ту, про­грам­ма выда­ёт нуж­ный результат.

А теперь давай­те посмот­рим, как это выгля­дит в одно­по­точ­ной и мно­го­по­точ­ной систе­ме. Вид­но, что если про­цес­сор поз­во­ля­ет делать несколь­ко дел одно­вре­мен­но, в мно­го­по­точ­ном режи­ме про­грам­ма будет рабо­тать быстрее:

Многопоточность в Python

Многопоточность на Python

За пото­ки в Python отве­ча­ет модуль threading, а сам поток мож­но создать с помо­щью клас­са Thread из это­го моду­ля. Под­клю­ча­ет­ся он так:

from threading import Thread

После это­го с помо­щью функ­ции Thread() мы смо­жем создать столь­ко пото­ков, сколь­ко нам нуж­но. Логи­ка рабо­ты такая:

  1. Под­клю­ча­ем нуж­ный модуль и класс Thread.
  2. Пишем функ­ции, кото­рые нам нуж­но выпол­нять в потоках.
  3. Созда­ём новую пере­мен­ную — поток, и пере­да­ём в неё назва­ние функ­ции и её аргу­мен­ты. Один поток = одна функ­ция на входе.
  4. Дела­ем так столь­ко пото­ков, сколь­ко тре­бу­ет логи­ка программы.
  5. Пото­ки сами сле­дят за тем, закон­чи­лась в них функ­ция или нет. Пока рабо­та­ет функ­ция — рабо­та­ет и поток.
  6. Всё это рабо­та­ет парал­лель­но и (в тео­рии) не меша­ет друг другу.

Для иллю­стра­ции запу­стим такой код:

import time
from threading import Thread

def sleepMe(i):
    print("Поток %i засыпает на 5 секунд.\n" % i)
    time.sleep(5)
    print("Поток %i сейчас проснулся.\n" % i)
for i in range(10):
    th = Thread(target=sleepMe, args=(i, ))
    th.start()

А вот как выгля­дит резуль­тат. Обра­ти­те вни­ма­ние — пото­ки про­сы­па­ют­ся не в той после­до­ва­тель­но­сти, как мы их запу­сти­ли, а в той, в какой их выпол­нил про­цес­сор. Ино­гда это может поме­шать рабо­те про­грам­мы, но про это мы пого­во­рим отдель­но в дру­гой статье.

Многопоточность в Python: обратите внимание — потоки просыпаются не в той последовательности, как мы их запустили, а в той, в какой их выполнил процессор

Добавляем потоки в таймер

Пер­вое, что нам нуж­но сде­лать, — выне­сти код таймера-напоминания в отдель­ную функ­цию, что­бы создать с ней поток. Для это­го исполь­зу­ем коман­ду def:

# Делаем отдельную функцию с напоминанием
def remind():
    # Спрашиваем текст напоминания, который нужно потом показать пользователю
    print("О чём вам напомнить?")
    # Ждём ответа пользователя и результат помещаем в строковую переменную text
    text = str(input())
    # Спрашиваем про время
    print("Через сколько минут?")
    # Тут будем хранить время, через которое нужно показать напоминание
    local_time = float(input())
    # Переводим минуты в секунды
    local_time = local_time * 60
    # Ждём нужное количество секунд, программа в это время ничего не делает
    time.sleep(local_time)
    # Показываем текст напоминания
    print(text)

Теперь сде­ла­ем новый поток, в кото­рый отпра­вим выпол­нять­ся нашу новую функ­цию. Так как аргу­мен­тов у нас нет, то и аргу­мен­ты пере­да­вать не будем, а напи­шем args=().

# Создаём новый поток

th = Thread(target=remind, args=())

# И запускаем его

th.start()

Нам оста­лось убе­дить­ся в том, что пока поток рабо­та­ет, мы можем выпол­нять в про­грам­ме что-то ещё и наше засы­па­ние в пото­ке на это не повли­я­ет. Для это­го мы через 20 секунд после запус­ка выве­дем сооб­ще­ние на экран. За 20 секунд поль­зо­ва­тель успе­ет вве­сти напо­ми­на­ние и вре­мя, после чего тай­мер уйдёт в новый поток и там уснёт на нуж­ное коли­че­ство минут. Но это не поме­ша­ет рабо­те основ­ной про­грам­мы — она тоже будет выпол­нять свои коман­ды парал­лель­но с потоком.

# Пока работает поток, выведем что-то на экран через 20 секунд после запуска

time.sleep(20)

print("Пока поток работает, мы можем сделать что-нибудь ещё.\n")

Резуль­тат:

Добавляем потоки в таймер: результат
Готовый код

# Подключаем модуль для работы со временем
import time

# Подключаем потоки
from threading import Thread

# Делаем отдельную функцию с напоминанием
def remind():
    # Спрашиваем текст напоминания, который нужно потом показать пользователю
    print("О чём вам напомнить?")
    # Ждём ответа пользователя и результат помещаем в строковую переменную text
    text = str(input())
    # Спрашиваем про время
    print("Через сколько минут?")
    # Тут будем хранить время, через которое нужно показать напоминание
    local_time = float(input())
    # Переводим минуты в секунды
    local_time = local_time * 60
    # Ждём нужное количество секунд, программа в это время ничего не делает
    time.sleep(local_time)
    # Показываем текст напоминания
    print(text)

# Создаём новый поток
th = Thread(target=remind, args=())
# И запускаем его
th.start()

# Пока работает поток, выведем что-то на экран через 20 секунд после запуска
time.sleep(20)
print("Пока поток работает, мы можем сделать что-нибудь ещё.\n")

Потоки — это ещё не всё

В Python кро­ме пото­ков есть ещё оче­ре­ди (queue) и управ­ле­ние про­цес­са­ми (multiprocessing). Про них мы пого­во­рим отдельно.

Текст и иллю­стра­ции:
Миша Поля­нин

Редак­тор:
Мак­сим Ильяхов

Заглав­ная иллю­стра­ция:
Даня Бер­ков­ский

Кор­рек­тор:
Ира Михе­е­ва

Вёрст­ка:
Маша Дро­но­ва

Соц­се­ти:
Олег Веш­кур­цев

Да про­длит­ся мно­го­по­точ­ность:
во веки веков!