Черга. Черга проти колекцій.deque


181

Мені потрібна черга, в яку можна покласти кілька потоків, і з яких можна прочитати кілька потоків.

У Python є щонайменше два класи черг, Queue.Queue і collection.deque, причому перший, здавалося б, використовує останній внутрішньо. Обидва заявляють, що захищені потоками в документації.

Однак у документах черги також зазначено:

collection.deque - це альтернативна реалізація необмежених черг з операціями швидкого атомного додавання () та popleft (), які не потребують блокування.

Я думаю, я не зовсім розумію: чи означає це, що врешті-решт деке не є повністю безпечним для потоків?

Якщо це так, я, можливо, не повністю розумію різницю між двома класами. Я бачу, що черга додає функцію блокування. З іншого боку, він втрачає деякі функції, такі як підтримка оператора.

Доступ до внутрішнього об'єкта deque безпосередньо, є

x у черзі (). deque

нитка-безпечна?

Крім того, чому черга використовує мьютекс для своїх операцій, коли deque вже захищений потоками?


RuntimeError: deque mutated during iterationце те, що ви можете отримати, - це використання спільного dequeміж декількома потоками та відсутність блокування ...
toine

4
@toine, який не має нічого спільного з потоками. Ви можете отримувати цю помилку щоразу, коли ви додаєте / видаляєте під dequeчас повторення навіть у тій самій потоці. Єдина причина, з якої ви не можете отримати цю помилку, Queue- це те, що Queueвона не підтримує ітерацію.
макс

Відповіді:


281

Queue.Queueі collections.dequeслужать різним цілям. Queue.Queue призначений для того, щоб дозволяти різним потокам спілкуватися за допомогою повідомлень / даних у черзі, тоді як collections.dequeвін просто призначений як структура даних. Ось чому Queue.Queueє такі методи , як put_nowait(), get_nowait()і join(), в той час як collections.dequeне робить. Queue.Queueне призначений для використання як колекція, тому не вистачає подобань inоператора.

Це зводиться до цього: якщо у вас є кілька потоків і ви хочете, щоб вони могли спілкуватися без необхідності блокування, ви шукаєте Queue.Queue; якщо ви просто хочете чергу чи двояку чергу в якості структури даних, використовуйте collections.deque.

Нарешті, отримати доступ до маніпуляцій та керувати ними внутрішнім деком - Queue.Queueце гра з вогнем - ви насправді не хочете цим займатися.


6
Ні, це зовсім не гарна ідея. Якщо ви подивитесь на джерело Queue.Queue, воно використовується dequeпід кришкою. collections.dequeє колекцією, тоді Queue.Queueяк механізмом комунікацій. Накладні витрати Queue.Queue- це зробити його безпечним. Використання dequeдля спілкування між нитками призведе лише до болісних перегонів. Кожного разу, коли dequeце може бути безпечно для потоків, це щасливий випадок того, як реалізується перекладач, а не те, на що слід покладатися. Ось чому Queue.Queueіснує в першу чергу.
Кіт Гоген

2
Просто майте на увазі, що якщо ви спілкуєтесь між нитками, ви граєте з вогнем, використовуючи deque. deque є безпечним для ниток випадково через існування GIL. Реалізація без GIL матиме зовсім інші характеристики продуктивності, тому дисконтування інших реалізацій не є розумним. Крім того, ви приурочили Чергу до деке для використання в різних потоках, на відміну від наївного орієнтира його використання в одній нитці? Якщо ваш код , який чутливий до швидкості черзі проти дека, Python не може бути мовою , який ви шукаєте.
Кіт Гоген

3
@KeithGaughan deque is threadsafe by accident due to the existence of GIL; це правда, що dequeпокладається на GIL, щоб забезпечити безпеку ниток - але це не так by accident. В офіційній документації python чітко зазначено, що deque pop*/ append*методи є безпечними для потоків. Таким чином, будь-яка дійсна реалізація python повинна надавати ту саму гарантію (без GIL-реалізацій доведеться з'ясувати, як це зробити без GIL). Ви можете сміливо покладатися на ці гарантії.
макс

2
@fantabolous мій попередній коментар, незважаючи на те, я не зовсім розумію, як би ви використовували dequeдля спілкування. Якщо ви перейдете popдо програми try/except, у вас виявиться зайнятий цикл, який з'їдає величезну кількість процесора, просто чекаючи нових даних. Це здається жахливо неефективним підходом порівняно з блокувальними викликами, які пропонують Queue, які гарантують, що потік, що чекає даних, перейде в режим сну і не витрачає час процесора.
макс

3
Ви можете скористатися зчитуванням вихідного коду Queue.Queue, тому що він написаний за допомогою collections.deque: hg.python.org/cpython/file/2.7/Lib/Queue.py - він використовує змінні умови, щоб ефективно дозволити доступ до dequeйого обгортання. над межами ниток безпечно та ефективно. Пояснення того, як ви використовуєте dequeдля спілкування, є саме там, у джерелі.
Кіт Гоген

44

Якщо все, що ви шукаєте, - безпечний для потоків спосіб передачі об'єктів між потоками , то обидва будуть працювати (як для FIFO, так і для LIFO). Для FIFO:

Примітка:

  • Інші операції на, dequeможливо, не є безпечними для потоків, я не впевнений.
  • dequeне блокується pop()або popleft()тому ви не можете базувати потоки споживчих ниток на блокуванні, поки не надійде новий товар.

Однак, схоже, що деке має значну перевагу в ефективності . Ось деякі результати орієнтиру за секунди, використовуючи CPython 2.7.3 для вставки та видалення 100k елементів

deque 0.0747888759791
Queue 1.60079066852

Ось базовий код:

import time
import Queue
import collections

q = collections.deque()
t0 = time.clock()
for i in xrange(100000):
    q.append(1)
for i in xrange(100000):
    q.popleft()
print 'deque', time.clock() - t0

q = Queue.Queue(200000)
t0 = time.clock()
for i in xrange(100000):
    q.put(1)
for i in xrange(100000):
    q.get()
print 'Queue', time.clock() - t0

1
Ви стверджуєте, що "Інші операції з dequeне можуть бути безпечними для потоку". Звідки ти це береш?
Метт

@Matt - перефразував, щоб краще передати моє значення
Джонатан

3
Добре, дякую. Це заважало мені використовувати deque, тому що я думав, що ти знаєш щось, чого я не знаю. Я думаю, я просто припускаю, що це безпечно для ниток, поки я не відкрию інше.
Метт

@Matt "Операції append (), appendleft (), pop (), popleft () та len (d)" deque "є безпечними для потоків у CPython." джерело: bugs.python.org/issue15329
Filippo Vitale

7

Для інформації є квиток на Python, на який посилається безпека потоків deque ( https://bugs.python.org/issue15329 ). Заголовок "уточнити, які методи deque безпечні для потоків"

Підсумок тут: https://bugs.python.org/issue15329#msg199368

Операції append (), appendleft (), pop (), popleft () та len (d) є безпечними для потоків у CPython. У методах додавання є DECREF в кінці (для випадків, коли встановлено maxlen), але це відбувається після того, як були зроблені всі оновлення структури та відновлені інваріанти, тож добре розглядати ці операції як атомні.

У будь-якому випадку, якщо ви не впевнені на 100% і віддаєте перевагу надійності щодо продуктивності, просто поставте подібний Lock;)


6

Усі одноелементні методи dequeє безпечними для атомів та потоків. Усі інші методи також є безпечними для потоків. Такі речі , як len(dq), dq[4]дають моментальні правильні значення. Але подумайте, наприклад dq.extend(mylist): ви не отримуєте гарантії, що всі елементи в mylistфайлі подаються підряд, коли інші потоки також додають елементи з тієї ж сторони, - але це зазвичай не є вимогою в міжпотоковому спілкуванні та для заданого питання.

Тож a на deque20 разів швидше, ніж Queue(що використовує dequeпід кришкою), і, якщо вам не потрібен "комфортний" API синхронізації (блокування / таймаут), чітке maxsizeдотримання або "Перевизначення цих методів (_put, _get, .. ) реалізовувати поведінку підкласових класів інших організацій черги або коли ви самі подбаєте про такі речі, тоді голі deque- це хороша та ефективна угода для високошвидкісного міжпотокового спілкування.

Насправді, велике використання додаткового методу мютексу та додаткового методу ._get()тощо Queue.pyвикликане обмеженнями сумісності назад, минулим надмірним дизайном та відсутністю турботи щодо забезпечення ефективного вирішення цієї важливої ​​проблеми із швидким вузьким місцем у комунікації між потоками. Список використовувався в старих версіях Python, але навіть list.append () /. Pop (0) був & є атомним та поточним безпечним ...


3

Додавання notify_all()до кожного deque appendі popleftпризводить до набагато гірших результатів, dequeніж 20-кратне покращення, досягнуте за dequeповедінкою за замовчуванням :

deque + notify_all: 0.469802
Queue:              0.667279

@Jonathan трохи змінить його код, і я отримую орієнтир за допомогою cPython 3.6.2 і додаю умову в циклі deque, щоб імітувати поведінку в черзі.

import time
from queue import Queue
import threading
import collections

mutex = threading.Lock()
condition = threading.Condition(mutex)
q = collections.deque()
t0 = time.clock()
for i in range(100000):
    with condition:
        q.append(1)
        condition.notify_all()
for _ in range(100000):
    with condition:
        q.popleft()
        condition.notify_all()
print('deque', time.clock() - t0)

q = Queue(200000)
t0 = time.clock()
for _ in range(100000):
    q.put(1)
for _ in range(100000):
    q.get()
print('Queue', time.clock() - t0)

І, здається, продуктивність обмежена цією функцією condition.notify_all()

collection.deque - це альтернативна реалізація необмежених черг з операціями швидкого атомного додавання () та popleft (), які не потребують блокування. Черга документів


2

dequeє безпечним для ниток. "операції, які не потребують блокування" означає, що вам не доведеться робити блокування самостійно, це dequeтурбується про це.

Поглянувши на Queueджерелі, внутрішній Deque називається self.queueі використовує м'ютекс для аксессор і мутацій, так що Queue().queueце НЕ поточно- для використання.

Якщо ви шукаєте оператора "in", то дека або черга, можливо, не є найбільш підходящою структурою даних для вашої проблеми.


1
Що ж, я хочу зробити так, щоб у черзі не було додано жодних дублікатів. Чи це не те, що черга могла б підтримувати?
miracle2k

1
Напевно, найкраще мати окремий набір та оновити це, коли ви додасте / вилучіть щось із черги. Це буде O (log n), а не O (n), але вам доведеться бути обережними, щоб синхронізувати набір і чергу (тобто блокування).
brian-brazil

Набір Python - це хеш-таблиця, тож це було б O (1). Але так, вам все одно доведеться робити замок.
AFoglia

1

(здається, я не маю репутації коментувати ...) Ви повинні бути обережними, які методи деке ви використовуєте з різних ниток.

deque.get (), здається, безпечний для потоків, але я виявив, що це робить

for item in a_deque:
   process(item)

може не вдатися, якщо інший потік одночасно додає елементи. Я отримав RuntimeException, який скаржився на "deque mutiated під час ітерації".

Перевірте collectionmodule.c, щоб побачити, на які операції впливає це


подібні помилки не є особливими для ниток та основних принципів безпеки. Це трапляється, наприклад, просто роблячи >>> di = {1:None} >>> for x in di: del di[x]
kxr

1
По суті, вам ніколи не слід перебирати щось, що може бути змінено іншою темою (хоча в деяких випадках ви можете це зробити, додавши власний захист). Як і в черзі, ви маєте намір вивести / вилучити елемент з черги перед його обробкою, і зазвичай це робите за допомогою whileциклу.
фантастичний
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.