Многопоточность в python. руководство для начинающих
Содержание:
- Стоимость диспетчеризации уровня ядра
- Reusing Threads: QRunnable and QThreadPool
- Время выполнения программы
- Барьеры памяти
- Multithreading in PyQt: Best Practices
- Отличия и особенности
- Enumerating All Threads¶
- Thread Objects¶
- Перечисления
- Синтаксис менеджера контекста
- Synchronizing Threads¶
- Synchronizing Threads
- Threading with queues
- Структуры
- Сопоставление шаблонов
- Subclassing Thread
- Простая блокировка в Python
- Изменения синтаксиса псевдонима типа
- Usage
- Многопоточность на Python
- Подклассы потоков
Стоимость диспетчеризации уровня ядра
Сами объекты-диспетчеры, а это события, мьютексы, таймеры и семафоры — это объекты ядра. Они разделимы между процессами, и по этой причине считаются дорогими объектами.
Посмотрите на график, посмотрите на стоимость инкрементирования или доступа к кэшу L1, а затем — на стоимость присваивания объекта диспетчера ядра. Две наносекунды и 295 наносекунд — огромная разница! А создание объекта диспетчера вообще занимает 2093 наносекунды.
При разработке важно писать код, который не будут создавать объекты диспетчера лишний раз, иначе можно ожидать большую потерю производительности. На уровне .NET у нас есть , который использует внутренний таймер
возвращает выполняющийся поток обратно в очередь ожидания потоков. блокирует вызывающий поток. Но сейчас я хочу более подробно рассказать про .
Reusing Threads: QRunnable and QThreadPool
If your GUI applications rely heavily on multithreading, then you’ll face significant overhead related to creating and destroying threads. You’ll also have to consider how many threads you can start on a given system so that your applications remain efficient. Fortunately, PyQt’s thread support provides you with a solution to these issues, too.
Each application has a global thread pool. You can get a reference to it by calling .
Note: Even though using the default thread pool is a fairly common choice, you can also create your own thread pool by instantiating , which provides a collection of reusable threads.
The global thread pool maintains and manages a suggested number of threads generally based on the number of cores in your current CPU. It also handles the queuing and execution of tasks in your application’s threads. The threads in the pool are reusable, which prevents the overhead associated with creating and destroying threads.
To create tasks and run them in a thread pool, you use . This class represents a task or piece of code that needs to be run. The process of creating and executing runnable tasks involves three steps:
- Subclass and reimplement with the code for the task that you want to run.
- Instantiate the subclass of to create a runnable task.
- Call with the runnable task as an argument.
must contain the required code for the task at hand. The call to launches your task in one of the available threads in the pool. If there’s no available thread, then puts the task in the pool’s run queue. When a thread becomes available, the code within gets executed in that thread.
Here’s a GUI application that shows how you can implement this process in your code:
Here’s how this code works:
- On lines 19 to 28, you subclass and reimplement with the code you want to execute. In this case, you use the usual loop for simulating a long-running task. The call to notifies you about the operation’s progress by printing a message to your terminal screen.
- On line 52, you get the number of available threads. This number will depend on your specific hardware and is normally based on the cores of your CPU.
- On line 53, you update the text of the label to reflect how many threads you can run.
- On line 55, you start a loop that iterates over the available threads.
- On line 57, you instantiate , passing the loop variable as an argument to identify the current thread. Then you call on the thread pool, using your runnable task as an argument.
It’s important to note that some of the examples in this tutorial use with a to print messages to the screen. You need to do this because , so using it might cause a mess in your output. Fortunately, the functions in are thread safe, so you can use them in multithreaded applications.
If you run this application, then you’ll get the following behavior:
When you click the Click me! button, the application launches up to four threads. In the background terminal, the application reports the progress of each thread. If you close the application, then the threads will continue running until they finish their respective tasks.
There’s no way of stopping a object from the outside in Python. To work around this, you can create a global Boolean variable and systematically check it from inside your subclasses to terminate them when your variable becomes .
Another drawback of using and is that doesn’t support signals and slots, so interthread communication can be challenging.
Время выполнения программы
Существует множество разных задач, для решения которых нужно найти время, потраченное на работу программы либо отдельных ее блоков. Чтобы найти данную величину, достаточно посчитать разницу в секундах между точкой старта определенной функции и местом, где она завершает свою работу. В следующем примере демонстрируется применение методов time() для получения текущего времени, чтобы в конечном итоге выявить, как долго работал блок кода. Метод sleep() здесь увеличивает время выполнения программы на 5 секунд.
import time start = time.time() time.sleep(5) finish = time.time() result = finish - start print("Program time: " + str(result) + " seconds.") Program time: 5.005090236663818 seconds.
Несмотря на простоту вышеописанного подхода, использовать его в серьезных целях, где требуется точный и независимый от ОС результат, не рекомендуется. Все дело в том, что числовое значение времени, получаемое таким образом, может иметь погрешности за счет внутренних особенностей работы компьютера, в среде которого выполняется программа. Например, системные часы могут быть подкорректированы вручную пользователем во время выполнения программы. Корректировка может произойти и автоматически, если настроена синхронизация системных часов по NTP.
Таким образом, может случиться такая ситуация, что функция вернет значение меньшее, чем при предыдущем вызове.
monotonic
По этой причине лучше всего использовать метод monotonic(), впервые появившийся в версии Python 3.3 на некоторых платформах, а начиная с выпуска 3.5 ставший доступным абсолютно везде. Его главной особенностью является точное представление измеряемого количества времени, вне зависимости от работы ОС и текущей платформы. Используемый таймер никогда не вернет при повторном вызове метода значение, которое будет меньше предыдущего. Это позволяет избежать многих ошибок, а также неожиданного поведения.
import time start = time.monotonic() time.sleep(15) result = time.monotonic() - start print("Program time: {:>.3f}".format(result) + " seconds.") Program time: 15.012 seconds.
В примере программы, описанном выше, продемонстрирована работа функции monotonic(). Принцип ее применения такой же, как и в предыдущем примере, однако здесь результат обладает гарантированной точностью и независим от внешних условий. Для отображения времени в более комфортной форме используется небольшое форматирование результата.
Барьеры памяти
Помимо оптимизации процессора, существует ещё оптимизация компилятора и среды выполнения. В контексте данной статьи под компилятором и средой я буду понимать JIT и CLR. Среда может кэшировать значения в регистрах процессора, переупорядочить операции и объединять операции записи (coalesce writes).
Например, для цикла for CLR может решить заранее рассчитать значение и вынести локальную переменную за пределы цикла. Во избежание этого, конечно же, можно использовать ключевое слово volatile, но бывают и более сложные случаи.
Иногда бывает нужно, чтобы весь код был точно выполнен до какой-либо определенной инструкции, и для этого используются барьеры памяти. Барьер памяти — это инструкция, которая реализуется процессором. В .NET она доступна с помощью вызова . И что же он делает? Вызов этого метода гарантирует, что все операции перед этим методом точно завершились.
Давайте снова вернемся к программе с двумя переменными. Предположим, что эти процессоры выполняют команды «сохранить» A и «загрузить» B. Если между этими двумя командами находится барьер памяти, то процессор сначала отправит запрос «сохранить» B, подождет, пока она не выполнится. Барьеры памяти дают возможность поставить что-то вроде чекпоинта или коммита, как в базе данных.
Есть еще одна причина использовать барьеры памяти. Если вы пишете код на 32-битной машине с использованием long, DateTime или struct, то атомарность выполнения операций может нарушаться. Это значит, что даже когда в коде записана одна инструкция, на самом деле могут произойти две операции вместо одной, причем они могут выполняться в разное время.
Multithreading in PyQt: Best Practices
There are a few best practices that you can apply when building multithreaded applications in PyQt. Here’s a non-exhaustive list:
- Avoid launching long-running tasks in the main thread of a PyQt application.
- Use and objects to create worker threads.
- Use and if you need to manage a pool of worker threads.
- Use signals and slots to establish safe interthread communication.
- Use , , or to prevent threads from accessing shared data and resources concurrently.
- Make sure to unlock or release , , or before finishing a thread.
- Release the lock in all possible execution paths in functions with multiple statements.
- Don’t try to create, access, or update GUI components or widgets from a worker thread.
- Don’t try to move a with a parent-child relationship to a different thread.
Отличия и особенности
JavaScript прекрасен тем, что его можно запустить в консоли любого современного браузера. Это для него родная среда, и JS легко работает со страницами, объектами на ней, вкладками браузера и всем, что с ним связано.
Python — более универсальный язык, который работает не только с браузерами, поэтому для него нужен отдельный интерпретатор. Интерпретатор — это программа, которая берёт исходный код и выполняет команду за командой. Вы можете написать отличный код, но чтобы его исполнить, вам всегда нужен будет интерпретатор.
Есть два способа запустить Python-код:
1. Поставить Python себе на компьютер — этот способ хорош, если вы решили основательно изучить язык или просто любите, когда всё быстро и под контролем. Скачать Python можно с официального сайта — есть версии для всех основных операционных систем. Из минусов — нужно разбираться в параметрах установки и настройки и уметь работать с командной строкой. Плюсы — полный контроль и быстродействие.
2. Использовать онлайн-сервисы, например, этот: onlinegdb.com/online_python_compiler. Работает точно так же — пишете код, нажимаете кнопку Run и смотрите на результат.
Минусы: так как это онлайн-сервис, им пользуется одновременно много человек, поэтому быстродействия от него ждать не стоит. С подключением внешних модулей тоже могут возникнуть проблемы, но с этим можно разобраться, если потратить немного времени.
Плюс: не нужно ничего настраивать и устанавливать, всё работает сразу из браузера. Есть подсветка синтаксиса, сообщения об ошибках и возможность сохранения кода.
Сейчас мы напишем таймер с оглядкой на онлайновый сервис. А отдельно ещё расскажем об установке.
Enumerating All Threads¶
It is not necessary to retain an explicit handle to all of the daemon
threads in order to ensure they have completed before exiting the main
process. enumerate() returns a list of active Thread
instances. The list includes the current thread, and since joining the
current thread is not allowed (it introduces a deadlock situation), it
must be skipped.
import random import threading import time import logging logging.basicConfig(level=logging.DEBUG, format='(%(threadName)-10s) %(message)s', ) def worker(): """thread worker function""" t = threading.currentThread() pause = random.randint(1,5) logging.debug('sleeping %s', pause) time.sleep(pause) logging.debug('ending') return for i in range(3): t = threading.Thread(target=worker) t.setDaemon(True) t.start() main_thread = threading.currentThread() for t in threading.enumerate(): if t is main_thread continue logging.debug('joining %s', t.getName()) t.join()
Since the worker is sleeping for a random amount of time, the output
from this program may vary. It should look something like this:
Thread Objects¶
The simplest way to use a Thread is to instantiate it with a
target function and call start() to let it begin working.
import threading def worker(): """thread worker function""" print 'Worker' return threads = [] for i in range(5): t = threading.Thread(target=worker) threads.append(t) t.start()
The output is five lines with "Worker" on each:
$ python threading_simple.py Worker Worker Worker Worker Worker
It useful to be able to spawn a thread and pass it arguments to tell
it what work to do. This example passes a number, which the thread
then prints.
import threading def worker(num): """thread worker function""" print 'Worker: %s' % num return threads = [] for i in range(5): t = threading.Thread(target=worker, args=(i,)) threads.append(t) t.start()
The integer argument is now included in the message printed by each
thread:
Перечисления
ApartmentState |
Задает апартаментное состояние потока Thread.Specifies the apartment state of a Thread. |
EventResetMode |
Указывает, сбрасывается ли EventWaitHandle автоматически или вручную после получения сигнала.Indicates whether an EventWaitHandle is reset automatically or manually after receiving a signal. |
LazyThreadSafetyMode |
Определяет, как экземпляр Lazy<T> синхронизирует доступ из нескольких потоков.Specifies how a Lazy<T> instance synchronizes access among multiple threads. |
LockRecursionPolicy |
Указывает, можно ли несколько раз войти в блокировку из одного и того же потока.Specifies whether a lock can be entered multiple times by the same thread. |
ThreadPriority |
Задает приоритет выполнения потока Thread.Specifies the scheduling priority of a Thread. |
ThreadState |
Задает состояния выполнения объекта Thread.Specifies the execution states of a Thread. |
Синтаксис менеджера контекста
Контекстные менеджеры отлично подходят, чтобы открывать и закрывать файлы, работать с соединениями баз данных и делать многое другое, а в Python 3.10 они станут немного удобнее. Изменение позволяет в скобках указывать несколько контекстных менеджеров, что удобно, если вы хотите создать в одном операторе with несколько менеджеров:
В коде выше видно, что мы даже можем ссылаться на переменную, созданную одним контекстным менеджером (… as some_file) в следующем за ним менеджере!
Это всего лишь два из многих новых форматов в Python 3.10. Улучшенный синтаксис довольно гибок, поэтому я не буду утруждать себя и показать все возможные варианты; я почти уверен, что новый Python обработает всё, что вы ему скормите.
Synchronizing Threads¶
In addition to using Events, another way of synchronizing
threads is through using a Condition object. Because the
Condition uses a Lock, it can be tied to a shared
resource. This allows threads to wait for the resource to be updated.
In this example, the consumer() threads wait() for the
Condition to be set before continuing. The producer()
thread is responsible for setting the condition and notifying the
other threads that they can continue.
import logging import threading import time logging.basicConfig(level=logging.DEBUG, format='%(asctime)s (%(threadName)-2s) %(message)s', ) def consumer(cond): """wait for the condition and use the resource""" logging.debug('Starting consumer thread') t = threading.currentThread() with cond cond.wait() logging.debug('Resource is available to consumer') def producer(cond): """set up the resource to be used by the consumer""" logging.debug('Starting producer thread') with cond logging.debug('Making resource available') cond.notifyAll() condition = threading.Condition() c1 = threading.Thread(name='c1', target=consumer, args=(condition,)) c2 = threading.Thread(name='c2', target=consumer, args=(condition,)) p = threading.Thread(name='p', target=producer, args=(condition,)) c1.start() time.sleep(2) c2.start() time.sleep(2) p.start()
The threads use with to acquire the lock associated with
the Condition. Using the acquire() and
release() methods explicitly also works.
Synchronizing Threads
The threading module provided with Python includes a simple-to-implement locking mechanism that allows you to synchronize threads. A new lock is created by calling the Lock() method, which returns the new lock.
The acquire(blocking) method of the new lock object is used to force threads to run synchronously. The optional blocking parameter enables you to control whether the thread waits to acquire the lock.
If blocking is set to 0, the thread returns immediately with a 0 value if the lock cannot be acquired and with a 1 if the lock was acquired. If blocking is set to 1, the thread blocks and wait for the lock to be released.
The release() method of the new lock object is used to release the lock when it is no longer required.
Example
#!/usr/bin/python import threading import time class myThread (threading.Thread): def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): print "Starting " + self.name # Get lock to synchronize threads threadLock.acquire() print_time(self.name, self.counter, 3) # Free lock to release next thread threadLock.release() def print_time(threadName, delay, counter): while counter: time.sleep(delay) print "%s: %s" % (threadName, time.ctime(time.time())) counter -= 1 threadLock = threading.Lock() threads = [] # Create new threads thread1 = myThread(1, "Thread-1", 1) thread2 = myThread(2, "Thread-2", 2) # Start new Threads thread1.start() thread2.start() # Add threads to thread list threads.append(thread1) threads.append(thread2) # Wait for all threads to complete for t in threads: t.join() print "Exiting Main Thread"
When the above code is executed, it produces the following result −
Starting Thread-1 Starting Thread-2 Thread-1: Thu Mar 21 09:11:28 2013 Thread-1: Thu Mar 21 09:11:29 2013 Thread-1: Thu Mar 21 09:11:30 2013 Thread-2: Thu Mar 21 09:11:32 2013 Thread-2: Thu Mar 21 09:11:34 2013 Thread-2: Thu Mar 21 09:11:36 2013 Exiting Main Thread
Threading with queues
we use the modules threading and queue to start several threads. A queue is created by calling Queue(). Then we have an array of urls as parameter for thread construction.
Threads are created in the for loop using the call threading.Thread(). Then they have to be started by calling thead.start(). Every thread calls the method getUrl.
import queueimport threadingimport urllib.requestdef getUrl(q, url): print('getUrl('+url+') called from a thead.') q.put(urllib.request.urlopen(url).read())theurls = ["http://google.com", "http://google.de","http://google.ca"]threadQueue = queue.Queue()for u in theurls: t = threading.Thread(target=getUrl, args = (threadQueue,u)) t.daemon = True t.start()output = threadQueue.get() |
Graphically this is what happens. Your program (the main thread) starts several new threads which execute a task “in parallel”.
A program can have any number of threads, each of which work on some data.
If you are a Python beginner, then I highly recommend this book.
Структуры
AsyncFlowControl |
Обеспечивает функциональность для восстановления миграции или перемещения контекста выполнения между потоками.Provides the functionality to restore the migration, or flow, of the execution context between threads. |
AsyncLocalValueChangedArgs<T> |
Класс, предоставляющий сведения об изменениях данных экземплярам AsyncLocal<T>, которые зарегистрированы для получения уведомлений об изменениях.The class that provides data change information to AsyncLocal<T> instances that register for change notifications. |
CancellationToken |
Распространяет уведомление о том, что операции следует отменить.Propagates notification that operations should be canceled. |
CancellationTokenRegistration |
Представляет делегат обратного вызова, зарегистрированный с объектом CancellationToken.Represents a callback delegate that has been registered with a CancellationToken. |
LockCookie |
Определяет блокировку, которая реализует семантику «один записывающий / много читающих».Defines the lock that implements single-writer/multiple-reader semantics. Это тип значения.This is a value type. |
NativeOverlapped |
Содержит явно заданный макет, видимый из неуправляемого кода и имеющий тот же макет, что и структура OVERLAPPED Win32, с дополнительными зарезервированными полями в конце.Provides an explicit layout that is visible from unmanaged code and that will have the same layout as the Win32 OVERLAPPED structure with additional reserved fields at the end. |
SpinLock |
Предоставляет примитив взаимно исключающей блокировки, в котором поток, пытающийся получить блокировку, ожидает в состоянии цикла, проверяя доступность блокировки.Provides a mutual exclusion lock primitive where a thread trying to acquire the lock waits in a loop repeatedly checking until the lock becomes available. |
SpinWait |
Предоставляет поддержку ожидания на основе прокруток.Provides support for spin-based waiting. |
Сопоставление шаблонов
Одна масштабная фича, о которой вы, конечно, слышали, — это структурное сопоставление шаблонов, добавляющее оператор известное выражение case из других языков. Мы знаем, как работать с case, но посмотрите на вариацию в Python это не просто switch/case, но также несколько мощных особенностей, которые мы должны исследовать.
Простое сопоставление шаблонов состоит из ключевого слова match, за которым следует выражение, а его результат проверяется на соответствие шаблонам, указанным в последовательных операторах case:
В этом простом примере мы воспользовались переменной day как выражением, которое затем сравнивается с конкретными строками в case. Кроме строк, вы также можете заметить case с маской _ — это эквивалент ключевого слова default в других языках. Хотя этот оператор можно опустить, в этом случае может произойти no-op, по существу это означает, что вернётся None.
Еще один момент, на который стоит обратить внимание в коде выше, это оператор |, позволяющий комбинировать несколько литералов | (другой его вариант — or). Как я уже упоминал, новое сопоставление шаблонов не заканчивается на базовом синтаксисе, напротив — оно привносит дополнительные возможности, например сопоставление сложных шаблонов:
Как я уже упоминал, новое сопоставление шаблонов не заканчивается на базовом синтаксисе, напротив — оно привносит дополнительные возможности, например сопоставление сложных шаблонов:
Во фрагменте выше мы воспользовались кортежем как выражением сопоставления. Однако мы не ограничены кортежами: работать будет любой итерируемый тип. Также выше видно, что маска (wildcard) _ может применяться внутри сложных шаблонов и не только сама по себе, как в предыдущих примерах. Простые кортежи или списки — не всегда лучший подход, поэтому, если вы предпочитаете классы, код можно переписать так:
Здесь видно, что с шаблонами, написанными в стиле конструкторов, можно сопоставить атрибуты класса. При использовании этого подхода отдельные атрибуты также попадают в переменные (как и в показанные ранее кортежи), с которыми затем можно работать в соответствующем операторе case.
Выше мы можем увидеть другие особенности сопоставления шаблонов: во-первых выражение в case — это гард, который также является условием в if. Это полезно, когда сопоставления по значению не достаточно и вам нужны дополнительные проверки. Посмотрите на оставшееся выражение case: видно, что и ключевые слова, (name-name) и позиционные аргументы работают с синтаксисом, похожим на синтаксис конструкторов; то же самое верно для маски _ (или отбрасываемой переменной).
Сопоставление шаблонов также позволяет работать с вложенными шаблонами. Вложенные шаблоны могут использовать любой итерируемый тип: и конструируемый объект, и несколько таких объектов, которые возможно итерировать:
В таких сложных шаблонах для дальнейшей обработки может быть полезно записать подшаблон в переменную. Это можно сделать с помощью ключевого слова as, как показано выше, во втором case.
Subclassing Thread
It is also possible to start a thread by subclassing threading.Thread. Depending on the design of your application, you may prefer this approach. Here, you extend threading.Thread and provide the implementation of your task in the run() method.
import threading import random, time class MyTask(threading.Thread): def __init__(self, sleepFor): self.secs = sleepFor threading.Thread.__init__(self) def run(self): print self, 'begin sleep(', self.secs, ')' time.sleep(self.secs) print self, 'end sleep(', self.secs, ')'
And here is the usage of the class defined above.
tasks = [] for x in xrange(0, 5): t = MyTask(random.randint(1, 10)) tasks.append(t) t.start() print 'joining ..' while threading.active_count() > 1: for t in tasks: t.join() print t, 'is done.' print 'all done.'
Простая блокировка в Python
Взаимоисключение (mutual exception, кратко — mutex) — простейшая блокировка, которая на время работы потока с ресурсом закрывает последний от других обращений. Реализуют это с помощью класса Lock.
import threading mutex = threading.Lock()
Мы создали блокировку с именем mutex, но могли бы назвать её lock или иначе. Теперь её можно ставить и снимать методами .acquire() и .release():
resource = def thread_safe_function(): global resource for i in range(1000000): mutex.acquire() # Делаем что-то с переменной resource mutex.release()
Обратите внимание: обойти простую блокировку не может даже поток, который её активировал. Он будет заблокирован, если попытается повторно захватить ресурс, который удерживает
Изменения синтаксиса псевдонима типа
В более ранних версиях Python добавлены псевдонимы типов, позволяющие создавать синонимы пользовательских классов. В Python 3.9 и более ранних версиях псевдонимы записывались так:
Здесь FileName — псевдоним базового типа строки Python. Однако, начиная с Python 3.10, синтаксис определения псевдонимов типов будет изменён:
Благодаря этому простому изменению и программистам, и инструментам проверки типа проще отличить присваивание переменной от псевдонима. Новый синтаксис обратно совместим, так что вам не нужно обновлять старый код с псевдонимами.
Кроме этих 2 изменений появилось другое улучшение модуля typing — в предложениях по улучшению номер 612 оно называется Parameter Specification Variables. Однако это не то, что вы найдете в основной массе кода на Python, поскольку эта функциональность используется для пересылки параметра типов от одного вызываемого типа к другому вызываемому типу, например, в декораторах. Если вам есть где применить эту функциональность, посмотрите её PEP.
Usage
ThreadPooled
Mostly it is required decorator: submit function to ThreadPoolExecutor on call.
Note
API quite differs between Python 3 and Python 2.7. See API section below.
threaded.ThreadPooled.configure(max_workers=3)
Note
By default, if executor is not configured — it configures with default parameters: max_workers=CPU_COUNT * 5
@threaded.ThreadPooled def func(): pass concurrent.futures.wait()
Python 3.5+ usage with asyncio:
Note
if loop_getter is not callable, loop_getter_need_context is ignored.
loop = asyncio.get_event_loop() @threaded.ThreadPooled(loop_getter=loop, loop_getter_need_context=False) def func(): pass loop.run_until_complete(asyncio.wait_for(func(), timeout))
Python 3.5+ usage with asyncio and loop extraction from call arguments:
loop_getter = lambda tgt_loop tgt_loop @threaded.ThreadPooled(loop_getter=loop_getter, loop_getter_need_context=True) # loop_getter_need_context is required def func(*args, **kwargs): pass loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait_for(func(loop), timeout))
During application shutdown, pool can be stopped (while it will be recreated automatically, if some component will request).
threaded.ThreadPooled.shutdown()
Threaded
Classic threading.Thread. Useful for running until close and self-closing threads without return.
Usage example:
@threaded.Threaded def func(*args, **kwargs): pass thread = func() thread.start() thread.join()
Without arguments, thread name will use pattern: 'Threaded: ' + func.__name__
Note
If func.__name__ is not accessible, str(hash(func)) will be used instead.
Override name can be don via corresponding argument:
@threaded.Threaded(name='Function in thread') def func(*args, **kwargs): pass
Thread can be daemonized automatically:
@threaded.Threaded(daemon=True) def func(*args, **kwargs): pass
Also, if no any addition manipulations expected before thread start,
it can be started automatically before return:
@threaded.Threaded(started=True) def func(*args, **kwargs): pass
Многопоточность на Python
За потоки в Python отвечает модуль threading, а сам поток можно создать с помощью класса Thread из этого модуля. Подключается он так:
from threading import Thread
После этого с помощью функции Thread() мы сможем создать столько потоков, сколько нам нужно. Логика работы такая:
- Подключаем нужный модуль и класс Thread.
- Пишем функции, которые нам нужно выполнять в потоках.
- Создаём новую переменную — поток, и передаём в неё название функции и её аргументы. Один поток = одна функция на входе.
- Делаем так столько потоков, сколько требует логика программы.
- Потоки сами следят за тем, закончилась в них функция или нет. Пока работает функция — работает и поток.
- Всё это работает параллельно и (в теории) не мешает друг другу.
Для иллюстрации запустим такой код:
А вот как выглядит результат. Обратите внимание — потоки просыпаются не в той последовательности, как мы их запустили, а в той, в какой их выполнил процессор. Иногда это может помешать работе программы, но про это мы поговорим отдельно в другой статье.
Подклассы потоков
При запуске Thread выполняет базовую инициализацию и затем вызывает свой метод run(). Он в свою очередь вызывает целевую функцию, переданную конструктору. Чтобы создать подкласс Thread, переопределите run().
import threading import logging logging.basicConfig(level=logging.DEBUG, format='(%(threadName)-10s) %(message)s', ) class MyThread(threading.Thread): def run(self): logging.debug('running') return for i in range(5): t = MyThread() t.start()
Возвращаемое значение метода run() игнорируется.
$ python threading_subclass.py (Thread-1 ) running (Thread-2 ) running (Thread-3 ) running (Thread-4 ) running (Thread-5 ) running
Значения args и kwargs, передаваемые в конструктор Thread, сохраняются в private переменных. Поэтому к ним трудно получить доступ из подкласса.
Для передачи аргументов пользовательскому потоку, переопределите конструктор, чтобы сохранить значения в атрибуте экземпляра.
import threading import logging logging.basicConfig(level=logging.DEBUG, format='(%(threadName)-10s) %(message)s', ) class MyThreadWithArgs(threading.Thread): def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, verbose=None): threading.Thread.__init__(self, group=group, target=target, name=name, verbose=verbose) self.args = args self.kwargs = kwargs return def run(self): logging.debug('running with %s and %s', self.args, self.kwargs) return for i in range(5): t = MyThreadWithArgs(args=(i,), kwargs={'a':'A', 'b':'B'}) t.start()
MyThreadWithArgs использует тот же API, что и Thread. Но другой класс может легко изменить метод конструктора, чтобы принимать другие аргументы, связанные с назначением потока.
$ python threading_subclass_args.py (Thread-1 ) running with (0,) and {'a': 'A', 'b': 'B'} (Thread-2 ) running with (1,) and {'a': 'A', 'b': 'B'} (Thread-3 ) running with (2,) and {'a': 'A', 'b': 'B'} (Thread-4 ) running with (3,) and {'a': 'A', 'b': 'B'} (Thread-5 ) running with (4,) and {'a': 'A', 'b': 'B'}