เมื่อใดควรใช้สารระเหยร่วมกับเธรดหลายตัว?


131

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

ดังนั้นการใช้งาน / วัตถุประสงค์ของการระเหยในโปรแกรมมัลติเธรดคืออะไร?


3
ในบางกรณีคุณไม่ต้องการ / ต้องการการปกป้องโดย mutex
Stefan Mai

4
บางครั้งมันก็ดีที่จะมีสภาพการแข่งขันบางครั้งก็ไม่เป็นเช่นนั้น คุณใช้ตัวแปรนี้อย่างไร?
David Heffernan

3
@ เดวิด: ตัวอย่างของการที่จะมีการแข่งขันได้หรือไม่?
John Dibling

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

5
@ จอห์นฮาร์ดแวร์ที่ใช้โค้ดนี้รับประกันว่าตัวแปรที่จัดแนวไม่สามารถทนต่อการฉีกขาดได้ หากผู้ปฏิบัติงานกำลังอัปเดต n เป็น n + 1 ตามที่ผู้อ่านอ่านผู้อ่านจะไม่สนใจว่าพวกเขาจะได้รับ n หรือ n + 1 จะไม่มีการตัดสินใจที่สำคัญเนื่องจากใช้สำหรับการรายงานความคืบหน้าเท่านั้น
David Heffernan

คำตอบ:


168

คำตอบสั้น ๆ และรวดเร็ว : volatile(เกือบ) ไม่มีประโยชน์สำหรับการเขียนโปรแกรมแอปพลิเคชันที่ไม่เชื่อเรื่องพระเจ้าบนแพลตฟอร์มแบบมัลติเธรด ไม่มีการซิงโครไนซ์ใด ๆ ไม่สร้างรั้วหน่วยความจำและไม่รับรองลำดับการดำเนินการของการดำเนินการ มันไม่ได้ทำให้การดำเนินงานเป็นปรมาณู มันไม่ได้ทำให้รหัสของคุณปลอดภัยอย่างน่าอัศจรรย์ volatileอาจเป็นสิ่งอำนวยความสะดวกเดียวที่เข้าใจผิดมากที่สุดใน C ++ ทั้งหมด ดูนี้ , นี้และนี้สำหรับข้อมูลเพิ่มเติมเกี่ยวกับvolatile

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

volatileมีวัตถุประสงค์เฉพาะเพื่อใช้เมื่อเชื่อมต่อกับฮาร์ดแวร์ที่แมปหน่วยความจำตัวจัดการสัญญาณและคำสั่งรหัสเครื่อง setjmp สิ่งนี้ทำให้volatileสามารถใช้ได้โดยตรงกับการเขียนโปรแกรมระดับระบบมากกว่าการเขียนโปรแกรมระดับแอปพลิเคชันปกติ

มาตรฐาน C ++ ปี 2003 ไม่ได้กล่าวว่าvolatileจะใช้ความหมายของการได้มาหรือการเผยแพร่กับตัวแปรใด ๆ ในความเป็นจริง Standard เงียบสนิทในทุกเรื่องของมัลติเธรด อย่างไรก็ตามแพลตฟอร์มเฉพาะจะใช้ความหมายของการได้มาและการเผยแพร่กับvolatileตัวแปร

[อัปเดตสำหรับ C ++ 11]

c ++ 11 มาตรฐานในขณะนี้ไม่ทราบ multithreading โดยตรงในรูปแบบหน่วยความจำและ lanuage และมันยังมีสิ่งอำนวยความสะดวกห้องสมุดที่จะจัดการกับมันในทางแพลตฟอร์มอิสระ อย่างไรก็ตามความหมายของvolatileยังคงไม่เปลี่ยนแปลง volatileยังไม่ใช่กลไกการซิงโครไนซ์ Bjarne Stroustrup พูดมากใน TCPPPL4E:

ห้ามใช้volatileยกเว้นในโค้ดระดับต่ำที่เกี่ยวข้องโดยตรงกับฮาร์ดแวร์

อย่าถือว่าvolatileมีความหมายพิเศษในโมเดลหน่วยความจำ มันไม่ใช่. มันไม่ได้เป็นกลไกการซิงโครไนซ์ในภาษาต่อมา หากต้องการรับการซิงโครไนซ์ให้ใช้atomica mutexหรือ a condition_variable.

[/ สิ้นสุดการอัปเดต]

ทั้งหมดข้างต้นใช้ภาษา C ++ เองตามที่กำหนดโดยมาตรฐานปี 2003 (และปัจจุบันเป็นมาตรฐานปี 2011) อย่างไรก็ตามบางแพลตฟอร์มจะเพิ่มฟังก์ชันหรือข้อ จำกัด เพิ่มเติมให้กับสิ่งที่volatileทำ ตัวอย่างเช่นใน MSVC 2010 (อย่างน้อย) Acquire and Release semantics จะใช้กับการดำเนินการบางอย่างกับvolatileตัวแปร จาก MSDN :

เมื่อทำการปรับให้เหมาะสมคอมไพลเลอร์ต้องรักษาการจัดลำดับระหว่างการอ้างอิงถึงอ็อบเจ็กต์ที่ระเหยได้ตลอดจนการอ้างอิงไปยังอ็อบเจ็กต์ส่วนกลางอื่น โดยเฉพาะอย่างยิ่ง,

การเขียนถึงวัตถุระเหย (volatile write) มี Release semantics; การอ้างอิงถึงวัตถุทั่วโลกหรือแบบคงที่ที่เกิดขึ้นก่อนการเขียนไปยังวัตถุที่ระเหยได้ในลำดับคำสั่งจะเกิดขึ้นก่อนการเขียนสารระเหยนั้นในไบนารีที่คอมไพล์

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

อย่างไรก็ตามคุณอาจทราบข้อเท็จจริงที่ว่าหากคุณทำตามลิงก์ด้านบนมีการถกเถียงกันในความคิดเห็นว่าการได้รับ / ปลดปล่อยความหมายมีผลบังคับใช้จริงหรือไม่ในกรณีนี้


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

20
@ เบ็นเพียงเพราะบางสิ่งบางอย่างท้าทายความเชื่อของคุณไม่ได้ทำให้มันโอ่อ่า
David Heffernan

39
@ Ben: ไม่อ่านข้อมูลเกี่ยวกับสิ่งที่volatileจริงไม่ใน C ++ สิ่งที่ @John พูดนั้นถูกต้องจบเรื่อง ไม่มีส่วนเกี่ยวข้องกับรหัสแอปพลิเคชันเทียบกับรหัสไลบรารีหรือ "ธรรมดา" เทียบกับ "โปรแกรมเมอร์รอบรู้เหมือนพระเจ้า" สำหรับเรื่องนั้น volatileไม่จำเป็นและไม่มีประโยชน์สำหรับการซิงโครไนซ์ระหว่างเธรด ไม่สามารถใช้ไลบรารีเธรดในแง่ของvolatile; volatileก็มีการพึ่งพารายละเอียดเฉพาะแพลตฟอร์มอยู่แล้วและเมื่อคุณต้องพึ่งพาผู้ต้องคุณไม่
jalf

6
@jalf: "การระเหยเป็นสิ่งที่ไม่จำเป็นและไม่มีประโยชน์สำหรับการซิงโครไนซ์ระหว่างเธรด" (ซึ่งก็คือสิ่งที่คุณพูด) ไม่ใช่สิ่งเดียวกับ "การระเหยนั้นไร้ประโยชน์สำหรับการเขียนโปรแกรมหลายเธรด" (ซึ่งเป็นสิ่งที่จอห์นกล่าวในคำตอบ) คุณถูกต้อง 100% แต่ฉันไม่เห็นด้วยกับ John (บางส่วน) - ยังสามารถใช้ volatile สำหรับการเขียนโปรแกรมแบบมัลติเธรดได้ (สำหรับชุดงานที่ จำกัด มาก)

4
@GMan: ทุกสิ่งที่เป็นประโยชน์มีประโยชน์ภายใต้ข้อกำหนดหรือเงื่อนไขบางอย่างเท่านั้น การระเหยมีประโยชน์สำหรับการเขียนโปรแกรมแบบมัลติเธรดภายใต้เงื่อนไขที่เข้มงวด (และในบางกรณีอาจดีกว่า (สำหรับคำจำกัดความที่ดีกว่า) มากกว่าทางเลือกอื่น) คุณพูดว่า "ไม่สนใจสิ่งนี้และ .. " แต่กรณีที่ volatile มีประโยชน์สำหรับการอ่านหลายเธรด คุณสร้างสิ่งที่ฉันไม่เคยอ้างสิทธิ์ ใช่ประโยชน์ของสารระเหยมี จำกัด แต่มีอยู่จริง - แต่เราทุกคนสามารถยอมรับได้ว่าไม่มีประโยชน์สำหรับการซิงโครไนซ์

31

(หมายเหตุของบรรณาธิการ: ใน C ++ 11 volatileไม่ใช่เครื่องมือที่เหมาะสมสำหรับงานนี้และยังมี data-race UB ใช้std::atomic<bool>กับstd::memory_order_relaxedโหลด / ร้านค้าเพื่อทำสิ่งนี้โดยไม่ใช้ UB ในการใช้งานจริงจะคอมไพล์เป็น asm เดียวกับที่volatileฉันเพิ่มคำตอบที่มีรายละเอียดมากขึ้นและยังอยู่ความเข้าใจผิดในความคิดเห็นว่าหน่วยความจำที่ไม่ค่อยสั่งซื้ออาจจะมีปัญหาในการนี้ใช้ในกรณี: ทุกซีพียูโลกแห่งความจริงมีหน่วยความจำที่ใช้ร่วมกันเชื่อมโยงกันเพื่อvolatileจะทำงานสำหรับการนี้ในจริง C ++ การใช้งาน แต่ก็ยังคงไม่. ไม่ทำ

การสนทนาในความคิดเห็นบางส่วนดูเหมือนจะพูดถึงกรณีการใช้งานอื่น ๆ ที่คุณ จะต้องการสิ่งที่แข็งแกร่งกว่าอะตอมผ่อนคลาย คำตอบนี้ชี้volatileให้เห็นแล้วว่าคุณไม่ต้องสั่งซื้อ)


ระเหยมีประโยชน์ในบางครั้งด้วยเหตุผลต่อไปนี้: รหัสนี้:

/* global */ bool flag = false;

while (!flag) {}

ได้รับการปรับให้เหมาะสมโดย gcc เพื่อ:

if (!flag) { while (true) {} }

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

มิฉะนั้นคีย์เวิร์ดระเหยจะแปลกเกินไปที่จะใช้งานได้ - ไม่มีการรับประกันการสั่งซื้อหน่วยความจำใด ๆ ที่เขียนทั้งการเข้าถึงแบบระเหยและไม่ลบเลือนและไม่ได้ให้การดำเนินการเกี่ยวกับอะตอมใด ๆ นั่นคือคุณไม่ได้รับความช่วยเหลือจากคอมไพเลอร์ที่มีคีย์เวิร์ดระเหยยกเว้นการแคชรีจิสเตอร์ที่ปิดใช้งาน .


4
ถ้าฉันจำอะตอม C ++ 0x มีขึ้นเพื่อทำอย่างถูกต้องในสิ่งที่คนจำนวนมากเชื่อว่า (ไม่ถูกต้อง) ทำโดยการระเหย
David Heffernan

14
volatileไม่ป้องกันการเข้าถึงหน่วยความจำจากการจัดลำดับใหม่ volatileการเข้าถึงจะไม่ได้รับการจัดลำดับใหม่ด้วยความเคารพซึ่งกันและกัน แต่พวกเขาไม่ให้การรับประกันใด ๆเกี่ยวกับการจัดลำดับใหม่ที่เกี่ยวกับสิ่งที่ไม่ใช่volatileวัตถุดังนั้นโดยพื้นฐานแล้วพวกมันก็ไม่มีประโยชน์เหมือนแฟล็กเช่นกัน
jalf

14
@ เบ็น: ฉันคิดว่าคุณคว่ำแล้ว ฝูงชนที่ "ระเหยนั้นไร้ประโยชน์" อาศัยข้อเท็จจริงง่ายๆที่ว่าการระเหยไม่ได้ป้องกันการจัดลำดับใหม่ซึ่งหมายความว่าไม่มีประโยชน์อย่างยิ่งสำหรับการซิงโครไนซ์ วิธีการอื่น ๆ อาจไร้ประโยชน์เท่า ๆ กัน (ดังที่คุณกล่าวถึงการเพิ่มประสิทธิภาพโค้ดเวลาลิงก์อาจทำให้คอมไพเลอร์สามารถมองเข้าไปในโค้ดที่คุณคิดว่าคอมไพเลอร์จะถือว่าเป็นกล่องดำ) แต่นั่นไม่ได้แก้ไขข้อบกพร่องของvolatile.
jalf

15
@jalf: ดูบทความโดย Arch Robinson (เชื่อมโยงที่อื่นในหน้านี้) ความคิดเห็นที่ 10 (โดย "Spud") โดยทั่วไปการเรียงลำดับใหม่จะไม่เปลี่ยนตรรกะของรหัส รหัสที่โพสต์ใช้แฟล็กเพื่อยกเลิกงาน (แทนที่จะส่งสัญญาณว่างานเสร็จสิ้น) ดังนั้นจึงไม่สำคัญว่างานจะถูกยกเลิกก่อนหรือหลังรหัส (เช่น: while (work_left) { do_piece_of_work(); if (cancel) break;}ถ้าการยกเลิกถูกเรียงลำดับใหม่ภายในลูป ตรรกะยังคงใช้ได้ฉันมีโค้ดส่วนหนึ่งที่ใช้งานได้ในทำนองเดียวกัน: หากเธรดหลักต้องการยุติมันจะตั้งค่าสถานะสำหรับเธรดอื่น ๆ แต่มันไม่ ...

15
... ไม่ว่าเธรดอื่นจะทำการวนซ้ำการทำงานของพวกเขาซ้ำสองสามครั้งก่อนที่จะยุติลงตราบใดที่มันเกิดขึ้นหลังจากตั้งค่าสถานะในไม่ช้า แน่นอนว่านี่เป็นการใช้งานเฉพาะที่ฉันคิดได้และมันค่อนข้างเฉพาะ (และอาจใช้ไม่ได้บนแพลตฟอร์มที่การเขียนไปยังตัวแปรระเหยไม่ได้ทำให้เธรดอื่นมองเห็นการเปลี่ยนแปลงได้แม้ว่าอย่างน้อย x86 และ x86-64 นี้ ผลงาน) แน่นอนฉันจะไม่แนะนำให้ใครทำเช่นนั้นโดยไม่มีเหตุผลที่ดีฉันแค่บอกว่าคำสั่งแบบครอบคลุมเช่น "ระเหยไม่ได้มีประโยชน์ในโค้ดมัลติเธรด" นั้นไม่ถูกต้อง 100%

16

ใน C ++ 11 โดยปกติไม่เคยใช้volatileสำหรับเธรดเฉพาะสำหรับ MMIO

แต่ TL: DR มัน "ทำงาน" เหมือนกับ atomic กับmo_relaxedฮาร์ดแวร์ที่มีแคชที่สอดคล้องกัน (เช่นทุกอย่าง); ก็เพียงพอแล้วที่จะหยุดคอมไพเลอร์ที่เก็บ vars ไว้ในรีจิสเตอร์ atomicไม่จำเป็นต้องมีอุปสรรคด้านหน่วยความจำในการสร้าง atomicity หรือการมองเห็นระหว่างเธรดเพียงเพื่อให้เธรดปัจจุบันรอก่อน / หลังการดำเนินการเพื่อสร้างลำดับระหว่างการเข้าถึงของเธรดนี้ไปยังตัวแปรต่างๆ mo_relaxedไม่ต้องมีอุปสรรคใด ๆ เพียงแค่โหลดจัดเก็บหรือ RMW

สำหรับอะตอมม้วนของคุณเองด้วยvolatile(และอินไลน์ asm สำหรับอุปสรรค) ในวันเก่าเสียก่อน C ++ 11 std::atomic,volatileเป็นวิธีที่ดีเท่านั้นที่จะได้รับบางสิ่งบางอย่างในการทำงาน แต่ขึ้นอยู่กับสมมติฐานมากมายเกี่ยวกับวิธีการใช้งานและไม่เคยได้รับการรับรองจากมาตรฐานใด ๆ

ตัวอย่างเช่นเคอร์เนลลินุกซ์ยังคงใช้อะตอมที่รีดด้วยมือของตัวเองด้วยvolatileแต่รองรับการใช้งาน C เฉพาะบางส่วนเท่านั้น (GNU C เสียงดังและอาจเป็น ICC) ส่วนหนึ่งเป็นเพราะส่วนขยาย GNU C และไวยากรณ์ asm แบบอินไลน์และความหมาย แต่ยังขึ้นอยู่กับสมมติฐานบางประการเกี่ยวกับวิธีการทำงานของคอมไพเลอร์

เกือบจะเป็นตัวเลือกที่ผิดสำหรับโครงการใหม่ คุณสามารถใช้std::atomic(มีstd::memory_order_relaxed) volatileเพื่อให้ได้คอมไพเลอร์ที่จะปล่อยรหัสเครื่องเดียวที่มีประสิทธิภาพให้คุณได้ด้วย std::atomicกับmo_relaxedล้าสมัยvolatileเพื่อวัตถุประสงค์ในการทำเกลียว (ยกเว้นบางทีเพื่อแก้ไขข้อบกพร่องที่ไม่ได้รับการปรับให้เหมาะสมกับatomic<double>คอมไพเลอร์บางตัว)

การใช้งานภายในของstd::atomicคอมไพเลอร์กระแสหลัก (เช่น gcc และ clang) ไม่ได้ใช้แค่volatileภายในเท่านั้น คอมไพเลอร์เผยให้เห็นโหลดอะตอมจัดเก็บและฟังก์ชันในตัว RMW โดยตรง (เช่นGNU C __atomicbuiltinsซึ่งทำงานบนวัตถุ "ธรรมดา")


ระเหยสามารถใช้ได้ในทางปฏิบัติ (แต่อย่าทำ)

ที่กล่าวว่าvolatileสามารถใช้งานได้ในทางปฏิบัติสำหรับสิ่งต่างๆเช่นการexit_nowตั้งค่าสถานะในการใช้งาน C ++ ที่มีอยู่ทั้งหมด (?) บนซีพียูจริงเนื่องจากซีพียูทำงานอย่างไร (แคชที่สอดคล้องกัน) และสมมติฐานที่ใช้ร่วมกันเกี่ยวกับวิธีการvolatileทำงาน แต่อย่างอื่นไม่มากและไม่แนะนำ คำตอบนี้มีจุดประสงค์เพื่ออธิบายว่า CPU และการใช้งาน C ++ ที่มีอยู่ทำงานอย่างไร หากคุณไม่สนใจสิ่งนั้นสิ่งที่คุณต้องรู้ก็คือstd::atomicมี mo_relaxed obsoletes volatileสำหรับเธรด

(มาตรฐาน ISO C ++ ค่อนข้างคลุมเครือเพียงแค่บอกว่าการvolatileเข้าถึงควรได้รับการประเมินอย่างเคร่งครัดตามกฎของเครื่องนามธรรม C ++ ไม่ใช่การปรับให้เหมาะสมเนื่องจากการใช้งานจริงจะใช้พื้นที่แอดเดรสหน่วยความจำของเครื่องเพื่อจำลองพื้นที่ที่อยู่ C ++ ซึ่งหมายความว่าการvolatileอ่านและการกำหนดต้องคอมไพล์เพื่อโหลด / จัดเก็บคำสั่งเพื่อเข้าถึงการแสดงวัตถุในหน่วยความจำ)


ดังที่คำตอบอื่นชี้ให้เห็นexit_nowแฟล็กเป็นกรณีง่ายๆของการสื่อสารระหว่างเธรดที่ไม่จำเป็นต้องมีการซิงโครไนซ์ใด ๆ : ไม่ได้เผยแพร่ว่าเนื้อหาอาร์เรย์พร้อมหรืออะไรทำนองนั้น มีเพียงร้านค้าที่สังเกตเห็นได้ทันทีจากการโหลดที่ไม่ได้รับการปรับให้เหมาะสมในเธรดอื่น

    // global
    bool exit_now = false;

    // in one thread
    while (!exit_now) { do_stuff; }

    // in another thread, or signal handler in this thread
    exit_now = true;

หากไม่มีการระเหยหรือปรมาณูกฎ as-if และสมมติฐานของการไม่มีข้อมูลการแข่งขัน UB ช่วยให้คอมไพเลอร์สามารถปรับให้เหมาะสมเป็น asm ที่ตรวจสอบแฟล็กเพียงครั้งเดียวก่อนที่จะเข้าสู่ (หรือไม่) ลูปที่ไม่มีที่สิ้นสุด นี่คือสิ่งที่เกิดขึ้นในชีวิตจริงสำหรับคอมไพเลอร์ตัวจริง (และโดยปกติจะปรับให้เหมาะสมที่สุดdo_stuffเนื่องจากลูปไม่เคยออกดังนั้นโค้ดใด ๆ ในภายหลังที่อาจใช้ผลลัพธ์จะไม่สามารถเข้าถึงได้หากเราเข้าสู่ลูป)

 // Optimizing compilers transform the loop into asm like this
    if (!exit_now) {        // check once before entering loop
        while(1) do_stuff;  // infinite loop
    }

โปรแกรมมัลติเธรดติดอยู่ในโหมดปรับให้เหมาะสม แต่ทำงานตามปกติใน -O0เป็นตัวอย่าง (พร้อมคำอธิบายเอาต์พุต asm ของ GCC) ว่าสิ่งนี้เกิดขึ้นกับ GCC บน x86-64 ได้อย่างไร นอกจากนี้การเขียนโปรแกรม MCU - การเพิ่มประสิทธิภาพ C ++ O2 หยุดลงในขณะที่วนซ้ำบนอุปกรณ์อิเล็กทรอนิกส์ SE แสดงตัวอย่างอื่น

โดยปกติเราต้องการการเพิ่มประสิทธิภาพเชิงรุกที่ CSE และรอกโหลดจากลูปรวมถึงตัวแปรส่วนกลาง

ก่อน C ++ 11 volatile bool exit_nowเป็นวิธีหนึ่งในการทำให้งานนี้เป็นไปตามที่ตั้งใจไว้ (ในการใช้งาน C ++ ปกติ) แต่ใน C ++ 11 UB การแข่งขันข้อมูลยังคงมีผลบังคับใช้volatileดังนั้นจึงไม่ได้รับการรับรองจากมาตรฐาน ISO ว่าจะทำงานได้ทุกที่แม้จะสมมติว่าแคชที่เชื่อมโยงกันของ HW

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

โปรดทราบว่า "ตามที่ตั้งใจไว้" ไม่ได้หมายความว่าเธรดกำลังexit_nowรอให้เธรดอื่นออกจริง หรือแม้กระทั่งว่าจะรอให้exit_now=trueร้านค้าระเหยปรากฏให้เห็นได้ทั่วโลกก่อนที่จะดำเนินการต่อในเธรดนี้ในภายหลัง ( atomic<bool>ด้วยค่าเริ่มต้นmo_seq_cstจะทำให้รอก่อนที่จะโหลด seq_cst ในภายหลังเป็นอย่างน้อยใน ISA จำนวนมากคุณจะได้รับสิ่งกีดขวางเต็มหลังร้านค้า)

C ++ 11 จัดเตรียมวิธีที่ไม่ใช่ UB ซึ่งรวบรวมแบบเดียวกัน

ธง "วิ่งต่อไป" หรือ "ออกทันที" ควรใช้std::atomic<bool> flagกับmo_relaxed

การใช้

  • flag.store(true, std::memory_order_relaxed)
  • while( !flag.load(std::memory_order_relaxed) ) { ... }

ที่จะทำให้คุณ asm เดียวกันแน่นอน (โดยไม่มีคำแนะนำอุปสรรคแพง) volatile flagที่คุณต้องการได้รับจาก

รวมทั้งไม่มีการฉีกขาด, atomicนอกจากนี้ยังช่วยให้คุณสามารถที่จะเก็บไว้ในหนึ่งหัวข้อและโหลดในอื่นโดยไม่ UB ดังนั้นคอมไพเลอร์ไม่สามารถยกภาระออกจากวงที่ (สมมติฐานของการไม่มี UB การแข่งขันข้อมูลคือสิ่งที่อนุญาตให้มีการเพิ่มประสิทธิภาพเชิงรุกที่เราต้องการสำหรับวัตถุที่ไม่ระเหยที่ไม่ใช่อะตอม) คุณสมบัติatomic<T>นี้ค่อนข้างเหมือนกับสิ่งที่volatileทำกับโหลดบริสุทธิ์และร้านค้าที่บริสุทธิ์

atomic<T>นอกจากนี้ยังสร้าง+=และอื่น ๆ ในการดำเนินการ RMW แบบปรมาณู (มีราคาแพงกว่าการโหลดปรมาณูอย่างมีนัยสำคัญในการดำเนินการชั่วคราวจากนั้นจึงจัดเก็บอะตอมแยกต่างหากหากคุณไม่ต้องการ RMW แบบปรมาณูให้เขียนรหัสของคุณด้วย local ชั่วคราว)

ด้วยการseq_cstสั่งซื้อเริ่มต้นที่คุณจะได้รับwhile(!flag)นอกจากนี้ยังเพิ่มการรับประกันการสั่งซื้อด้วย การเข้าถึงที่ไม่ใช่อะตอมและการเข้าถึงอะตอมอื่น ๆ

(ตามทฤษฎีแล้วมาตรฐาน ISO C ++ ไม่ได้กำหนดว่าการเพิ่มประสิทธิภาพของอะตอมในการคอมไพล์ - ไทม์ แต่ในทางปฏิบัติคอมไพเลอร์ไม่ได้เป็นเพราะไม่มีวิธีควบคุมว่าเมื่อใดที่จะไม่เป็นไปตามนั้นมีบางกรณีที่volatile atomic<T>อาจไม่ได้ สามารถควบคุมการเพิ่มประสิทธิภาพของอะตอมได้เพียงพอหากคอมไพเลอร์ทำการปรับให้เหมาะสมดังนั้นสำหรับตอนนี้คอมไพเลอร์ทำไม่ได้ดูเหตุใดคอมไพเลอร์จึงไม่ผสาน std ที่ซ้ำซ้อน :: atomic เขียน โปรดทราบว่า wg21 / p0062 แนะนำให้ใช้volatile atomicในโค้ดปัจจุบันเพื่อป้องกันการเพิ่มประสิทธิภาพของ อะตอม.)


volatile ใช้งานได้จริงกับ CPU จริง (แต่ยังไม่ได้ใช้งาน)

แม้จะมีรุ่นหน่วยความจำที่ไม่ค่อยสั่งซื้อ (Non-x86) แต่ไม่ได้ใช้งานจริงมันใช้atomic<T>กับmo_relaxedแทน !! volatileจุดของส่วนนี้คือการเข้าใจผิดเกี่ยวกับการทำงานอยู่ซีพียูวิธีจริงไม่ได้ที่จะปรับ หากคุณกำลังเขียนรหัสแบบไม่ต้องล็อกคุณอาจสนใจเกี่ยวกับประสิทธิภาพ การทำความเข้าใจแคชและต้นทุนของการสื่อสารระหว่างเธรดมักมีความสำคัญต่อประสิทธิภาพที่ดี

ซีพียูจริงมีแคช / หน่วยความจำที่ใช้ร่วมกัน: หลังจากที่เก็บจากคอร์หนึ่งกลายเป็นที่มองเห็นได้ทั่วโลกไม่มีคอร์อื่นใดที่สามารถโหลดค่าที่ค้างได้ (โปรดดูMyths Programmers Believe เกี่ยวกับ CPU Cachesซึ่งพูดถึง Java volatiles ซึ่งเทียบเท่ากับ C ++ atomic<T>พร้อมลำดับหน่วยความจำ seq_cst)

เมื่อฉันพูดว่าloadฉันหมายถึงคำสั่ง asm ที่เข้าถึงหน่วยความจำ นั่นคือสิ่งที่การvolatileเข้าถึงทำให้มั่นใจได้และไม่ใช่สิ่งเดียวกับการแปลงค่า lvalue-to-rvalue ของตัวแปร C ++ ที่ไม่ใช่อะตอม / ไม่ระเหย (เช่นlocal_tmp = flagหรือwhile(!flag))

สิ่งเดียวที่คุณต้องเอาชนะคือการเพิ่มประสิทธิภาพเวลาคอมไพล์ซึ่งจะไม่โหลดซ้ำหลังจากการตรวจสอบครั้งแรก โหลด + ตรวจสอบการทำซ้ำแต่ละครั้งก็เพียงพอแล้วโดยไม่ต้องสั่งใด ๆ หากไม่มีการซิงโครไนซ์ระหว่างเธรดนี้และเธรดหลักก็ไม่มีความหมายที่จะพูดถึงเวลาที่ร้านค้าเกิดขึ้นหรือลำดับของการโหลด wrt การดำเนินการอื่น ๆ ในลูป เฉพาะเมื่อเธรดนี้มองเห็นได้เท่านั้นคือสิ่งที่สำคัญ เมื่อคุณเห็นชุดค่าสถานะ exit_now คุณจะออก แฝงอินเตอร์-core บน x86 ทั่วไป Xeon สามารถเป็นสิ่งที่ชอบ 40ns ระหว่างแกนทางแยก


ในทางทฤษฎี: เธรด C ++ บนฮาร์ดแวร์ที่ไม่มีแคชที่สอดคล้องกัน

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

ในทางทฤษฎีคุณสามารถมี C ++ การดำเนินงานในเครื่องที่ไม่ได้เป็นเช่นนี้ต้องวูบวาบอย่างชัดเจนคอมไพเลอร์ที่สร้างขึ้นเพื่อให้สิ่งที่มองเห็นหัวข้ออื่น ๆ บนแกนอื่น (หรือสำหรับการอ่านเพื่อไม่ใช้สำเนาที่อาจจะเก่า) มาตรฐาน C ++ ไม่ได้ทำให้สิ่งนี้เป็นไปไม่ได้ แต่โมเดลหน่วยความจำของ C ++ ได้รับการออกแบบมาเพื่อให้มีประสิทธิภาพบนเครื่องหน่วยความจำที่ใช้ร่วมกัน เช่นมาตรฐาน C ++ ยังพูดถึง "การเชื่อมโยงการอ่าน - การอ่าน", "การเชื่อมโยงกันของการอ่าน - เขียน" ฯลฯ หมายเหตุหนึ่งในมาตรฐานยังชี้ถึงการเชื่อมต่อกับฮาร์ดแวร์:

http://eel.is/c++draft/intro.races#19

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

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

ที่เกี่ยวข้อง: คำตอบของฉันเกี่ยวกับmov + mfence ปลอดภัยใน NUMA หรือไม่ กล่าวถึงรายละเอียดเกี่ยวกับการไม่มีอยู่ของระบบ x86 โดยไม่มีหน่วยความจำร่วมกัน นอกจากนี้ยังเกี่ยวข้องกับ: โหลดและร้านค้าการจัดเรียงใหม่บนแขนสำหรับข้อมูลเพิ่มเติมเกี่ยวกับการโหลด / ร้านค้าไปที่เดียวกันสถานที่ตั้ง

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

เพื่อให้พวกเขาสื่อสารกันผ่านการฟลัชแบบชัดแจ้งโดยทั่วไปคุณจะใช้ MPI หรือ API การส่งข้อความอื่น ๆ เพื่อให้โปรแกรมระบุช่วงที่อยู่ที่ต้องการการล้าง


ฮาร์ดแวร์จริงไม่ทำงานstd::threadข้ามขอบเขตการเชื่อมโยงกันของแคช:

ชิป ARM แบบไม่สมมาตรบางตัวมีอยู่โดยมีพื้นที่ที่อยู่ทางกายภาพที่ใช้ร่วมกัน แต่ไม่ใช่โดเมนแคชภายในที่แชร์ได้ ดังนั้นไม่สอดคล้องกัน (เช่นคอมเมนต์เธรดแกน A8 และ Cortex-M3 เช่น TI Sitara AM335x)

แต่เคอร์เนลที่แตกต่างกันจะทำงานบนคอร์เหล่านั้นไม่ใช่อิมเมจระบบเดียวที่สามารถรันเธรดในทั้งสองคอร์ได้ ฉันไม่ทราบถึงการใช้งาน C ++ ใด ๆ ที่รันstd::threadเธรดข้ามคอร์ CPU โดยไม่มีแคชที่สอดคล้องกัน

สำหรับ ARM โดยเฉพาะ GCC และ clang จะสร้างโค้ดโดยสมมติว่าเธรดทั้งหมดทำงานในโดเมนที่แชร์ได้ภายในเดียวกัน ในความเป็นจริงคู่มือ ARMv7 ISA กล่าวว่า

สถาปัตยกรรมนี้ (ARMv7) เขียนขึ้นโดยคาดหวังว่าโปรเซสเซอร์ทั้งหมดที่ใช้ระบบปฏิบัติการเดียวกันหรือไฮเปอร์ไวเซอร์จะอยู่ในโดเมน Inner Shareable Shareable

ดังนั้นหน่วยความจำแบบแบ่งใช้ที่ไม่สอดคล้องกันระหว่างโดเมนที่แยกจากกันจึงเป็นเพียงสิ่งเดียวสำหรับการใช้พื้นที่หน่วยความจำแบบแบ่งใช้เฉพาะระบบอย่างชัดเจนสำหรับการสื่อสารระหว่างกระบวนการต่างๆภายใต้เคอร์เนลที่แตกต่างกัน

ดูการอภิปรายเกี่ยวกับCoreCLRเกี่ยวกับ code-gen โดยใช้dmb ish(Inner Shareable barrier) เทียบกับdmb sy(System) memory barriers ในคอมไพเลอร์นั้น

ฉันยืนยันว่าไม่มีการใช้งาน C ++ สำหรับ ISA อื่นใดที่ทำงานstd::threadข้ามคอร์ด้วยแคชที่ไม่ต่อเนื่องกัน ฉันไม่มีหลักฐานว่าไม่มีการใช้งานดังกล่าว แต่ดูเหมือนว่าไม่น่าเป็นไปได้สูง เว้นแต่คุณจะกำหนดเป้าหมายไปยังส่วนที่แปลกใหม่ของ HW ที่ทำงานในลักษณะนั้นความคิดของคุณเกี่ยวกับประสิทธิภาพควรถือว่าการเชื่อมโยงกันของแคชเหมือน MESI ระหว่างเธรดทั้งหมด (ควรใช้atomic<T>ในรูปแบบที่รับประกันความถูกต้องแม้ว่า!)


แคชที่สอดคล้องกันทำให้ง่าย

แต่ในระบบมัลติคอร์ที่มีแคชที่สอดคล้องกันการใช้รีลีสสโตร์นั้นหมายถึงการสั่งคอมมิตในแคชสำหรับร้านค้าของเธรดนี้โดยไม่ทำการล้างข้อมูลใด ๆ อย่างชัดเจน ( https://preshing.com/20120913/acquire-and-release-semantics/และhttps://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ) (และการรับโหลดหมายถึงการสั่งให้เข้าถึงแคชในคอร์อื่น ๆ )

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

ดูการแมป C / C ++ 11 กับโปรเซสเซอร์ด้วย

ข้อเท็จจริงที่น่าสนใจ: ใน x86 ร้านค้า asm ทุกตัวเป็นรีลีสสโตร์เนื่องจากโมเดลหน่วยความจำ x86 นั้นโดยทั่วไปแล้ว seq-cst บวกบัฟเฟอร์ร้านค้า (พร้อมการส่งต่อร้านค้า)


กึ่งเกี่ยวข้อง re: บัฟเฟอร์การจัดเก็บการมองเห็นทั่วโลกและการเชื่อมโยงกัน: C ++ 11 รับประกันน้อยมาก ISAs จริงส่วนใหญ่ (ยกเว้น PowerPC) รับประกันได้ว่าเธรดทั้งหมดสามารถเห็นด้วยกับลำดับการปรากฏของร้านค้าสองแห่งโดยอีกสองเธรด (ในคำศัพท์เกี่ยวกับโมเดลหน่วยความจำสถาปัตยกรรมคอมพิวเตอร์อย่างเป็นทางการพวกเขาคือ "ปรมาณูหลายสำเนา")

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

(โมเดลหน่วยความจำ asm ที่ได้รับคำสั่งอย่างรุนแรงของ x86 หมายความว่าvolatileใน x86 อาจทำให้คุณเข้าใกล้ได้มากขึ้นmo_acq_relยกเว้นว่าการเรียงลำดับเวลาคอมไพล์ใหม่ด้วยตัวแปรที่ไม่ใช่อะตอมยังคงเกิดขึ้นได้ แต่ส่วนใหญ่ที่ไม่ใช่ x86 จะมีโมเดลหน่วยความจำที่มีลำดับต่ำดังนั้นvolatileและrelaxedมีค่าประมาณเท่า อ่อนแอเท่าที่mo_relaxedอนุญาต)


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Samuel Liew

2
การเขียนที่ยอดเยี่ยม ตรงนี้เป็นสิ่งที่ผมกำลังมองหา (ให้ทุกข้อเท็จจริง) แทนคำสั่งผ้าห่มที่เพียงแค่บอกว่า "ใช้อะตอมแทนการระเหยสำหรับโลกธงบูลีนเดียวที่ใช้ร่วมกัน"
bernie

2
@bernie: ผมเขียนนี้หลังจากที่ได้รับความผิดหวังจากการเรียกร้องซ้ำแล้วซ้ำอีกที่ไม่ได้ใช้atomicอาจนำไปสู่หัวข้อที่แตกต่างกันมีค่าแตกต่างกันสำหรับตัวแปรเดียวกันในแคช / facepalm ในแคชไม่ใช่ใน CPU จะลงทะเบียนใช่ (ด้วยตัวแปรที่ไม่ใช่อะตอม); ซีพียูใช้แคชที่สอดคล้องกัน ฉันหวังว่าคำถามอื่น ๆ เกี่ยวกับ SO จะไม่เต็มไปด้วยคำอธิบายสำหรับatomicความเข้าใจผิดเกี่ยวกับการทำงานของ CPU (เนื่องจากเป็นสิ่งที่มีประโยชน์ในการทำความเข้าใจด้วยเหตุผลด้านประสิทธิภาพและยังช่วยอธิบายว่าเหตุใดกฎอะตอม ISO C ++ จึงถูกเขียนตามที่เป็นอยู่)
Peter Cordes

-1
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

bool checkValue = false;

int main()
{
    std::thread writer([&](){
            sleep(2);
            checkValue = true;
            std::cout << "Value of checkValue set to " << checkValue << std::endl;
        });

    std::thread reader([&](){
            while(!checkValue);
        });

    writer.join();
    reader.join();
}

ครั้งหนึ่งผู้สัมภาษณ์ที่เชื่อว่าการระเหยนั้นไร้ประโยชน์เถียงกับฉันว่าการเพิ่มประสิทธิภาพจะไม่ทำให้เกิดปัญหาใด ๆ และอ้างถึงคอร์ที่แตกต่างกันซึ่งมีบรรทัดแคชแยกกันและทั้งหมดนั้น (ไม่เข้าใจจริงๆว่าเขาหมายถึงอะไร) แต่โค้ดชิ้นนี้เมื่อคอมไพล์ด้วย -O3 บน g ++ (g ++ -O3 thread.cpp -lpthread) จะแสดงพฤติกรรมที่ไม่ได้กำหนดไว้ โดยทั่วไปถ้าค่าได้รับการตั้งค่าก่อน while ให้ตรวจสอบว่าทำงานได้ดีและหากไม่เข้าสู่ลูปโดยไม่ต้องกังวลกับการดึงค่า (ซึ่งเธรดอื่นเปลี่ยนไปจริง) โดยทั่วไปฉันเชื่อว่าค่าของ checkValue จะถูกดึงเข้าไปในรีจิสเตอร์เพียงครั้งเดียวและจะไม่ถูกตรวจสอบอีกครั้งภายใต้การเพิ่มประสิทธิภาพระดับสูงสุด หากตั้งค่าเป็นจริงก่อนการดึงข้อมูลจะใช้งานได้ดีและถ้าไม่วนซ้ำ กรุณาแก้ไขฉันถ้าฉันผิด


4
สิ่งนี้เกี่ยวข้องกับvolatileอะไร? ใช่รหัสนี้คือ UB - แต่เป็น UB ด้วยvolatileเช่นกัน
David Schwartz

-2

คุณต้องการความผันผวนและอาจเกิดการล็อก

ความผันผวนจะบอกผู้เพิ่มประสิทธิภาพว่าค่าสามารถเปลี่ยนแปลงแบบอะซิงโครนัสได้

volatile bool flag = false;

while (!flag) {
    /*do something*/
}

จะอ่านค่าสถานะทุกครั้งรอบ ๆ ลูป

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

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


1
แต่นี่ไม่ใช่และตัวอย่างเดียวกันในการตอบกลับอื่น ๆ การรอคอยที่วุ่นวายและเป็นสิ่งที่ควรหลีกเลี่ยง? หากนี่เป็นตัวอย่างที่สร้างขึ้นมีตัวอย่างชีวิตจริงที่ไม่ได้ถูกสร้างขึ้นหรือไม่?
David Preston

7
@ คริส: การรอไม่ว่างในบางครั้งเป็นทางออกที่ดี โดยเฉพาะอย่างยิ่งหากคุณคาดว่าจะต้องรอเพียงสองรอบนาฬิกาก็จะมีค่าใช้จ่ายที่น้อยกว่าวิธีการระงับเธรดที่มีน้ำหนักมาก แน่นอนดังที่ฉันได้กล่าวไว้ในความคิดเห็นอื่น ๆ ตัวอย่างเช่นข้อนี้มีข้อบกพร่องเนื่องจากถือว่าการอ่าน / เขียนลงในแฟล็กจะไม่ได้รับการจัดลำดับใหม่ตามรหัสที่ปกป้องและไม่มีการรับประกันดังกล่าวเป็นต้น , volatileไม่เป็นประโยชน์จริงๆแม้ในกรณีนี้ แต่การรออย่างวุ่นวายเป็นเทคนิคที่มีประโยชน์ในบางครั้ง
jalf

3
@richard ใช่และไม่ใช่ ครึ่งแรกถูกต้อง แต่นี่หมายความว่าซีพียูและคอมไพเลอร์ไม่ได้รับอนุญาตให้จัดลำดับตัวแปรที่ผันผวนซ้ำกัน ถ้าฉันอ่านตัวแปรระเหย A แล้วอ่านตัวแปรที่ระเหยได้ B คอมไพเลอร์จะต้องส่งรหัสที่รับประกัน (แม้จะมีการเรียงลำดับใหม่ของ CPU) เพื่ออ่าน A ก่อน B แต่ก็ไม่รับประกันเกี่ยวกับการเข้าถึงตัวแปรที่ไม่ลบเลือนทั้งหมด . สามารถจัดเรียงใหม่รอบ ๆ การอ่าน / เขียนแบบระเหยของคุณได้ดี ดังนั้นถ้าคุณทำทุกตัวแปรในโปรแกรมของคุณมีความผันผวนก็จะไม่ให้การรับประกันที่คุณกำลังสนใจใน
jalf

2
@ ctrl-alt-delor: นั่นไม่ใช่สิ่งที่volatile"ไม่เรียงลำดับใหม่" หมายถึง คุณหวังว่ามันหมายความว่าร้านค้าจะปรากฏให้เห็นทั่วโลก (ไปยังเธรดอื่น ๆ ) ตามลำดับโปรแกรม นั่นคือสิ่งที่atomic<T>มีmemory_order_releaseหรือseq_cstให้คุณ แต่volatile เพียงช่วยให้คุณมีการรับประกันไม่มีเวลารวบรวมจัดเรียงใหม่: การเข้าถึงแต่ละจะปรากฏใน asm ในการสั่งซื้อโปรแกรม มีประโยชน์สำหรับไดรเวอร์อุปกรณ์ และมีประโยชน์สำหรับการโต้ตอบกับตัวจัดการขัดจังหวะตัวดีบักเกอร์หรือตัวจัดการสัญญาณบนแกน / เธรดปัจจุบัน แต่ไม่ใช่สำหรับการโต้ตอบกับคอร์อื่น ๆ
Peter Cordes

1
volatileในทางปฏิบัติก็เพียงพอแล้วสำหรับการตรวจสอบkeep_runningแฟล็กเหมือนที่คุณทำที่นี่: ซีพียูจริงมักจะมีแคชที่สอดคล้องกันซึ่งไม่จำเป็นต้องมีการล้างข้อมูล แต่มีเหตุผลที่จะขอแนะนำให้ไม่มีvolatileมากกว่าatomic<T>ด้วยmo_relaxed; คุณจะได้รับ asm เดียวกัน
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.