В реальной жизни, если команда людей работает над общей задачей, между ними должна быть связь для правильного выполнения задачи. Та же аналогия применима и к потокам. В программировании, чтобы уменьшить идеальное время процессора, мы создаем несколько потоков и назначаем разные подзадачи каждому потоку. Следовательно, должны быть средства связи, и они должны взаимодействовать друг с другом, чтобы завершить работу синхронизированным образом.
Рассмотрим следующие важные моменты, связанные с взаимосвязью потоков:
-
Нет выигрыша в производительности — если мы не можем добиться правильной связи между потоками и процессами, выигрыш в производительности от параллелизма и параллелизма не имеет смысла.
-
Выполнить задачу должным образом — Без надлежащего механизма взаимодействия между потоками назначенная задача не может быть выполнена должным образом.
-
Более эффективный, чем межпроцессное взаимодействие. Межпотоковое взаимодействие является более эффективным и простым в использовании, чем межпроцессное взаимодействие, поскольку все потоки в рамках процесса совместно используют одно и то же адресное пространство, и им не нужно использовать общую память.
Нет выигрыша в производительности — если мы не можем добиться правильной связи между потоками и процессами, выигрыш в производительности от параллелизма и параллелизма не имеет смысла.
Выполнить задачу должным образом — Без надлежащего механизма взаимодействия между потоками назначенная задача не может быть выполнена должным образом.
Более эффективный, чем межпроцессное взаимодействие. Межпотоковое взаимодействие является более эффективным и простым в использовании, чем межпроцессное взаимодействие, поскольку все потоки в рамках процесса совместно используют одно и то же адресное пространство, и им не нужно использовать общую память.
Структуры данных Python для поточно-ориентированной связи
Многопоточный код сталкивается с проблемой передачи информации из одного потока в другой поток. Стандартные коммуникационные примитивы не решают эту проблему. Следовательно, нам нужно реализовать наш собственный составной объект, чтобы делить объекты между потоками, чтобы сделать потокобезопасным взаимодействие. Ниже приведены несколько структур данных, которые обеспечивают потокобезопасную связь после внесения в них некоторых изменений:
наборы
Для использования заданной структуры данных в поточно-ориентированном режиме нам необходимо расширить класс set для реализации нашего собственного механизма блокировки.
пример
Вот пример расширения класса в Python —
class extend_class(set): def __init__(self, *args, **kwargs): self._lock = Lock() super(extend_class, self).__init__(*args, **kwargs) def add(self, elem): self._lock.acquire() try: super(extend_class, self).add(elem) finally: self._lock.release() def delete(self, elem): self._lock.acquire() try: super(extend_class, self).delete(elem) finally: self._lock.release()
В приведенном выше примере был определен объект класса extended_class , который унаследован от класса набора Python. Объект блокировки создается в конструкторе этого класса. Теперь есть две функции — add () и delete () . Эти функции определены и являются потокобезопасными. Они оба полагаются на функциональность суперкласса с одним ключевым исключением.
декоратор
Это еще один ключевой метод для потоковой связи — использование декораторов.
пример
Рассмотрим пример Python, который показывает, как использовать декораторы & mminus;
def lock_decorator(method): def new_deco_method(self, *args, **kwargs): with self._lock: return method(self, *args, **kwargs) return new_deco_method class Decorator_class(set): def __init__(self, *args, **kwargs): self._lock = Lock() super(Decorator_class, self).__init__(*args, **kwargs) @lock_decorator def add(self, *args, **kwargs): return super(Decorator_class, self).add(elem) @lock_decorator def delete(self, *args, **kwargs): return super(Decorator_class, self).delete(elem)
В приведенном выше примере был определен метод декоратора с именем lock_decorator, который также наследуется от класса метода Python. Затем в конструкторе этого класса создается объект блокировки. Теперь есть две функции — add () и delete (). Эти функции определены и являются потокобезопасными. Они оба полагаются на функциональность суперкласса с одним ключевым исключением.
Списки
Структура данных списка является поточно-ориентированной, быстрой и простой структурой для временного хранения в памяти. В Cpython GIL защищает от одновременного доступа к ним. Как мы узнали, списки являются поточно-ориентированными, но как насчет данных, лежащих в них. На самом деле, данные списка не защищены. Например, L.append (x) не гарантирует возврата ожидаемого результата, если другой поток пытается сделать то же самое. Это потому, что, хотя append () является атомарной операцией и поточно-ориентированной, но другой поток пытается изменить данные списка одновременно, поэтому мы можем видеть побочные эффекты условий гонки на выходе.
Чтобы решить эту проблему и безопасно изменить данные, мы должны реализовать надлежащий механизм блокировки, который дополнительно гарантирует, что несколько потоков не могут потенциально работать в условиях гонки. Чтобы реализовать правильный механизм блокировки, мы можем расширить класс, как мы это делали в предыдущих примерах.
Некоторые другие атомарные операции над списками следующие:
L.append(x) L1.extend(L2) x = L[i] x = L.pop() L1[i:j] = L2 L.sort() x = y x.field = y D[x] = y D1.update(D2) D.keys()
Здесь —
- L, L1, L2 все списки
- D, D1, D2 — это диктанты
- х, у объекты
- я, j являются целыми
Очереди
Если данные списка не защищены, нам, возможно, придется столкнуться с последствиями. Мы можем получить или удалить неправильный элемент данных о состоянии гонки. Вот почему рекомендуется использовать структуру данных очереди. Реальным примером очереди может быть дорога с односторонним движением с односторонним движением, на которой транспортное средство въезжает первым, выезжает первым. Более реальные примеры можно увидеть в очередях в кассах и автобусных остановках.
По умолчанию очереди представляют собой поточно-ориентированную структуру данных, и нам не нужно беспокоиться о реализации сложного механизма блокировки. Python предоставляет нам
Типы очередей
В этом разделе мы заработаем о различных типах очередей. Python предоставляет три варианта очередей для использования из модуля <queue> —
- Нормальные очереди (FIFO, первый на первом)
- LIFO, последний на первом
- приоритет
Мы узнаем о различных очередях в следующих разделах.
Нормальные очереди (FIFO, первый на первом)
Это наиболее часто используемые реализации очереди, предлагаемые Python. В этом механизме очередей, кто бы ни пришел первым, сначала получит услугу. FIFO также называют обычными очередями. Очереди FIFO могут быть представлены следующим образом:
Реализация Python FIFO Queue
В Python очередь FIFO может быть реализована как с одним потоком, так и с многопоточностью.
FIFO очередь с одним потоком
Для реализации очереди FIFO с одним потоком в классе Queue будет реализован базовый контейнер типа «первым пришел — первым вышел». Элементы будут добавлены к одному «концу» последовательности, используя put () , и удалены с другого конца, используя get () .
пример
Ниже приведена программа на Python для реализации очереди FIFO с одним потоком:
import queue q = queue.Queue() for i in range(8): q.put("item-" + str(i)) while not q.empty(): print (q.get(), end = " ")
Выход
item-0 item-1 item-2 item-3 item-4 item-5 item-6 item-7
Вывод показывает, что вышеприведенная программа использует один поток для иллюстрации того, что элементы удаляются из очереди в том же порядке, в котором они были вставлены.
FIFO очередь с несколькими потоками
Для реализации FIFO с несколькими потоками нам нужно определить функцию myqueue (), которая расширена из модуля очереди. Работа методов get () и put () аналогична описанной выше при реализации очереди FIFO с одним потоком. Затем, чтобы сделать его многопоточным, нам нужно объявить и создать экземпляры потоков. Эти потоки будут использовать очередь в режиме FIFO.
пример
Ниже приведена программа на Python для реализации очереди FIFO с несколькими потоками.
import threading import queue import random import time def myqueue(queue): while not queue.empty(): item = queue.get() if item is None: break print("{} removed {} from the queue".format(threading.current_thread(), item)) queue.task_done() time.sleep(2) q = queue.Queue() for i in range(5): q.put(i) threads = [] for i in range(4): thread = threading.Thread(target=myqueue, args=(q,)) thread.start() threads.append(thread) for thread in threads: thread.join()
Выход
<Thread(Thread-3654, started 5044)> removed 0 from the queue <Thread(Thread-3655, started 3144)> removed 1 from the queue <Thread(Thread-3656, started 6996)> removed 2 from the queue <Thread(Thread-3657, started 2672)> removed 3 from the queue <Thread(Thread-3654, started 5044)> removed 4 from the queue
LIFO, очередь «Последний пришел первым»
Эта очередь использует полностью противоположную аналогию с очередями FIFO (первым пришел — первым вышел). В этом механизме организации очереди тот, кто придет последним, получит службу первым. Это похоже на реализацию структуры данных стека. Очереди LIFO оказываются полезными при реализации поиска в глубину как алгоритмы искусственного интеллекта.
Python-реализация очереди LIFO
В Python очередь LIFO может быть реализована как с одним потоком, так и с многопоточностью.
LIFO очередь с одним потоком
Для реализации очереди LIFO с одним потоком класс Queue реализует базовый контейнер типа «последний пришел — первый вышел», используя структуру Queue .LifoQueue. Теперь при вызове метода put () элементы добавляются в начало контейнера и удаляются из него также при использовании get () .
пример
Ниже приведена программа на Python для реализации очереди LIFO с одним потоком:
import queue q = queue.LifoQueue() for i in range(8): q.put("item-" + str(i)) while not q.empty(): print (q.get(), end=" ") Output: item-7 item-6 item-5 item-4 item-3 item-2 item-1 item-0
Выходные данные показывают, что вышеприведенная программа использует один поток, чтобы проиллюстрировать, что элементы удаляются из очереди в обратном порядке их вставки.
Очередь LIFO с несколькими потоками
Реализация схожа с реализацией очередей FIFO с несколькими потоками. Единственное отличие состоит в том, что нам нужно использовать класс Queue, который будет реализовывать базовый контейнер « первым пришел — первым вышел», используя структуру Queue.LifoQueue .
пример
Ниже приведена программа на Python для реализации очереди LIFO с несколькими потоками:
import threading import queue import random import time def myqueue(queue): while not queue.empty(): item = queue.get() if item is None: break print("{} removed {} from the queue".format(threading.current_thread(), item)) queue.task_done() time.sleep(2) q = queue.LifoQueue() for i in range(5): q.put(i) threads = [] for i in range(4): thread = threading.Thread(target=myqueue, args=(q,)) thread.start() threads.append(thread) for thread in threads: thread.join()
Выход
<Thread(Thread-3882, started 4928)> removed 4 from the queue <Thread(Thread-3883, started 4364)> removed 3 from the queue <Thread(Thread-3884, started 6908)> removed 2 from the queue <Thread(Thread-3885, started 3584)> removed 1 from the queue <Thread(Thread-3882, started 4928)> removed 0 from the queue
Приоритетная очередь
В очередях FIFO и LIFO порядок элементов связан с порядком вставки. Однако во многих случаях приоритет важнее, чем порядок вставки. Давайте рассмотрим пример из реального мира. Предположим, что служба безопасности в аэропорту проверяет людей разных категорий. Люди из VVIP, сотрудники авиакомпании, таможенники, категории могут быть проверены по приоритету, а не проверены по прибытии, как это делается для простых людей.
Другим важным аспектом, который необходимо учитывать для приоритетной очереди, является разработка планировщика задач. Один из распространенных способов — обслуживать большинство задач агента на приоритетной основе в очереди. Эту структуру данных можно использовать для выбора элементов из очереди на основе их значения приоритета.
Реализация приоритетов в Python
В Python очередь с приоритетами может быть реализована как с одним потоком, так и с многопоточностью.
Приоритетная очередь с одним потоком
Для реализации приоритетной очереди с одним потоком класс Queue будет реализовывать задачу в приоритетном контейнере с использованием структуры Queue .PriorityQueue. Теперь, при вызове put () , элементы добавляются со значением, где наименьшее значение будет иметь самый высокий приоритет, и, следовательно, сначала извлекается с помощью get () .
пример
Рассмотрим следующую программу на Python для реализации очереди Priority с одним потоком:
import queue as Q p_queue = Q.PriorityQueue() p_queue.put((2, 'Urgent')) p_queue.put((1, 'Most Urgent')) p_queue.put((10, 'Nothing important')) prio_queue.put((5, 'Important')) while not p_queue.empty(): item = p_queue.get() print('%s - %s' % item)
Выход
1 – Most Urgent 2 - Urgent 5 - Important 10 – Nothing important
В приведенном выше выводе мы видим, что в очереди хранятся элементы, основанные на приоритете — меньшее значение имеет высокий приоритет.
Приоритетная очередь с несколькими потоками
Реализация аналогична реализации очередей FIFO и LIFO с несколькими потоками. Единственное отличие состоит в том, что нам нужно использовать класс Queue для инициализации приоритета, используя структуру Queue.PriorityQueue . Другое отличие заключается в том, как будет создаваться очередь. В приведенном ниже примере он будет создан с двумя одинаковыми наборами данных.
пример
Следующая программа Python помогает в реализации приоритетной очереди с несколькими потоками —