Threading в Python: что это такое и как работает многопоточность

0
21

Краткая памятка по работе с потоками в Python

  1. Импортируйте модуль threading.
  2. Создайте функцию, которую будет выполнять поток.
  3. Создайте объект Thread(target=ваша_функция).
  4. Запустите поток методом start().
  5. Используйте join() для ожидания завершения потока.
  6. Для синхронизации применяйте Lock.
  7. Для обмена данными используйте queue.Queue.
  8. Для I/O-bound задач используйте потоки, для CPU-bound — процессы.
  9. Помните о GIL при работе с потоками.
  10. Используйте ThreadPoolExecutor для управления группой потоков.
  11. Устанавливайте daemon=True для фоновых потоков.
  12. Избегайте состояния гонки с помощью синхронизации.
  13. Проверяйте активность потока через is_alive().
  14. Используйте таймеры (Timer) для отложенного выполнения.

Небольшое интро

Основы многопоточного программирования в - изображение номер один
Основы многопоточного программирования в — изображение номер один

Поток позволяет вам запустить часть длинного кода так, как если бы он был отдельной программой. Это своего рода вызов наследуемого процесса, за исключением того, что вы вызываете функцию или класс, вместо отдельной программы. Я всегда находил конкретные примеры крайне полезными. Давайте взглянем на нечто совершенно простое:

Здесь мы импортируем модуль threading и создаем обычную функцию под названием doubler. Наша функция принимает значение и удваивает его. Также она выводи название потока, который вызывает функцию и выводит бланк-строчку в конце. Далее, в последнем блоке кода, мы создаем пять потоков, и запускаем каждый из них по очереди.

Используя многопоточность можно решить много рутинных моментов. Например загрузка видео или другого материала в социальные сети, такие как Youtube или Facebook. Для развития своего Youtube канала можно использовать который возьмет на себя администрирование вашего канала. Youtube отличный источник заработка и чем больше каналов тем лучше. Без Publbox вам не обойтись.

Обратите внимание на то, что когда мы определяем поток, мы устанавливаем его целью на нашу функцию doubler, и мы также передаем аргумент функции. Причина, по которой параметр args выглядит немного непривычно, заключается в том, что нам нужно передать sequence функции doubler, и она принимает только один аргумент, так что нужно добавить запятую в конце, чтобы создать sequence одной из них. Обратите внимание на то, что если вы хотите подождать, пока поток определится, вы можете вызвать его метод join(). Когда вы запустите этот код, вы получите следующую выдачу:

Конечно, вам скорее всего не захочется выводить вашу выдачу в stdout. Это может закончиться сильным беспорядком. Вместо этого, вам нужно использовать модуль Python под названием logging. Это защищенный от потоков модуль и он прекрасно выполняет свою работу. Давайте немного обновим указанный ранее пример и добавим модуль logging, и заодно назовем наши потоки:

Threading - изображение номер два
Threading — изображение номер два

Эта выдача достаточно понятная, так что давайте пойдем дальше. Я хочу разобрать еще один вопрос в этой статье. Мы поговорим о наследовании класса под названием. Давайте снова рассмотрим предыдущий пример, только вместо вызова потока напрямую, мы создадим свой собственный подкласс. Вот обновленный код:

В этом примере мы только что унаследовали класс. Мы передали число, которое хотим удвоить, а также передали объект логгированмя, как делали это ранее. Но на этот раз, мы настроим название потока по-другому, вызвав функцию setName в объекте потока. Нам все еще нужно вызвать старт в каждом потоке, но запомните, что нам не нужно определять это в наследуемом классе. Когда вы вызываете старт, он запускает ваш поток, вызывая метод run. В нашем классе мы вызываем функцию doubler для выполнения наших вычислений. Выдача сильно похожа на ту, что была в примере ранее, за исключением того, что я добавил дополнительную строку в выдаче. Попробуйте сами и посмотрите, что получится.

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

Многопоточность в - изображение номер три
Многопоточность в — изображение номер три

В информатике поток — это минимальная единица работы, запланированная для выполнения операционной системой.

  • Они существуют внутри процесса;
  • В одном процессе может быть несколько потоков;
  • Потоки в одном процессе разделяют состояние и память родительского процесса.

У вас он может отличаться, потому что у параллельных потоков нет определенного порядка.

Функции threading в Python

Python threading 07 - изображение номер четыре
Python threading 07 — изображение номер четыре

Возьмем программу из первого примера и воспользуемся ею для демонстрации разных функций модуля.

threading.active_count()

Python threading 02 - изображение номер пять
Python threading 02 — изображение номер пять

Эта функция возвращает количество исполняемых на текущий момент потоков. Изменим последнюю программу, чтобы она выглядела вот так:

import time import threading from threading import Thread def sleepMe(i): print(«Поток %i засыпает на 5 секунд.» % i) (5) print(«Поток %i сейчас проснулся.» % i) for i in range(10): th = Thread(target=sleepMe, args=(i,)) () print(«Запущено потоков: %i.» % threading.active_count())

Теперь в выводе будет показываться количество активных на текущий момент потоков:

Также обратите внимание, что после запуска всех потоков счетчик показывает число 11, а не 10. Причина в том, что основной поток также учитывается наравне с 10 остальными.

threading.current_thread()

Python-GTK 02-09 - изображение номер шесть
Python-GTK 02-09 — изображение номер шесть

Эта функция возвращает исполняемый прямо сейчас поток. С ее помощью можно выполнять определенные действия с ним. Поменяем все тот же скрипт:

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

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

Multithreading in - изображение номер семь
Multithreading in — изображение номер семь

В этом материале были разобраны некоторые функции модуля threading. Они предоставляют удобные методы для управления потоками в многопоточной среде.

Параллельная обработка данных на основе потоков

Потоковые и многопроцессорные модули на - изображение номер восемь
Потоковые и многопроцессорные модули на — изображение номер восемь

Изменено в Python 3.7: раньше этот модуль был необязательным, теперь он доступен всегда.

Конструкция модуля threading основывается на модели потоков в Java. Но там, где Java делает блокировки, а переменные состояния базовым поведением каждого объекта, то в Python они являются отдельными объектами. Класс потоков Python поддерживает подмножество поведения класса потоков Java.

В настоящее время нет приоритетов, нет групп потоков, а потоки не могут быть уничтожены, остановлены, приостановлены, возобновлены или прерваны. Статические методы класса Thread Java при реализации сопоставляются с функциями уровня модуля.

Примечание-1. Чтобы применение многопоточного режима дало ощутимое увеличение производительности, используйте модуль threading, там где встречается много не связанных друг с другом блокирующих операций ввода/вывода. Например, нужно обрабатывать много разрозненных запросов с большой задержкой на ожидание. В режиме «живой очереди» это долго и лучше распараллелить задачу.

Примечание-2. Считать что-то в одном потоке и передавать для дальнейшей обработки другому это не очень здорово, так как возникает лишняя зависимость, которая приводит к снижению производительности, а в случае ошибки приведет к краху всей программы.

Предупреждение. Любой процессор поддерживает определенное количество потоков на ядро, заложенное производителем (обычно 4-6 потоков), при которых он работает оптимально быстро. Нельзя создавать безгранично много потоков. При увеличении числа потоков на величину, большую, чем заложил производитель, программа будет выполняться дольше или вообще поведет себя непредсказуемым образом (вплоть до зависания).

Откажитесь от использования потоков в коде, если нужна хорошая переносимость между разными устройствами. Правильно подобрать число потоков для машины пользователя — трудная задача. Если пишете код под известное «железо», то оптимальное количество потоков можно посмотреть в документации или подобрать экспериментально (тестированием).

Описание класса Thread():

Threading in - изображение номер девять
Threading in — изображение номер девять

Класс Thread модуля threading запускает какое либо действие, которое будет выполняется в отдельном потоке управления.

Внимание! Никакие другие методы не должны переопределяться в подклассе (кроме конструктора). Другими словами, можно переопределять только методы __init__() и () этого класса.

Как только объект потока создан, его деятельность должна быть запущена путем вызова метода потока (). Это вызывает метод () в отдельном потоке управления.

Как только активность потока запущена, он считается «живым». Поток перестает быть активным, когда его метод () завершается либо обычно, либо при возникновении необработанного исключения. Метод Thread.is_alive() проверяет, жив ли поток.

Другие потоки могут вызывать метод (), который блокирует вызывающий поток до тех пор, пока не завершится поток, чей метод.join() вызван. Например, если для всех порожденных программой потоков вызвать этот метод, то дальнейшее выполнение программы будет заблокировано до тех пор пока все потоки не завершатся.

У потока есть имя. Имя может быть передано конструктору (аргумент name) и прочитано или изменено через атрибут.

ЧИТАТЬ ТАКЖЕ:  Правила именования переменных в Python: допустимые имена, PEP8 и нотация

Поток можно пометить как «демонический поток». Значение этого флага заключается в том, что когда программа Python завершается, работающими остаются только потоки демона. Начальное значение наследуется от создающего потока. Флаг можно установить с помощью свойства или аргумента конструктора daemon.

Примечание. Потоки демона внезапно останавливаются при завершении работы. Их ресурсы (такие как открытые файлы, транзакции базы данных и т. д.) могут быть освобождены неправильно. Если необходимо, чтобы потоки корректно останавливались, то делайте их недемоническими и используйте подходящий механизм сигнализации, такой как объект ().

Существует объект основного потока программы. Основной поток соответствует начальному потоку управления в программе Python. Это не поток демона.

Существует вероятность того, что будут созданы «объекты фиктивного потока». Это объекты потоков, соответствующие «чужеродным потокам», которые представляют собой потоки управления, запускаемые вне модуля потоковой передачи, например непосредственно из кода языка C. Объекты фиктивного потока имеют ограниченную функциональность. Они всегда считаются живыми и демоническими и не могут быть объединены методом (). Они никогда не удаляются, так как невозможно обнаружить завершение чужих потоков.

Изменено в версии 3.10: если аргумент name опущен, то используется имя функции target.

Аргумент context — это значение Context, которое используется при запуске потока. Значение по умолчанию None, что указывает на то, что флаг.thread_inherit_context управляет поведением. Если флаг равен True, потоки будут запускаться с копией контекста вызывающего start(). Если False, то они будут запускаться с пустым контекстом. Чтобы явно начать с пустого контекста, передайте новый экземпляр Context(). Чтобы явно начать с копии текущего контекста, передайте значение из copy_context(). Флаг по умолчанию равен True в free-threaded сборках и False в других случаях.

Пример использования параметра context при создании потоков в Python 3.14:

PPT - изображение номер десять
PPT — изображение номер десять

Параметр context в Python 3.14+ дает точный контроль над тем, какой контекст будет доступен в новых потоках, что особенно важно для многопоточных приложений с контекстными переменными.

  • Наследование контекста — поведение по умолчанию
  • Явная копия контекста — поток получает копию текущего контекста
  • Работа с ThreadPoolExecutor — как контекст передается в пуле потоков
  • Изоляция контекста — изменения в потоке не влияют на основной контекст

import threading import contextvars from import ThreadPoolExecutor # Создание контекстных переменных request_id = (‘request_id’) user_name = (‘user_name’) def worker_function(task_id): «»»Функция, выполняемая в потоке»»» try: # Попытка получить значения из контекста req_id = request_id.get() usr_name = user_name.get() print(f»Поток {task_id}: request_id={req_id}, user_name={usr_name}») except LookupError: print(f»Поток {task_id}: Контекстные переменные не найдены») # Установка новых значений в контексте потока request_id.set(f»thread_{task_id}») user_name.set(f»worker_{task_id}») print(f»Поток {task_id}: Установлены новые значения») # Поток с наследованием контекста (по умолчанию) def example_with_inherited_context(): print(«=== Наследование контекста ===») # Установка значений в основном потоке request_id.set(«main_123») user_name.set(«main_user») # Создание потока с наследованием контекста thread = (target=worker_function, args=(1,)) () () # Поток с явной копией контекста def example_with_copied_context(): print(«\n=== Явная копия контекста ===») # Установка значений в основном потоке request_id.set(«main_789») user_name.set(«main_user_3») # Создание копии текущего контекста copied_context = contextvars.copy_context() thread = (target=worker_function, args=(3,), context=copied_context) () () # Использование с ThreadPoolExecutor def example_with_threadpool(): print(«\n=== ThreadPoolExecutor ===») # Установка значений в основном потоке request_id.set(«pool_main») user_name.set(«pool_user») # ThreadPoolExecutor также поддерживает параметр context (в Python 3.14+) with ThreadPoolExecutor(max_workers=2) as executor: # Задачи будут наследовать контекст по умолчанию futures = [(worker_function, i) for i in range(4, 6)] # Ожидание завершения всех задач for future in futures: () # Демонстрация изменения контекста в потоке def example_context_isolation(): print(«\n=== Изоляция контекста ===») # Установка начальных значений request_id.set(«original») user_name.set(«original_user») def modify_context_worker(thread_id): # Изменение контекста в потоке request_id.set(f»modified_{thread_id}») user_name.set(f»modified_user_{thread_id}») print(f»В потоке {thread_id}: {request_id.get()}, {user_name.get()}») # Создание потока с копией контекста copied_context = contextvars.copy_context() thread = (target=modify_context_worker, args=(7,), context=copied_context) () () # Проверка, что основной контекст не изменился print(f»В основном потоке: {request_id.get()}, {user_name.get()}») # Запуск всех примеров if __name__ == «__main__»: example_with_inherited_context() example_with_empty_context() example_with_copied_context() example_with_threadpool() example_context_isolation()

Параметры:

An - изображение номер одиннадцать
An — изображение номер одиннадцать

Если подкласс переопределяет конструктор, то он должен обязательно вызвать конструктор базового класса (Thread.__init__()), прежде чем делать что-либо еще с потоком.

Thread.native_id:

c++ - изображение номер двенадцать
c++ — изображение номер двенадцать

Атрибут Thread.native_id представляет собой идентификатор потока (TID), присвоенный потоку ОС (ядром). Его значение может использоваться для однозначной идентификации этого конкретного потока в масштабах всей системы до завершения потока, после чего значение может быть переработано ОС.

Примечание. Подобно идентификаторам процессов, идентификаторы потоков действительны (гарантированно уникальны для всей системы) только с момента создания потока до момента его завершения.

Thread.is_alive():

Python threading 04 - изображение номер тринадцать
Python threading 04 — изображение номер тринадцать

Этот метод возвращает True непосредственно перед запуском метода () до тех пор, пока метод () не завершится.

Общий пример создания потоков классом Thread.

Python: 3 - изображение номер четырнадцать
Python: 3 — изображение номер четырнадцать

import threading def worker(i): «»»thread worker function»»» print(f’Worker-{i}’) threads = [] # запускаем функцию ‘worker()’ # для выполнения в 5-ти потоках for i in range(5): t = (target=worker, args=(i,)) (t) () # блокируем дальнейшее выполнение программы # пока не закончат выполняться все 5 потоков [() for thread in threads]

Как получить значение, возвращаемое потоком Thread в Python?

Basic - изображение номер пятнадцать
Basic — изображение номер пятнадцать

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

  1. Если потоки трудятся над какой-то общей задачей, то результаты можно складывать в список или очередь, а по завершении работы получить результаты из соответствующего объекта.
  2. Если необходимо получать результат работы нескольких потоков, но в той последовательности, в которой стоят задачи (ведь потоки могут возвращать результаты не по порядку), то используйте очередь с приоритетом.
  3. Если потоки трудятся над разными задачами и результаты работы потоков смешивать нельзя, то возвращаемые значения можно складывать в словарь, где ключами могут быть имена потоков или простые идентификаторы потоков threading.get_ident().
  4. Если стоит предыдущая задача, но запущено несколько экземпляров программы и каждая работает в несколько потоков, то ключами к результатам работы будет интегральный идентификатор текущего потока threading.get_native_id().
  5. Если для нормальной работы программы, потокам необходимо обмениваться результатами, то подойдет та же многопоточная очередь. Вот хороший пример обмена информацией между потоками: в примере происходит чтение и обработка файлов из директории в 2 потока с последующей передачей информации в 3-й поток, в котором она записывается в общий файл.
  6. Если результаты работы потоков нужно получать в реальном времени в основном потоке программы (хотя я не знаю зачем они там нужны в реальном времени см. пункт 5.), то можно в цикле проверять, живы ли потоки, и пока они живы вытаскивать результаты из той же очереди, в которую потоки будут складывать результаты.

import threading, time, queue, random def worker(data, result): # цикл, пока в очереди есть задания while not (): # получаем задание из очереди с данными task = () # для приличия, умножим хотя бы на 2 res = task * 2 # результаты будем возвращать как кортеж, # в котором будет (результат и ID_потока) ((res, threading.get_ident())) # имитируем нагрузку t_sleep = (0.5, 2) (t_sleep) # говорим очереди с данными ‘data’, # что задание выполнено data.task_done() # заполняем очередь заданиями для потоков # пускай это будет простой список чисел, # которые потоки будут возвращать data = () for i in range(10, 20): (i) # очередь с возвращаемыми # результатами работы потоков result = () # создаем и запускаем потоки for _ in range(3): # имена потоков будут одинаковыми, что бы можно # было их отличить от основного потока программы thread = (name=’worker’, target=worker, args=(data, result,)) () # получаем результаты работы потоков в реальном # времени в основном потоке программы. t_start = () # цикл, пока жив хоть один поток ‘worker’ while any(th.is_alive() for th in () if == ‘worker’): #!Внимание! очередь с результатами при # работе потоков с разной нагрузкой, # на короткие промежутки может быть пустой, # к тому же мы сразу извлекаем результаты if not (): res, id_thread = () # прошедшее время с момента запуска потоков tm = round(() — t_start, 2) print(f’ThID-{id_thread}: результат {res}, время: {tm}’) # ThID-140613307041536: результат 24, время: 0.62 # ThID-140613323826944: результат 20, время: 0.86 # ThID-140613307041536: результат 26, время: 1.49 # ThID-140613315434240: результат 22, время: 1.94 # ThID-140613323826944: результат 28, время: 2.04 # ThID-140613307041536: результат 30, время: 3.19 # ThID-140613323826944: результат 34, время: 3.79 # ThID-140613315434240: результат 32, время: 3.8 # ThID-140613307041536: результат 36, время: 4.59

Пример использования потоков в параллельной обработке файлов.

Python: как сделать многопоточность в программе - изображение номер шестнадцать
Python: как сделать многопоточность в программе — изображение номер шестнадцать

В этом примере будем сканировать каталог на предмет файлов с расширением.txt, а потом обрабатывать их например в 3 потока. Обработка будет заключаться в изменении строк и запись измененных данных в другой каталог.

# import pathlib, random path = (‘.’) # название тестовой директории test_dir = ‘test_dir’ # Путь к тестовой директории path_dir = (test_dir) # создаем тестовый директорий path_dir.mkdir(exist_ok=True) # количество создаваемых файлов n_files = 50 # скобочки {} — это шаблон для метода строки # () туда вставим имя файла line = «{} — Эту строку будем писать в файл» if path_dir.is_dir(): for n in range(n_files): # название файла f_name = f’file-{n}.txt’ # путь к файлу path_file = path_dir.joinpath(f_name) # Генерируем разное количество строк, # которые будут писаться в файл data = [(f_name) for _ in range((5000,15000))] # пишем данные в файл path_file.write_text(‘\n’.join(data))

ЧИТАТЬ ТАКЖЕ:  Как заработать на ботах Python: торговля криптовалютой, разработка на Binance и создание с нуля

Предупреждение: При такой обработке файлов прирост производительности будет незначительным по сравнению с однопоточной обработкой, так как во-первых: 3 потока создают дополнительную загрузку файловой системы (одновременное чтение/запись 3-х файлов), следовательно файловая система будет работать медленнее, чем при однопоточной. И, во вторых: GIL еще ни кто не отменял.

import pathlib, threading, time, queue def worker(que): while True: # Получаем задание (имя файла) из очереди job = () # Путь к новому (обработанному) файлу file_write = path_dir_modified.joinpath() # открываем файл из очереди на чтение и # новый файл на запись with open(job, ‘r’) as fr, open(file_write, ‘w’) as fw: # дописываем имя файла (f’\n\n============> {file_write}\n\n’) # читаем данные построчно for line in fr: # например, заменим букву у на 0 line = (‘у’, ‘0’) # пишем измененные данные (line) # Сообщаем очереди что задача выполнена que.task_done() path = (‘.’) # тестовый каталог с файлами test_dir = ‘test_dir’ # Путь к тестовой директории path_dir = (test_dir) # получаем список файлов list_files = path_dir.glob(‘*.txt’) # каталог с обработанными файлами test_dir_modified = ‘test_dir_modified’ path_dir_modified = (test_dir_modified) path_dir_modified.mkdir(exist_ok=True) # создаем и заполняем очередь именами файлов que = () for file in list_files: (file) if (): # Создаем и запускаем потоки n_thead = 3 for _ in range(n_thead): th = (target=worker, args=(que,), daemon=True) () # Блокируем дальнейшее выполнение # программы до тех пор пока потоки # не обслужат все элементы очереди () else: print(‘Файлы не найдены.’)

Замки и Синхронизация

Locking &amp - изображение номер семнадцать
Locking &amp — изображение номер семнадцать

Когда у вас в распоряжении более одного потока, тогда вам, возможно, понадобится понять, как избежать конфликтов. Под этим я имею ввиду то, что вы можете использовать случай, где более одного потока нуждаются в доступе к одном и тому же ресурсу в одно и то же время. Если вы не думаете о таких проблемах и соответственном планировании, тогда вы можете столкнуться с самыми худшими проблемами в крайне неудобное время, и, как правило, в момент выпуска кода.

Решение проблемы – это использовать замки. Замок предоставлен модулем Python threading и может держать один поток, или не держать поток вообще. Если поток пытается acquire замок на ресурсе, который уже закрыт, этот поток будет ожидать до тех пор, пока замок не откроется. Давайте посмотрим на практичный пример одного кода, который не имеет никакого замочного функционала, но мы попробуем его добавить:

Мы можем сделать этот пример еще интереснее, добавив вызов. Следовательно, проблема здесь в том, что один поток может вызывать update_total и перед тем, как он обновится, другой поток может вызвать его и тоже попытается обновить его. В зависимости от порядка операций, значение может быть добавлено единожды. Давайте добавим замок к функции. Существует два способа сделать эта. Первый – это использование try/finally, если мы хотим убедиться, что замок снят. Вот пример:

Здесь мы просто вешаем замок, перед тем как сделать что-либо другое. Далее, мы пытаемся обновить total и finally, мы снимаем замок и выводим нынешний total. Мы можем упростить данную задачу, используя оператор Python под названием with:

Как вы видите, нам больше не нужны try/finally, так как контекстный менеджер, предоставленный оператором with, сделал все это за нас. Конечно, вы можете обнаружить, что пишите код там, где необходимы несколько потоков с доступом к нескольким функциям. Когда вы впервые начнете писать конкурентный код, вы можете сделать что-нибудь на подобии следующего:

Этот код хорошо работает в данном случае, но подразумевается, что у вас есть несколько потоков, вызывающих обе эти функции. Пока один поток работает над функциями, второй может, в свою очередь, обновлять данные и вы получите некорректный результат. Проблема в том, что вы сначала можете не заметить, что с результатами что-то неладное. Как найти решение этой проблеме? Давайте в ней разберемся. Первое, к чему можно прийти, это повесить замок на двух вызовах функций. Давайте попробуем обновить указанный ранее пример, чтобы получить что-то вроде следующего:

Когда вы запустите этот код, вы увидите, что он просто висит. Причина в том, что мы просто указываем модулю threading повесить замок. Так что когда мы вызываем первую функцию, она видит, что замок уже висит и блокируется. Это будет длиться до тех пор, пока замок не снимут, что никогда и не случится, так как это не предусмотрено в коде. Хорошее решение в данном случае – использовать re-entrant замок. Модуль threading предоставляет такой, в виде функции RLock. Просто замените строку lock = () на lock = () и попробуйте перезапустить код. Теперь он должен заработать. Если вы хотите попробовать код выше но добавить в него потоки, то мы можем заменить call на main следующим образом:

Так мы запустим основную функцию в каждом потоке, что в свою очередь приведет к вызову остальных двух функций. В конце вы получите достаточно крупную выдачу.

Другие Компоненты Потоков

Существует ряд случаев, когда вам нужно сделать так, чтобы потоки были связанны друг с другом. Как я упоминал ранее, вы можете использовать Event для этой цели. Но более удобный способ – использовать Queue. В нашем примере мы используем оба способа! Давайте посмотрим, как это будет выглядеть:

Давайте немного притормозим. Во первых, у нас есть функция creator (также известная, как producer), которую мы используем для создания данных, с которыми мы хотим работать (или использовать). Далее мы получаем еще одну функцию, которую мы используем для обработки данных, под названием my_consumer. Функция creator использует метод Queue под названием put, чтобы добавить данные в очередь, затем потребитель, в свою очередь, будет проверять, есть ли новые данные и обрабатывать их, когда такие появятся. Queue обрабатывает все закрытия и открытия замков, так что лично вам эта участь не грозит.

В данном примере мы создали список значений, которые мы хотим дублировать. Далее мы создаем два потока, один для функции creator/producer, второй для consumer (потребитель). Обратите внимание на то, что мы передаем объект Queue каждому потоку, что является прямо таки магией, учитывая то, как обрабатываются замки. Очередь начнется с первого потока, который передает данные второму. Когда первый поток передает те или иные данные в очередь, он также передает их к Event, после чего дожидается, когда произойдет события, чтобы закончить. Далее, в функции consumer, данные обрабатываются, и после этого вызывается метод настройки Event, который указывает первому потоку, что второй закончил обработку, так что он может продолжать. Последняя строка кода вызывает метод join объекта Queue, который указывает Queue подождать, пока потоки закончат обработку. Первый поток заканчивает, когда ему больше нечего передавать в Queue.

Ответы на частые вопросы о threading в Python

Вопрос: В чем разница между потоками и процессами в Python?
Ответ: Потоки выполняются в одном процессе и разделяют память, а процессы имеют изолированную память. Из-за GIL потоки в Python не дают прироста производительности для CPU-задач, в отличие от процессов.

Вопрос: Что такое GIL и как он влияет на threading?
Ответ: GIL (Global Interpreter Lock) — это блокировка, которая позволяет выполнять только один поток за раз в CPython. Из-за этого многопоточность эффективна только для I/O-bound задач.

Вопрос: Как создать и запустить поток в Python?
Ответ: Используйте класс Thread из модуля threading: создайте экземпляр, передав целевую функцию, и вызовите метод start().

Вопрос: Как дождаться завершения потока?
Ответ: Вызовите метод join() у объекта потока. Это заблокирует основной поток до завершения дочернего.

Вопрос: Что такое Lock и зачем он нужен?
Ответ: Lock (мьютекс) используется для синхронизации доступа к общим ресурсам, чтобы избежать состояния гонки (race condition).

Вопрос: Как передать аргументы в функцию потока?
Ответ: Используйте параметр args (кортеж) или kwargs (словарь) при создании объекта Thread.

Вопрос: Что такое daemon-потоки?
Ответ: Daemon-потоки работают в фоне и автоматически завершаются, когда все обычные (не-daemon) потоки завершились. Устанавливаются через параметр daemon=True.

Вопрос: Как получить результат выполнения функции в потоке?
Ответ: Прямого способа нет. Используйте очередь (queue.Queue) или передайте изменяемый объект (например, список) для сохранения результата.

Вопрос: В чем разница между threading.Thread и multiprocessing.Process?
Ответ: Thread использует потоки (одна память, GIL), Process — отдельные процессы (своя память, нет GIL). Для CPU-задач лучше Process, для I/O — Thread.

Вопрос: Что такое ThreadPoolExecutor?
Ответ: Это высокоуровневый интерфейс из модуля concurrent.futures для управления пулом потоков. Упрощает запуск и сбор результатов.