Queue.Queue vs. collection.deque


181

ฉันต้องการคิวที่หลายเธรดสามารถใส่ข้อมูลและหลายกระทู้อาจอ่านได้

Python มีคลาสคิวอย่างน้อยสองคลาสคือ Queue.Queue และ collection.deque โดยที่ก่อนหน้านี้ดูเหมือนจะใช้หลังภายใน ทั้งสองอ้างว่าปลอดภัยต่อเธรดในเอกสารประกอบ

อย่างไรก็ตามคิวเอกสารยังระบุ:

collection.deque เป็นการใช้งานทางเลือกของคิวที่ไม่ได้ถูก จำกัด ด้วยการดำเนินการผนวกอะตอมอย่างรวดเร็ว () และ popleft () ที่ไม่ต้องการการล็อค

ซึ่งฉันคิดว่าฉันไม่ได้พูดค่อนข้างไกล: นี่หมายความว่า deque ไม่ได้เป็นเธรดที่ปลอดภัยทั้งหมดหรือไม่?

ถ้าเป็นฉันอาจไม่เข้าใจความแตกต่างระหว่างสองคลาสอย่างสมบูรณ์ ฉันเห็นว่าคิวเพิ่มฟังก์ชันการบล็อก ในทางกลับกันจะสูญเสียคุณสมบัติบางอย่างเช่นการสนับสนุนสำหรับผู้ประกอบการ

การเข้าถึงวัตถุ deque ภายในโดยตรงคือ

x ในคิว (). deque

ด้ายปลอดภัย?

นอกจากนี้เหตุใดคิวจึงใช้ mutex สำหรับการดำเนินการเมื่อ 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; ถ้าคุณเพียงต้องการคิวหรือแถวคอยสองหน้าเป็น datastructure collections.dequeใช้

ในที่สุดการเข้าถึงและจัดการภายใน deque ของ a Queue.Queueกำลังเล่นด้วยไฟ - คุณไม่ต้องการทำเช่นนั้นจริงๆ


6
ไม่นั่นไม่ใช่ความคิดที่ดีเลย หากคุณดูที่แหล่งที่มาของQueue.Queueมันจะใช้dequeภายใต้ประทุน collections.dequeคือชุดสะสมในขณะที่Queue.Queueเป็นกลไกการสื่อสาร ค่าใช้จ่ายในQueue.Queueคือการทำให้มันปลอดภัย การใช้dequeเพื่อการสื่อสารระหว่างเธรดจะนำไปสู่การแข่งขันที่เจ็บปวดเท่านั้น เมื่อใดก็ตามdequeที่เป็นเธรดเซฟนั่นเป็นอุบัติเหตุที่เกิดความสุขในการใช้งานล่ามและไม่ใช่สิ่งที่ต้องพึ่งพา นั่นเป็นเหตุผลที่Queue.Queueมีอยู่ในสถานที่แรก
Keith Gaughan

2
เพียงจำไว้ว่าถ้าคุณกำลังสื่อสารข้ามเธรดคุณกำลังเล่นด้วยไฟโดยใช้ deque deque เป็น threadsafe โดยไม่ได้ตั้งใจเนื่องจากการมีอยู่ของ GIL การติดตั้งที่ไม่ใช้ GIL จะมีคุณสมบัติด้านประสิทธิภาพที่แตกต่างไปจากเดิมอย่างสิ้นเชิงดังนั้นการลดการใช้งานอื่น ๆ จึงไม่ฉลาด นอกจากนี้คุณได้ตั้งเวลา Queue vs deque สำหรับใช้ข้ามเธรดซึ่งตรงข้ามกับเกณฑ์มาตรฐานที่ไร้เดียงสาของการใช้งานในเธรดเดี่ยวหรือไม่? หากรหัสของคุณนั้นอ่อนไหวต่อความเร็วของ Queue vs deque Python อาจไม่ใช่ภาษาที่คุณต้องการ
Keith Gaughan

3
@ KeithGaughan deque is threadsafe by accident due to the existence of GIL; มันเป็นความจริงที่dequeอาศัย GIL เพื่อให้แน่ใจว่าด้ายความปลอดภัย - by accidentแต่ก็ไม่ได้ เอกสารไพ ธ อนอย่างเป็นทางการระบุไว้อย่างชัดเจนว่าdeque pop*/ append*methods นั้นปลอดภัยต่อเธรด ดังนั้นการใช้งานไพ ธ อนที่ถูกต้องจะต้องให้การรับประกันแบบเดียวกัน (การใช้งานแบบไม่ต้องใช้ GIL จะต้องคิดออกว่าจะทำอย่างไรหากไม่มี GIL) คุณสามารถพึ่งพาการรับประกันเหล่านั้นได้อย่างปลอดภัย
สูงสุด

2
@fantabolous ความคิดเห็นก่อนหน้าของฉันแม้ว่าฉันจะไม่เข้าใจว่าคุณจะใช้dequeสื่อสารอย่างไร หากคุณpopใส่เข้าไปใน a try/exceptคุณจะจบลงด้วยการวนลูปที่ยุ่งอยู่กับการกินซีพียูจำนวนมหาศาลเพียงแค่รอข้อมูลใหม่ ดูเหมือนว่าจะเป็นวิธีที่ไม่มีประสิทธิภาพอย่างน่ากลัวเมื่อเทียบกับการบล็อกการโทรที่เสนอโดยQueueทำให้แน่ใจได้ว่าเธรดที่รอข้อมูลจะเข้าสู่โหมดสลีปและไม่เสียเวลากับ CPU
สูงสุด

3
คุณอาจต้องการอ่านซอร์สโค้ดในQueue.Queueตอนนั้นเนื่องจากเขียนโดยใช้collections.deque: hg.python.org/cpython/file/2.7/Lib/Queue.py - มันใช้ตัวแปรเงื่อนไขเพื่อให้dequeสามารถเข้าถึง wraps ได้อย่างมีประสิทธิภาพข้ามขอบเขตของเธรดอย่างปลอดภัยและมีประสิทธิภาพ คำอธิบายวิธีที่คุณใช้dequeสำหรับการสื่อสารนั้นมีอยู่ในแหล่งที่มา
Keith Gaughan

44

หากสิ่งที่คุณกำลังมองหาเป็นวิธีที่ปลอดภัยสำหรับเธรดในการถ่ายโอนออบเจ็กต์ระหว่างเธรดทั้งสองจะทำงานได้ (ทั้งสำหรับ FIFO และ LIFO) สำหรับ FIFO:

บันทึก:

  • การดำเนินการอื่น ๆdequeอาจไม่ปลอดภัยสำหรับเธรดฉันไม่แน่ใจ
  • dequeไม่บล็อกpop()หรือpopleft()ดังนั้นคุณจึงไม่สามารถยึดการไหลของเธรดผู้บริโภคในการบล็อกจนกว่าจะมีรายการใหม่มาถึง

แต่ก็ดูเหมือนว่าdeque มีข้อได้เปรียบที่มีประสิทธิภาพอย่างมีนัยสำคัญ นี่คือผลการวัดประสิทธิภาพในไม่กี่วินาทีโดยใช้ 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

@Matt - ใช้ถ้อยคำใหม่เพื่อสื่อความหมายของฉันให้ดีขึ้น
Jonathan

3
โอเคขอบคุณ. นั่นคือการหยุดฉันจากการใช้ deque เพราะฉันคิดว่าคุณรู้ว่าสิ่งที่ฉันไม่ได้ ฉันเดาว่าฉันแค่คิดว่ามันปลอดภัยจนกว่าฉันจะค้นพบ
Matt

@Matt "การดำเนินการผนวก (), appendleft (), pop (), popleft () และ len (d) การดำเนินการของเธรดนั้นปลอดภัยต่อเธรดใน CPython" แหล่งที่มา: bugs.python.org/issue15329
Filippo Vitale

7

สำหรับข้อมูลมีตั๋ว Python อ้างอิงสำหรับ deque thread-safety ( https://bugs.python.org/issue15329 ) ชื่อ "ชี้แจงว่าวิธีการถอนคำใดที่ปลอดภัยสำหรับเธรด"

บรรทัดล่างสุดที่นี่: https://bugs.python.org/issue15329#msg199368

การดำเนินการผนวก (), appendleft (), pop (), popleft (), และ len (d) ของ deque นั้นปลอดภัยสำหรับเธรดใน CPython เมธอดผนวกมี DECREF ที่ส่วนท้าย (สำหรับกรณีที่ตั้งค่า maxlen) แต่สิ่งนี้เกิดขึ้นหลังจากมีการอัพเดตโครงสร้างทั้งหมดและค่าคงที่ได้รับการกู้คืนดังนั้นจึงไม่ถือว่าการดำเนินการเหล่านี้เป็นอะตอม

อย่างไรก็ตามหากคุณไม่แน่ใจ 100% และคุณต้องการความน่าเชื่อถือมากกว่าประสิทธิภาพเพียงใส่รหัสล็อค;)


6

วิธีการแบบองค์ประกอบเดียวทั้งหมดdequeเป็น atomic และ thread-safe วิธีการอื่น ๆ ทั้งหมดก็ปลอดภัยเช่นกัน สิ่งที่ชอบlen(dq), dq[4]ผลผลิตค่าที่ถูกต้องชั่วขณะ แต่คิดเช่นเกี่ยวกับdq.extend(mylist): คุณไม่ได้รับการรับประกันว่าองค์ประกอบทั้งหมดmylistจะถูกจัดเก็บในแถวเมื่อเธรดอื่น ๆ ผนวกองค์ประกอบในด้านเดียวกัน - แต่นั่นมักจะไม่ต้องการในการสื่อสารระหว่างเธรดและงานที่ถูกถาม

ดังนั้น a dequeจะเร็วกว่า ~ ~ ~ ~ 20x Queue(ซึ่งใช้dequeภายใต้ประทุน) และถ้าคุณไม่ต้องการ API การประสาน "ที่สะดวกสบาย" (การบล็อก / หมดเวลา) การmaxsizeเชื่อฟังอย่างเข้มงวดหรือ"แทนที่วิธีการเหล่านี้ (_put, _get, .. ) เพื่อใช้องค์กรคิวอื่น ๆ "พฤติกรรมการแบ่งกลุ่มย่อยหรือเมื่อคุณดูแลสิ่งต่าง ๆ ด้วยตัวคุณเองแล้วการเปิดเผยdequeก็เป็นข้อตกลงที่ดีและมีประสิทธิภาพสำหรับการสื่อสารระหว่างเธรดความเร็วสูง

ในความเป็นจริงการใช้งานอย่างหนักของ mutex พิเศษและวิธีการพิเศษ._get()อื่น ๆ ที่เรียกQueue.pyเป็นเพราะข้อ จำกัด ความเข้ากันได้ย้อนหลังผ่านการออกแบบมากเกินไปและขาดการดูแลเพื่อให้การแก้ปัญหาที่มีประสิทธิภาพสำหรับปัญหาคอขวดความเร็วที่สำคัญนี้ในการสื่อสารระหว่างเธรด รายการถูกใช้ในเวอร์ชัน Python รุ่นเก่า - แต่แม้กระทั่ง list.append () /. pop (0) คือ & เป็น atomic และ threadsafe ...


3

การเพิ่มnotify_all()แต่ละรายการdeque appendและpopleftผลลัพธ์ที่แย่ยิ่งdequeกว่าการปรับปรุง 20 เท่าที่ทำได้โดยdequeพฤติกรรมเริ่มต้น :

deque + notify_all: 0.469802
Queue:              0.667279

@ โจนาธานแก้ไขรหัสของเขาเล็กน้อยและฉันได้รับมาตรฐานโดยใช้ cPython 3.6.2 และเพิ่มเงื่อนไขในการวนซ้ำเพื่อจำลองพฤติกรรมที่คิวทำ

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และใช้ mutex สำหรับ accessors และการกลายพันธุ์เพื่อให้Queue().queueเป็นไม่ด้ายปลอดภัยในการใช้งาน

หากคุณกำลังมองหาโอเปอเรเตอร์ "in" แสดงว่า deque หรือคิวนั้นอาจไม่ใช่โครงสร้างข้อมูลที่เหมาะสมที่สุดสำหรับปัญหาของคุณ


1
สิ่งที่ฉันต้องการทำคือตรวจสอบให้แน่ใจว่าไม่มีการเพิ่มรายการที่ซ้ำกันในคิว นี่ไม่ใช่สิ่งที่คิวอาจสนับสนุนหรือไม่
miracle2k

1
อาจเป็นการดีที่สุดที่จะมีชุดแยกต่างหากและอัปเดตเมื่อคุณเพิ่ม / ลบบางสิ่งออกจากคิว นั่นจะเป็น O (log n) มากกว่า O (n) แต่คุณจะต้องระมัดระวังในการตั้งค่าและคิวให้ซิงค์ (เช่นการล็อค)
brian-Brazil

ชุด Python เป็นตารางแฮชดังนั้นมันจะเป็น O (1) แต่ใช่คุณยังต้องทำการล็อค
AFoglia

1

(ดูเหมือนว่าฉันไม่มีชื่อเสียงที่จะแสดงความคิดเห็น ... ) คุณต้องระวังวิธีการ deque ที่คุณใช้จากกระทู้ที่แตกต่างกัน

deque.get () ดูเหมือนจะเป็น threadsafe แต่ฉันพบว่าการทำเช่นนั้น

for item in a_deque:
   process(item)

สามารถล้มเหลวหากเธรดอื่นกำลังเพิ่มรายการในเวลาเดียวกัน ฉันได้ RuntimeException ที่บ่นว่า "deque กลายพันธุ์ในระหว่างการทำซ้ำ"

ตรวจสอบcollectionmodule.cเพื่อดูว่าการดำเนินงานใดได้รับผลกระทบจากสิ่งนี้


ข้อผิดพลาดชนิดนี้ไม่ได้พิเศษสำหรับเธรดและความปลอดภัยของเธรดหลัก มันเกิดขึ้นเช่นโดยเพียงแค่ทำ >>> di = {1:None} >>> for x in di: del di[x]
kxr

1
โดยทั่วไปคุณไม่ควรวนซ้ำสิ่งที่อาจมีการแก้ไขโดยเธรดอื่น (แม้ว่าในบางกรณีคุณสามารถทำได้โดยเพิ่มการป้องกันของคุณเอง) เช่นเดียวกับคิวคุณตั้งใจที่จะป๊อป / เอาไอเท็มออกจากคิวก่อนที่จะประมวลผลและโดยปกติคุณจะทำกับwhileลูป
เพ้อฝัน
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.