รูปแบบการทำลายของ LMAX ทำงานอย่างไร


205

ฉันพยายามที่จะเข้าใจรูปแบบ disruptor ฉันดูวิดีโอ InfoQ และพยายามอ่านบทความของพวกเขา ฉันเข้าใจว่ามีบัฟเฟอร์วงแหวนที่เกี่ยวข้องซึ่งเริ่มต้นเป็นอาร์เรย์ขนาดใหญ่มากเพื่อใช้ประโยชน์จากตำแหน่งแคชการกำจัดการจัดสรรหน่วยความจำใหม่

ดูเหมือนจะมีจำนวนเต็มหนึ่งอะตอมหรือมากกว่านั้นซึ่งติดตามตำแหน่ง แต่ละเหตุการณ์ดูเหมือนว่าจะได้รับ id ที่ไม่ซ้ำกันและมันอยู่ในตำแหน่งของวงแหวนโดยการหาโมดูลัสของมันตามขนาดของแหวน ฯลฯ

น่าเสียดายที่ฉันไม่เข้าใจวิธีการทำงานของสัญชาตญาณ ฉันได้ทำการซื้อขายแอพพลิเคชั่นมากมายและศึกษาโมเดลนักแสดงดูที่ SEDA และอื่น ๆ

ในการนำเสนอของพวกเขาพวกเขากล่าวว่ารูปแบบนี้โดยทั่วไปแล้วเราเตอร์ทำงานอย่างไร อย่างไรก็ตามฉันไม่พบคำอธิบายที่ดีว่าเราเตอร์ทำงานอย่างไร

มีคำแนะนำที่ดีสำหรับคำอธิบายที่ดีกว่านี้หรือไม่?

คำตอบ:


210

โครงการ Google Code อ้างอิงเอกสารทางเทคนิคเกี่ยวกับการใช้งาน ring buffer อย่างไรก็ตามมันค่อนข้างจะแห้งนักวิชาการและยากสำหรับผู้ที่ต้องการเรียนรู้วิธีการทำงาน อย่างไรก็ตามมีบางโพสต์บล็อกที่เริ่มอธิบาย internals ในทางที่อ่านง่ายขึ้น มีคำอธิบายของ ring bufferที่เป็นแกนหลักของรูปแบบ disruptor คำอธิบายของอุปสรรคของผู้บริโภค (ส่วนที่เกี่ยวข้องกับการอ่านจาก disruptor) และข้อมูลบางอย่างเกี่ยวกับการจัดการกับผู้ผลิตหลายรายที่มีอยู่

คำอธิบายที่ง่ายที่สุดของ Disruptor คือ: มันเป็นวิธีการส่งข้อความระหว่างเธรดในลักษณะที่มีประสิทธิภาพที่สุดเท่าที่จะทำได้ มันสามารถใช้เป็นทางเลือกให้กับคิวได้ แต่ก็ยังแบ่งปันคุณสมบัติจำนวนมากกับ SEDA และนักแสดง

เปรียบเทียบกับ Queues:

Disruptor จัดเตรียมความสามารถในการส่งข้อความไปยังเธรดอื่นโดยจะปลุกให้ทำงานหากจำเป็น (คล้ายกับ BlockingQueue) อย่างไรก็ตามมีความแตกต่าง 3 อย่างชัดเจน

  1. ผู้ใช้ Disruptor จะกำหนดวิธีจัดเก็บข้อความโดยการขยายคลาส Entry และจัดให้โรงงานทำการจัดสรรล่วงหน้า สิ่งนี้อนุญาตให้ใช้ซ้ำ (คัดลอก) หน่วยความจำหรือรายการอาจมีการอ้างอิงไปยังวัตถุอื่น
  2. การใส่ข้อความลงใน Disruptor นั้นเป็นกระบวนการแบบ 2 เฟสโดยขั้นแรกจะมีการอ้างสิทธิ์ในสล็อตในบัฟเฟอร์ริงซึ่งให้ผู้ใช้งานสามารถป้อนข้อมูลที่เหมาะสมได้ จากนั้นรายการจะต้องมีการกระทำวิธีการ 2 เฟสนี้มีความจำเป็นเพื่อให้การใช้งานที่ยืดหยุ่นของหน่วยความจำดังกล่าวข้างต้น เป็นความมุ่งมั่นที่ทำให้ข้อความปรากฏแก่เธรดผู้บริโภค
  3. มันเป็นความรับผิดชอบของผู้บริโภคในการติดตามข้อความที่ถูกใช้จากบัฟเฟอร์วงแหวน การย้ายความรับผิดชอบนี้ออกไปจากบัฟเฟอร์ของวงแหวนนั้นช่วยลดปริมาณการเขียนในขณะที่เธรดแต่ละตัวรักษาตัวนับของตนเอง

เปรียบเทียบกับนักแสดง

โมเดล Actor นั้นใกล้กับ Disruptor มากกว่ารุ่นอื่น ๆ โดยเฉพาะถ้าคุณใช้คลาส BatchConsumer / BatchHandler ที่มีให้ คลาสเหล่านี้ซ่อนความซับซ้อนทั้งหมดในการรักษาหมายเลขลำดับที่ใช้และจัดเตรียมชุดการเรียกกลับอย่างง่าย ๆ เมื่อมีเหตุการณ์สำคัญเกิดขึ้น อย่างไรก็ตามมีความแตกต่างเล็กน้อย

  1. Disruptor ใช้โมเดลผู้บริโภค 1 เธรด - 1 โดยที่นักแสดงใช้โมเดล N: M นั่นคือคุณสามารถมีนักแสดงได้มากเท่าที่คุณต้องการและพวกเขาจะกระจายไปทั่วเธรดจำนวนคงที่ (โดยทั่วไป 1 ต่อแกน)
  2. อินเทอร์เฟซ BatchHandler จัดเตรียมการติดต่อกลับ (และสำคัญมาก) onEndOfBatch()เพิ่มเติม สิ่งนี้ช่วยให้ผู้บริโภคช้าเช่นผู้ที่ทำ I / O เพื่อแบทช์เหตุการณ์ร่วมกันเพื่อปรับปรุงปริมาณงาน เป็นไปได้ที่จะทำการแบตช์ในเฟรมเวิร์ก Actor อื่น ๆ อย่างไรก็ตามเนื่องจากเฟรมเวิร์กอื่นเกือบทั้งหมดไม่ได้ให้การเรียกกลับที่จุดสิ้นสุดของแบทช์ที่คุณต้องใช้การหมดเวลาเพื่อกำหนดจุดสิ้นสุดของแบทช์

เปรียบเทียบกับ SEDA

LMAX สร้างรูปแบบ Disruptor เพื่อแทนที่วิธีการตาม SEDA

  1. การปรับปรุงหลักที่ให้ไว้กับ SEDA คือความสามารถในการทำงานแบบขนาน เมื่อต้องการทำสิ่งนี้ Disruptor สนับสนุนการส่งข้อความเดียวกันหลายรายการ (ตามลำดับเดียวกัน) ไปยังผู้บริโภคหลายราย สิ่งนี้หลีกเลี่ยงความจำเป็นในการแยกขั้นตอนในท่อ
  2. นอกจากนี้เรายังอนุญาตให้ผู้บริโภครอผลลัพธ์ของผู้บริโภครายอื่นโดยไม่ต้องรอขั้นตอนการรอคิวอีกครั้งระหว่างพวกเขา ผู้บริโภคสามารถดูหมายเลขลำดับของผู้บริโภคที่ขึ้นอยู่กับ วิธีนี้จะช่วยหลีกเลี่ยงความจำเป็นในการเข้าร่วมขั้นตอนต่างๆ

เมื่อเปรียบเทียบกับ Memory Barriers

วิธีคิดอีกอย่างคือมันเป็นโครงสร้างกั้นหน่วยความจำที่มีโครงสร้าง ในกรณีที่ผู้ผลิตสร้างอุปสรรคเขียนและอุปสรรคผู้บริโภคเป็นอุปสรรคอ่าน


1
ขอบคุณไมเคิล บทความและลิงก์ที่คุณให้ไว้ช่วยให้ฉันเข้าใจวิธีการใช้งานของมันได้ดีขึ้น ที่เหลือฉันคิดว่าฉันแค่ต้องปล่อยให้มันจมลงไป
Shahbaz

ฉันยังมีคำถาม: (1) 'การกระทำ' ทำงานอย่างไร (2) เมื่อบัฟเฟอร์บัฟเฟอร์เต็มผู้ผลิตจะตรวจพบว่าผู้บริโภคทั้งหมดได้เห็นข้อมูลเพื่อให้ผู้ผลิตสามารถนำรายการกลับมาใช้ใหม่ได้อย่างไร
Qwertie

@Qwertie น่าจะโพสต์คำถามใหม่
Michael Barker

1
ไม่ควรเป็นประโยคแรกของสัญลักษณ์แสดงหัวข้อย่อยล่าสุด (หมายเลข 2) ภายใต้การเปรียบเทียบกับ SEDAแทนที่จะอ่าน "เรายังอนุญาตให้ผู้บริโภครอผลการค้นหาของผู้บริโภครายอื่นโดยไม่ต้องรอขั้นตอนการรอคิวระหว่างพวกเขา" อ่าน "อีกด้วย ผู้บริโภครอผลลัพธ์ของผู้บริโภครายอื่นโดยไม่ต้องรอขั้นตอนการจัดคิวอื่นระหว่างพวกเขา "(เช่น" กับ "ควร" แทนที่ด้วย "ไม่")?
runeks

@ runeks ใช่มันควร
Michael Barker

135

ก่อนอื่นเราต้องเข้าใจรูปแบบการเขียนโปรแกรมที่เสนอ

มีนักเขียนอย่างน้อยหนึ่งคน มีผู้อ่านหนึ่งคนขึ้นไป มีรายการที่เรียงลำดับจากเก่าไปใหม่ทั้งหมด (ภาพจากซ้ายไปขวา) นักเขียนสามารถเพิ่มรายการใหม่ทางด้านขวา ผู้อ่านทุกคนอ่านรายการตามลำดับจากซ้ายไปขวา ผู้อ่านไม่สามารถอ่านนักเขียนที่ผ่านมาได้อย่างชัดเจน

ไม่มีแนวคิดของการลบรายการ ฉันใช้ "reader" แทน "consumer" เพื่อหลีกเลี่ยงภาพของรายการที่ถูกใช้ไป อย่างไรก็ตามเราเข้าใจว่ารายการทางด้านซ้ายของผู้อ่านล่าสุดไม่มีประโยชน์

โดยทั่วไปผู้อ่านสามารถอ่านพร้อมกันและเป็นอิสระ อย่างไรก็ตามเราสามารถประกาศการพึ่งพาระหว่างผู้อ่าน ผู้อ่านสามารถอ้างอิงกราฟ acyclic หากตัวอ่าน B ขึ้นอยู่กับตัวอ่าน A ตัวอ่าน B จะไม่สามารถอ่านตัวอ่านที่ผ่านมา A ได้

การพึ่งพาผู้อ่านเกิดขึ้นเนื่องจากผู้อ่าน A สามารถใส่คำอธิบายประกอบรายการและผู้อ่าน B ขึ้นอยู่กับคำอธิบายประกอบนั้น ตัวอย่างเช่น A ทำการคำนวณบางรายการและเก็บผลลัพธ์ไว้ในฟิลด์aในรายการ จากนั้นไปต่อและตอนนี้ B สามารถอ่านรายการและค่าของaA ที่เก็บไว้ ถ้าอ่าน C ไม่ขึ้นอยู่กับ A, C aไม่ควรพยายามที่จะอ่าน

นี่เป็นรูปแบบการเขียนโปรแกรมที่น่าสนใจ ไม่ว่าจะมีประสิทธิภาพเพียงใดรุ่นนั้นมีประโยชน์ต่อแอปพลิเคชั่นมากมาย

แน่นอนเป้าหมายหลักของ LMAX คือประสิทธิภาพ มันใช้แหวนที่จัดสรรล่วงหน้าของรายการ แหวนมีขนาดใหญ่พอ แต่มีข้อ จำกัด เพื่อไม่ให้โหลดระบบเกินความสามารถในการออกแบบ หากแหวนเต็มผู้เขียนจะรอจนกว่าผู้อ่านที่ช้าที่สุดเลื่อนไปข้างหน้าและสร้างที่ว่าง

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

setNewEntry(EntryPopulator);

interface EntryPopulator{ void populate(Entry existingEntry); }

รายการจัดสรรล่วงหน้ายังหมายถึงรายการที่อยู่ติดกัน (มีโอกาสมาก) ที่จะค้นหาในเซลล์หน่วยความจำที่อยู่ติดกันและเนื่องจากผู้อ่านอ่านรายการตามลำดับนี่เป็นสิ่งสำคัญที่จะใช้แคชของ CPU

และความพยายามมากมายในการหลีกเลี่ยงการล็อก CAS แม้กระทั่งอุปสรรคหน่วยความจำ (เช่นใช้ตัวแปรลำดับที่ไม่ลบเลือนหากมีนักเขียนเพียงคนเดียว)

สำหรับนักพัฒนาของผู้อ่าน: ผู้อ่านคำอธิบายประกอบที่แตกต่างกันควรเขียนไปยังฟิลด์ต่าง ๆ เพื่อหลีกเลี่ยงข้อขัดแย้งในการเขียน (ที่จริงแล้วพวกเขาควรเขียนไปยังบรรทัดแคชที่แตกต่างกัน) ผู้อ่านหมายเหตุประกอบไม่ควรแตะสิ่งใด ๆ ที่ผู้อ่านที่ไม่เกี่ยวข้องอื่น ๆ อาจอ่าน นี่คือเหตุผลที่ฉันพูดว่าผู้อ่านเหล่านี้ใส่คำอธิบายประกอบรายการแทนที่จะแก้ไขรายการ


2
ดูโอเคสำหรับฉัน ฉันชอบการใช้คำอธิบายประกอบ
Michael Barker

21
+1 นี่เป็นคำตอบเดียวที่พยายามอธิบายว่ารูปแบบของตัวทำลายนั้นใช้งานได้จริงอย่างไรเมื่อ OP ถาม
G-Wiz

1
หากแหวนเต็มผู้เขียนจะรอจนกว่าผู้อ่านที่ช้าที่สุดจะเข้ามาและสร้างที่ว่าง - ปัญหาหนึ่งที่เกิดขึ้นกับคิว FIFO ที่ลึกนั้นทำให้พวกเขาเต็มได้ง่ายเกินไปภายใต้การโหลดเนื่องจากพวกเขาไม่ได้พยายามกดดันกลับจนกว่าพวกเขาจะถูกยัดและเวลาแฝงอยู่ในระดับสูงแล้ว
bestsss

1
@ โต้แย้งคุณสามารถเขียนคำอธิบายที่คล้ายกันสำหรับฝั่งผู้เขียนได้หรือไม่
Buchi

ฉันชอบ แต่ฉันพบสิ่งนี้ "ผู้เขียนขอรายการที่มีอยู่แล้วเติมข้อมูลในฟิลด์และแจ้งให้ผู้อ่านการกระทำ 2 เฟสที่เห็นได้ชัดนี้เป็นเพียงการกระทำของอะตอม" ทำให้สับสนและอาจผิดหรือเปล่า? ไม่มี "แจ้งเตือน" ใช่ไหม นอกจากนี้ยังไม่ใช่อะตอม แต่เป็นเพียงการเขียนที่มีประสิทธิภาพ / มองเห็นได้ถูกต้องใช่ไหม คำตอบที่ดีเพียงภาษาที่คลุมเครือ?
HaveAGuess


17

ที่จริงฉันใช้เวลาศึกษาแหล่งที่มาจริงจากความอยากรู้อยากเห็นและความคิดที่อยู่เบื้องหลังมันค่อนข้างง่าย รุ่นล่าสุดในขณะเขียนโพสต์นี้คือ 3.2.1

มีบัฟเฟอร์ที่จัดเก็บเหตุการณ์ที่จัดสรรไว้ล่วงหน้าซึ่งจะเก็บข้อมูลไว้ให้ผู้บริโภคอ่าน

บัฟเฟอร์ได้รับการสนับสนุนโดยอาร์เรย์ของธง (อาร์เรย์จำนวนเต็ม) ของความยาวที่อธิบายถึงความพร้อมใช้งานของช่องบัฟเฟอร์ (ดูรายละเอียดเพิ่มเติม) มีการเข้าถึงอาร์เรย์เช่น java # AtomicIntegerArray ดังนั้นเพื่อจุดประสงค์ในการอธิบายนี้คุณอาจถือว่ามันเป็นหนึ่ง

อาจมีผู้ผลิตจำนวนเท่าใดก็ได้ เมื่อผู้ผลิตต้องการเขียนลงในบัฟเฟอร์จะมีการสร้างจำนวนที่ยาวขึ้น (เช่นเดียวกับในการเรียก AtomicLong # getAndIncrement, Disruptor ใช้การใช้งานจริงของตัวเอง แต่ทำงานในลักษณะเดียวกัน) เราเรียกสิ่งนี้ว่า ในทำนองเดียวกัน ConsumerCallId จะถูกสร้างขึ้นเมื่อ consumer ENDS อ่านสล็อตจากบัฟเฟอร์ เข้าถึง ConsumerCallId ล่าสุด

(หากมีผู้บริโภคจำนวนมากการโทรที่มี id ต่ำสุดจะถูกเลือก)

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

(ถ้า producerCallId มากกว่า ConsumerCallId + bufferSize ล่าสุดแสดงว่าบัฟเฟอร์เต็มและผู้ผลิตถูกบังคับให้รอจนกระทั่งบัสว่าง)

โปรดิวเซอร์ได้รับการกำหนดสล็อตในบัฟเฟอร์ตาม callId ของเขา (ซึ่งคือ prducerCallId modulo bufferSize แต่เนื่องจาก bufferSize อยู่เสมอกำลังของ 2 (จำกัด บังคับใช้กับการสร้างบัฟเฟอร์) การดำเนินการ actuall ที่ใช้คือ producerCallId & (bufferSize - 1 )) จากนั้นจะสามารถแก้ไขเหตุการณ์ในสล็อตนั้นได้ฟรี

(อัลกอริทึมที่เกิดขึ้นจริงนั้นซับซ้อนกว่าเล็กน้อยซึ่งเกี่ยวข้องกับการแคช ConsumerId ล่าสุดในการอ้างอิงอะตอมมิกแยกกันเพื่อจุดประสงค์ในการปรับให้เหมาะสม)

เมื่อเหตุการณ์มีการแก้ไขการเปลี่ยนแปลงคือ "เผยแพร่" เมื่อเผยแพร่สล็อตตามลำดับในอาร์เรย์แฟล็กจะเต็มไปด้วยแฟล็กที่อัพเดต ค่าแฟล็กคือจำนวนของลูป (producerCallId หารด้วย bufferSize (อีกครั้งเนื่องจาก bufferSize คือกำลัง 2 การดำเนินการจริงคือการเปลี่ยนแปลงที่ถูกต้อง)

ในทำนองเดียวกันอาจมีผู้บริโภคจำนวนเท่าใดก็ได้ ทุกครั้งที่ผู้บริโภคต้องการเข้าถึงบัฟเฟอร์ consumerCallId จะถูกสร้างขึ้น (ขึ้นอยู่กับวิธีที่ผู้บริโภคถูกเพิ่มเข้าไปในเครื่องทำลายอะตอมที่ใช้ในการสร้างรหัสอาจถูกแชร์หรือแยกกันสำหรับแต่ละรายการ) consumerCallId นี้จะถูกเปรียบเทียบกับ producentCallId ล่าสุดและหากน้อยกว่าสองรายการผู้อ่านจะได้รับอนุญาตให้ดำเนินการต่อไป

(ในทำนองเดียวกันถ้า producerCallId ยังคงเป็น consumerCallId ก็หมายความว่าบัฟเฟอร์นั้นมีคุณสมบัติและผู้บริโภคถูกบังคับให้รอลักษณะของการรอจะถูกกำหนดโดย WaitStrategy ในระหว่างการสร้าง disruptor)

สำหรับผู้บริโภครายบุคคล (ผู้ที่มีตัวสร้างรหัสของตัวเอง) สิ่งต่อไปที่ตรวจสอบคือความสามารถในการใช้แบทช์ สล็อตในบัฟเฟอร์ถูกตรวจสอบตามลำดับจากรายการที่สัมพันธ์กับ consumerCallId (ดัชนีถูกกำหนดในลักษณะเดียวกับผู้ผลิต) ไปยังรายการที่เกี่ยวข้องกับ producerCallId ล่าสุด

พวกเขาจะถูกตรวจสอบในวงโดยการเปรียบเทียบค่าสถานะที่เขียนในอาร์เรย์ธงกับค่าสถานะที่สร้างขึ้นสำหรับ ConsumerCallId หากธงตรงกันนั่นหมายความว่าผู้ผลิตที่เติมช่องได้ยอมรับการเปลี่ยนแปลงของพวกเขา ถ้าไม่ใช่ลูปจะถูกทำลายและจะส่งคืน changeId ที่คอมมิตที่ยอมรับสูงสุด สล็อตจาก ConsumerCallId ถึงได้รับใน changeId สามารถบริโภคเป็นชุด

หากกลุ่มผู้บริโภคอ่านด้วยกัน (กลุ่มที่มีตัวสร้าง id ที่แชร์) แต่ละคนจะรับ callId เพียงครั้งเดียวและเฉพาะสล็อตสำหรับ callId นั้นเท่านั้นที่จะถูกตรวจสอบและส่งคืน


7

จากบทความนี้ :

รูปแบบ disruptor คือคิวการแบตช์ที่สำรองไว้โดยอาร์เรย์แบบวงกลม (เช่นบัฟเฟอร์วงแหวน) ที่เต็มไปด้วยวัตถุการถ่ายโอนที่จัดสรรไว้ล่วงหน้าซึ่งใช้หน่วยความจำ - อุปสรรคในการซิงโครไนซ์ผู้ผลิตและผู้บริโภคผ่านลำดับ

อุปสรรคของหน่วยความจำนั้นยากที่จะอธิบายและบล็อกของ Trisha ได้พยายามอย่างดีที่สุดในความคิดของฉันกับโพสต์นี้: http://mechanitis.blogspot.com/2011/08/dissecting-disruptor-why-its-so-fast HTML

แต่ถ้าคุณไม่ต้องการที่จะดำน้ำเข้าไปในรายละเอียดในระดับต่ำที่คุณก็สามารถรู้ว่าหน่วยความจำอุปสรรคในชวาจะดำเนินการผ่านคำหลักหรือผ่านvolatile java.util.concurrent.AtomicLongลำดับรูปแบบของตัวทำลายนั้นAtomicLongมีการสื่อสารกันไปมาระหว่างผู้ผลิตและผู้บริโภคผ่านทางหน่วยความจำ - อุปสรรคแทนที่จะเป็นล็อค

ฉันคิดว่ามันง่ายกว่าที่จะเข้าใจแนวคิดผ่าน code ดังนั้นโค้ดด้านล่างนี้เป็นhelloworldง่ายๆจากCoralQueueซึ่งเป็นการใช้รูปแบบการทำลายที่ดำเนินการโดย CoralBlocks ที่ฉันเข้าร่วม ในรหัสด้านล่างคุณสามารถดูว่ารูปแบบของเครื่องทำลายนั้นใช้การแบตช์และวิธีที่วงแหวนบัฟเฟอร์ (เช่นอาร์เรย์แบบวงกลม) อนุญาตให้มีการสื่อสารแบบไม่ใช้ขยะระหว่างสองเธรด:

package com.coralblocks.coralqueue.sample.queue;

import com.coralblocks.coralqueue.AtomicQueue;
import com.coralblocks.coralqueue.Queue;
import com.coralblocks.coralqueue.util.MutableLong;

public class Sample {

    public static void main(String[] args) throws InterruptedException {

        final Queue<MutableLong> queue = new AtomicQueue<MutableLong>(1024, MutableLong.class);

        Thread consumer = new Thread() {

            @Override
            public void run() {

                boolean running = true;

                while(running) {
                    long avail;
                    while((avail = queue.availableToPoll()) == 0); // busy spin
                    for(int i = 0; i < avail; i++) {
                        MutableLong ml = queue.poll();
                        if (ml.get() == -1) {
                            running = false;
                        } else {
                            System.out.println(ml.get());
                        }
                    }
                    queue.donePolling();
                }
            }

        };

        consumer.start();

        MutableLong ml;

        for(int i = 0; i < 10; i++) {
            while((ml = queue.nextToDispatch()) == null); // busy spin
            ml.set(System.nanoTime());
            queue.flush();
        }

        // send a message to stop consumer...
        while((ml = queue.nextToDispatch()) == null); // busy spin
        ml.set(-1);
        queue.flush();

        consumer.join(); // wait for the consumer thread to die...
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.