Многопоточность в Python
Многопоточность похожа на одновременное выполнение нескольких различных программ. Запуск нескольких потоков имеет следующие преимущества:
- Использование потоков позволяет помещать задачи, занимающие много времени в программе, в фоновый режим для обработки.
- Пользовательский интерфейс может быть более привлекательным. Например, когда пользователь нажимает кнопку, чтобы запустить обработку определенных событий, может появиться индикатор выполнения, показывающий прогресс обработки.
- Скорость работы программы может быть ускорена.
- Потоки более полезны для реализации некоторых задач ожидания, таких как ввод пользователя, чтение и запись файлов, отправка/получение сетевых данных. В таких случаях мы можем освободить некоторые ценные ресурсы, например, объем используемой памяти.
Потоки все еще отличаются от процессов во время выполнения. Каждый независимый процесс имеет точку входа для выполнения программы, последовательный порядок выполнения и точку выхода программы. Однако потоки не могут выполняться независимо; они должны зависеть от приложения, которое обеспечивает управление выполнением нескольких потоков.
Каждый поток имеет собственный набор регистров ЦП, называемый контекстом потока. Этот контекст отражает состояние регистров ЦП во время последнего выполнения потока.
Регистры указателя инструкций и указателя стека — это два наиболее важных регистра в контексте потока. Поток всегда выполняется в контексте процесса, и эти адреса используются для идентификации памяти в адресном пространстве процесса, которому принадлежит поток.
- Потоки могут быть вытеснены (прерваны).
- Пока выполняются другие потоки, поток может быть временно приостановлен (также называемый спящим) -- это уступка потока.
Начните изучать потоки Python
В Python есть два способа использования потоков: с помощью функций или с помощью класса, оборачивающего объекты потоков.
Функциональный подход: вызовите функцию start_new_thread() в модуле thread, чтобы создать новый поток. Синтаксис следующий:
thread.start_new_thread ( function, args[, kwargs] )
Описание параметра:
- function - Функция потока.
- args - аргументы, передаваемые функции потока; должен быть типа tuple.
- kwargs - необязательные аргументы.
Пример (Python 2.0+)
Вывод выполнения вышеуказанной программы выглядит следующим образом:
Thread-1: Thu Jan 22 15:42:17 2009 Thread-1: Thu Jan 22 15:42:19 2009 Thread-2: Thu Jan 22 15:42:19 2009 Thread-1: Thu Jan 22 15:42:21 2009 Thread-2: Thu Jan 22 15:42:23 2009 Thread-1: Thu Jan 22 15:42:23 2009 Thread-1: Thu Jan 22 15:42:25 2009 Thread-2: Thu Jan 22 15:42:27 2009 Thread-2: Thu Jan 22 15:42:31 2009 Thread-2: Thu Jan 22 15:42:35 2009
Завершение потока обычно происходит при естественном завершении функции потока; также можно вызвать thread.exit() в функции потока, что вызывает исключение SystemExit для достижения цели выхода из потока.
Модуль thread
Python поддерживает потоки через две стандартные библиотеки: thread и threading. thread предоставляет низкоуровневые примитивные потоки и простую блокировку.
Другие методы, предоставляемые модулем threading:
- threading.currentThread(): возвращает переменную текущего потока.
- threading.enumerate(): возвращает список, содержащий работающие потоки. Работающие означает после запуска потока и до его завершения, не включая потоки до запуска и после завершения.
- threading.activeCount(): возвращает количество работающих потоков, тот же результат, что и len(threading.enumerate()).
Помимо использования методов, модуль thread также предоставляет класс Thread для работы с потоками. Класс Thread предоставляет следующие методы:
- run():Метод, используемый для представления активности потока.
- start():Запускает активность потока.
- join([time]):Ожидает завершения потока. Это блокирует вызывающий поток до тех пор, пока поток, для которого вызван метод join(), не завершится — при нормальном выходе или необработанном исключении — либо не истечёт необязательный тайм-аут.
- isAlive():Возвращает, активен ли поток.
- getName():Возвращает имя потока.
- setName():Задаёт имя потока.
Создание потоков с помощью модуля Threading
Чтобы создать поток с помощью модуля Threading, напрямую унаследуйте от threading.Thread, а затем переопределите метод __init__ и метод run:
Пример (Python 2.0+)
Результат выполнения вышеприведённой программы выглядит следующим образом:
Starting Thread-1 Starting Thread-2 Exiting Main Thread Thread-1: Thu Mar 21 09:10:03 2013 Thread-1: Thu Mar 21 09:10:04 2013 Thread-2: Thu Mar 21 09:10:04 2013 Thread-1: Thu Mar 21 09:10:05 2013 Thread-1: Thu Mar 21 09:10:06 2013 Thread-2: Thu Mar 21 09:10:06 2013 Thread-1: Thu Mar 21 09:10:07 2013 Exiting Thread-1 Thread-2: Thu Mar 21 09:10:08 2013 Thread-2: Thu Mar 21 09:10:10 2013 Thread-2: Thu Mar 21 09:10:12 2013 Exiting Thread-2
Синхронизация потоков
Если несколько потоков совместно изменяют один и тот же фрагмент данных, могут возникнуть непредсказуемые результаты. Чтобы обеспечить корректность данных, несколько потоков необходимо синхронизировать.
Использование Lock и RLock объектов Thread позволяет реализовать простую синхронизацию потоков. Оба этих объекта имеют методы acquire и release. Для данных, с которыми в каждый момент времени должен работать только один поток, операции можно разместить между методами acquire и release. Как показано ниже:
Преимущество многопоточности в том, что она может выполнять несколько задач одновременно (по крайней мере, так кажется). Однако когда потокам нужно совместно использовать данные, могут возникнуть проблемы с рассогласованием данных.
Рассмотрим такую ситуацию: все элементы в списке равны 0. Поток "set" изменяет все элементы на 1 с конца к началу, а поток "print" отвечает за чтение списка с начала к концу и вывод на печать.
Тогда возможно, что когда поток "set" начинает изменение, поток "print" приступает к печати списка, и вывод становится наполовину из 0, наполовину из 1. Это рассогласование данных. Чтобы избежать этой ситуации, вводится понятие блокировок.
Блокировка имеет два состояния — заблокирована и разблокирована. Всякий раз, когда поток, например "set", хочет получить доступ к общим данным, он должен сначала захватить блокировку; если другой поток, например "print", уже захватил блокировку, то потоку "set" нужно приостановиться, что является синхронным блокированием; после того как поток "print" завершит доступ и освободит блокировку, потоку "set" разрешается продолжить.
При такой обработке при печати списка выводятся либо все 0, либо все 1, и неловкая ситуация с половиной 0 и половиной 1 больше не возникнет.
Пример (Python 2.0+)
Очередь с приоритетом потоков
Модуль Queue в Python предоставляет синхронизированные, потокобезопасные классы очередей, включая очередь FIFO (первым пришёл — первым ушёл) Queue, очередь LIFO (последним пришёл — первым ушёл) LifoQueue и очередь с приоритетом PriorityQueue. Все эти очереди реализуют примитивы блокировки и могут использоваться непосредственно в многопоточности. Очереди можно использовать для достижения синхронизации между потоками.
Общие методы в модуле Queue:
- Queue.qsize() Возвращает размер очереди
- Queue.empty() Если очередь пуста, возвращает True, иначе False
- Queue.full() Если очередь полна, возвращает True, иначе False
- Queue.full соответствует размеру maxsize
- Queue.get([block[, timeout]]) Получает из очереди, timeout — время ожидания
- Queue.get_nowait() Эквивалентно Queue.get(False)
- Queue.put(item, block=True, timeout=None) Записывает в очередь, timeout — время ожидания
- Queue.put_nowait(item) Эквивалентно Queue.put(item, False)
- Queue.task_done() После завершения работы функция Queue.task_done() отправляет в очередь сигнал о том, что задача завершена.
- Queue.join() Фактически означает ожидание, пока очередь не опустеет, прежде чем выполнять другие операции.
Пример (Python 2.0+)
Результат выполнения вышеуказанной программы:
Starting Thread-1 Starting Thread-2 Starting Thread-3 Thread-1 processing One Thread-2 processing Two Thread-3 processing Three Thread-1 processing Four Thread-2 processing Five Exiting Thread-3 Exiting Thread-1 Exiting Thread-2 Exiting Main ThreadДругие расширения