คีย์เวิร์ดที่ลบเลือน C ++ แนะนำรั้วหน่วยความจำหรือไม่


86

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

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


มีการอภิปรายที่น่าสนใจสำหรับคำถามที่เกี่ยวข้องนี้

Jonathan Wakely เขียน :

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

ซึ่งDavid Schwartzตอบกลับในความคิดเห็น :

... ไม่มีความแตกต่างจากมุมมองของมาตรฐาน C ++ ระหว่างคอมไพเลอร์กำลังทำอะไรบางอย่างกับคอมไพเลอร์ส่งคำสั่งที่ทำให้ฮาร์ดแวร์ทำบางอย่าง หากซีพียูอาจจัดลำดับการเข้าถึงใหม่ไปยัง volatiles มาตรฐานก็ไม่จำเป็นต้องรักษาลำดับของมันไว้ ...

... มาตรฐาน C ++ ไม่ได้สร้างความแตกต่างเกี่ยวกับสิ่งที่จัดลำดับใหม่ และคุณไม่สามารถโต้แย้งได้ว่า CPU สามารถเรียงลำดับใหม่ได้โดยไม่มีเอฟเฟกต์ที่สังเกตเห็นได้ดังนั้นจึงไม่เป็นไร - มาตรฐาน C ++ กำหนดลำดับของมันให้สังเกตได้ คอมไพเลอร์เป็นไปตามมาตรฐาน C ++ บนแพลตฟอร์มหากสร้างโค้ดที่ทำให้แพลตฟอร์มทำตามที่มาตรฐานต้องการ หากมาตรฐานกำหนดให้เข้าถึง volatiles ไม่ได้รับการจัดลำดับใหม่แสดงว่าแพลตฟอร์มที่จัดลำดับใหม่ไม่เป็นไปตามข้อกำหนด ...

ประเด็นของฉันคือถ้ามาตรฐาน C ++ ห้ามไม่ให้คอมไพเลอร์จัดลำดับการเข้าถึงใหม่ไปยัง volatiles ที่แตกต่างกันบนทฤษฎีที่ว่าลำดับของการเข้าถึงดังกล่าวเป็นส่วนหนึ่งของพฤติกรรมที่สังเกตได้ของโปรแกรมดังนั้นจึงต้องใช้คอมไพเลอร์ในการส่งรหัสที่ห้ามไม่ให้ CPU ทำ ดังนั้น. มาตรฐานไม่ได้แยกความแตกต่างระหว่างสิ่งที่คอมไพลเลอร์ทำและสิ่งที่โค้ดสร้างของคอมไพเลอร์ทำให้ CPU ทำ

ข้อใดก่อให้เกิดคำถามสองข้อ: ทั้งสองข้อ "ถูก" หรือไม่? การนำไปใช้จริงทำอะไรได้บ้าง?


9
ส่วนใหญ่หมายความว่าคอมไพเลอร์ไม่ควรเก็บตัวแปรนั้นไว้ในรีจิสเตอร์ ทุกการมอบหมายและการอ่านในซอร์สโค้ดควรสอดคล้องกับการเข้าถึงหน่วยความจำในรหัสไบนารี
Basile Starynkevitch


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

เท่าที่ฉันทราบการระเหยใช้สำหรับตัวแปรที่ฮาร์ดแวร์สามารถเปลี่ยนแปลงได้ (มักใช้กับไมโครคอนโทรลเลอร์) หมายความว่าการอ่านตัวแปรไม่สามารถทำได้ในลำดับอื่นและไม่สามารถปรับให้เหมาะสมได้ นั่นคือ C แต่ควรเหมือนกันใน ++
เสา

1
@Mast ฉันยังไม่เห็นคอมไพเลอร์ที่ป้องกันการอ่านvolatileตัวแปรจากการปรับให้เหมาะสมโดยแคช CPU คอมไพเลอร์ทั้งหมดเหล่านี้ไม่เป็นไปตามมาตรฐานหรือมาตรฐานไม่ได้หมายถึงสิ่งที่คุณคิด (มาตรฐานไม่ได้แยกความแตกต่างระหว่างสิ่งที่คอมไพเลอร์ทำและสิ่งที่คอมไพเลอร์ทำให้ CPU ทำมันเป็นหน้าที่ของคอมไพเลอร์ในการส่งโค้ดที่เมื่อรันจะเป็นไปตามมาตรฐาน)
David Schwartz

คำตอบ:


58

แทนที่จะอธิบายสิ่งที่ไม่ให้ฉันอธิบายเมื่อคุณควรใช้volatilevolatile

  • เมื่ออยู่ในเครื่องจัดการสัญญาณ เนื่องจากการเขียนถึงvolatileตัวแปรเป็นสิ่งเดียวที่มาตรฐานอนุญาตให้คุณทำได้จากภายในตัวจัดการสัญญาณ เนื่องจาก C ++ 11 คุณสามารถใช้std::atomicเพื่อจุดประสงค์นั้นได้ แต่เฉพาะในกรณีที่อะตอมไม่มีการล็อค
  • เมื่อจัดการกับตามอินเทลsetjmp
  • เมื่อจัดการกับฮาร์ดแวร์โดยตรงและคุณต้องการให้แน่ใจว่าคอมไพเลอร์ไม่ได้ปรับการอ่านหรือเขียนของคุณให้เหมาะสมที่สุด

ตัวอย่างเช่น:

volatile int *foo = some_memory_mapped_device;
while (*foo)
    ; // wait until *foo turns false

หากไม่มีตัวvolatileระบุคอมไพลเลอร์จะได้รับอนุญาตให้เพิ่มประสิทธิภาพการวนซ้ำได้อย่างสมบูรณ์ ตัวvolatileระบุจะบอกคอมไพลเลอร์ว่าอาจไม่คิดว่า 2 การอ่านที่ตามมาจะส่งคืนค่าเดียวกัน

โปรดทราบว่าvolatileไม่มีส่วนเกี่ยวข้องกับเธรด ตัวอย่างข้างต้นใช้งานไม่ได้หากมีการเขียนเธรดที่แตกต่างกัน*fooเนื่องจากไม่มีการดำเนินการรับ

ในกรณีอื่น ๆ การใช้งานvolatileควรได้รับการพิจารณาว่าไม่พกพาและไม่ผ่านการตรวจสอบโค้ดอีกต่อไปยกเว้นเมื่อจัดการกับคอมไพเลอร์ pre-C ++ 11 และส่วนขยายของคอมไพเลอร์ (เช่น/volatile:msสวิตช์ของ msvc ซึ่งเปิดใช้งานโดยค่าเริ่มต้นภายใต้ X86 / I64)


5
มันเข้มงวดกว่า "อาจไม่ถือว่าการอ่าน 2 ครั้งต่อมาส่งคืนค่าเดียวกัน" แม้ว่าคุณจะอ่านเพียงครั้งเดียวและ / หรือโยนค่าออกไป แต่การอ่านจะต้องเสร็จสิ้น
philipxy

1
การใช้งานในตัวจัดการสัญญาณและsetjmpทั้งสองอย่างนี้รับประกันว่าทำให้ได้มาตรฐาน ในทางกลับกันเจตนาอย่างน้อยที่สุดในตอนเริ่มต้นคือเพื่อสนับสนุนหน่วยความจำที่แมป IO ซึ่งในโปรเซสเซอร์บางตัวอาจต้องใช้รั้วหรือเมมบาร์
James Kanze

@philipxy ยกเว้นไม่มีใครรู้ว่า "อ่าน" หมายถึงอะไร ตัวอย่างเช่นไม่มีใครเชื่อว่าต้องทำการอ่านจริงจากหน่วยความจำ - ไม่มีคอมไพเลอร์ที่ฉันรู้จักพยายามที่จะข้ามแคช CPU ในการvolatileเข้าถึง
David Schwartz

@JamesKanze: ไม่อย่างนั้น ตัวจัดการสัญญาณ Re มาตรฐานกล่าวว่าในระหว่างการจัดการสัญญาณมีเพียงวัตถุอะตอมที่ระเหยได้ std :: sig_atomic_t & lock-free อะตอมเท่านั้น แต่ยังกล่าวด้วยว่าการเข้าถึงวัตถุระเหยเป็นผลข้างเคียงที่สังเกตเห็นได้
philipxy

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

25

คีย์เวิร์ดที่ลบเลือน C ++ แนะนำรั้วหน่วยความจำหรือไม่

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

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

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

ยิ่งไปกว่านั้น: memory fences เป็นรายละเอียดการใช้งานของสถาปัตยกรรมโปรเซสเซอร์โดยเฉพาะ ใน C # ที่ผันผวนอย่างชัดเจนถูกออกแบบมาสำหรับ multithreading, สเปคไม่ได้บอกว่ารั้วครึ่งหนึ่งจะได้รับการแนะนำให้รู้จักเพราะโปรแกรมอาจจะมีการทำงานบนสถาปัตยกรรมที่ไม่ได้มีรั้วในสถานที่แรก แต่อีกครั้งข้อกำหนดดังกล่าวทำให้บางอย่าง (อ่อนแอมาก) รับประกันได้ว่าการเพิ่มประสิทธิภาพใดบ้างที่คอมไพเลอร์รันไทม์และ CPU จะหลีกเลี่ยงข้อ จำกัด บางประการ (ที่อ่อนแอมาก) เกี่ยวกับวิธีการจัดลำดับผลข้างเคียงบางอย่าง ในทางปฏิบัติการเพิ่มประสิทธิภาพเหล่านี้จะถูกตัดออกโดยใช้ครึ่งรั้ว แต่รายละเอียดการใช้งานอาจมีการเปลี่ยนแปลงในอนาคต

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


19
"ความผันผวนค่อนข้างไม่มีประโยชน์ใน C / C ++" ไม่ใช่เลย! คุณมีมุมมองที่ใช้โหมดเดสก์ท็อปเป็นศูนย์กลางของโลก ... แต่โค้ด C และ C ++ ส่วนใหญ่ทำงานบนระบบฝังตัวซึ่งความผันผวนเป็นสิ่งจำเป็นอย่างมากสำหรับ I / O ที่แมปหน่วยความจำ
Ben Voigt

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

3
@ BenVoigt: ไม่มีประโยชน์สำหรับการจัดการกับเธรดอย่างมีประสิทธิภาพเป็นความหมายที่ฉันตั้งใจไว้
Eric Lippert

4
@DavidSchwartz มาตรฐานไม่สามารถรับประกันได้ว่า IO ที่แมปหน่วยความจำทำงานอย่างไร แต่หน่วยความจำที่แมป IO จึงvolatileถูกนำมาใช้ในมาตรฐาน C ถึงกระนั้นเนื่องจากมาตรฐานไม่สามารถระบุสิ่งต่างๆเช่นสิ่งที่เกิดขึ้นจริงที่ "การเข้าถึง" ได้จึงกล่าวว่า "สิ่งที่ก่อให้เกิดการเข้าถึงออบเจ็กต์ที่มีคุณสมบัติระเหยได้คือการกำหนดการนำไปใช้งาน" การใช้งานมากเกินไปในปัจจุบันไม่ได้ให้คำจำกัดความที่เป็นประโยชน์ของการเข้าถึงซึ่ง IMHO ละเมิดเจตนารมณ์ของมาตรฐานแม้ว่าจะเป็นไปตามตัวอักษรก็ตาม
James Kanze

8
การแก้ไขนั้นเป็นการปรับปรุงที่ชัดเจน แต่คำอธิบายของคุณยังเน้นไปที่ "หน่วยความจำอาจมีการเปลี่ยนแปลงจากภายนอก" มากเกินไป volatileความหมายนั้นแข็งแกร่งกว่านั้นคอมไพเลอร์ต้องสร้างการเข้าถึงที่ร้องขอทุกครั้ง (1.9 / 8, 1.9 / 12) ไม่เพียง แต่รับประกันว่าจะตรวจพบการเปลี่ยนแปลงภายนอกในที่สุด (1.10 / 27) ในโลกของ I / O ที่แมปหน่วยความจำการอ่านหน่วยความจำสามารถมีตรรกะที่เกี่ยวข้องโดยพลการเช่นตัวรับคุณสมบัติ คุณจะไม่เพิ่มประสิทธิภาพการโทรไปยังผู้รับทรัพย์สินตามกฎที่คุณระบุไว้volatileและมาตรฐานไม่อนุญาต
Ben Voigt

13

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

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


5
อธิบายได้ดีขอบคุณ เพียงมาตรฐานกำหนดลำดับของการเข้าถึงที่จะระเหยเป็นที่สังเกตได้ตราบใดที่โปรแกรมไม่มีพฤติกรรมที่ไม่ได้กำหนด
Jonathan Wakely

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

ทำไมคุณถึงคิดว่าฉันมองข้ามสิ่งนั้นไป คุณคิดว่าส่วนใดของข้อโต้แย้งของฉันที่ทำให้ไม่ถูกต้อง? ฉันยอมรับ 100% ว่าคอมไพเลอร์มีสิทธิ์ที่จะละทิ้งการซิงโครไนซ์ใด ๆ
David Schwartz

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

@JamesKanze volatileมีส่วนเกี่ยวข้องมากมายกับเธรด: volatileเกี่ยวข้องกับหน่วยความจำที่สามารถเข้าถึงได้โดยที่คอมไพเลอร์ไม่ทราบว่าสามารถเข้าถึงได้และครอบคลุมการใช้ข้อมูลที่แชร์ในโลกแห่งความเป็นจริงจำนวนมากระหว่างเธรดบน CPU เฉพาะ
ซอกแซก

12

ประการแรกมาตรฐาน C ++ ไม่ได้รับประกันอุปสรรคของหน่วยความจำที่จำเป็นสำหรับการสั่งอ่าน / เขียนที่ไม่ใช่อะตอมอย่างถูกต้อง แนะนำให้ใช้ตัวแปรระเหยสำหรับการใช้งานกับ MMIO การจัดการสัญญาณ ฯลฯ ในการใช้งานส่วนใหญ่การระเหยไม่เป็นประโยชน์สำหรับการเชื่อมต่อแบบมัลติเธรดและโดยทั่วไปไม่แนะนำ

เกี่ยวกับการใช้การเข้าถึงแบบระเหยนี่คือทางเลือกของคอมไพเลอร์

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

เกี่ยวกับพฤติกรรมของiccฉันพบว่าแหล่งข้อมูลนี้บอกด้วยว่าการระเหยไม่รับประกันการสั่งการเข้าถึงหน่วยความจำ

คอมไพเลอร์Microsoft VS2013มีลักษณะการทำงานที่แตกต่างกัน เอกสารนี้อธิบายถึงวิธีการบังคับใช้ความหมายของรีลีส / รับความหมายของการระเหยและทำให้สามารถใช้อ็อบเจ็กต์ที่ระเหยได้ในการล็อก / รีลีสบนแอพพลิเคชั่นแบบมัลติเธรด

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

ดังนั้นคำตอบของฉันสำหรับ:

คีย์เวิร์ดที่ลบเลือน C ++ แนะนำรั้วหน่วยความจำหรือไม่

จะเป็น: ไม่รับประกันอาจไม่ใช่ แต่คอมไพเลอร์บางตัวอาจทำได้ คุณไม่ควรพึ่งพาความจริงที่ว่ามันเป็นเช่นนั้น


2
มันไม่ได้ป้องกันการปรับให้เหมาะสม แต่เพียงแค่ป้องกันไม่ให้คอมไพเลอร์แก้ไขโหลดและจัดเก็บเกินข้อ จำกัด บางประการ
Dietrich Epp

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

@DavidSchwartz มาตรฐานป้องกันไม่ให้มีการเรียงลำดับใหม่ (จากแหล่งใด ๆ ) ของการเข้าถึงผ่านvolatilelvalue เนื่องจากมันทิ้งคำจำกัดความของ "การเข้าถึง" ไปจนถึงการนำไปใช้งานอย่างไรก็ตามสิ่งนี้ไม่ได้ซื้ออะไรให้เรามากนักหากการใช้งานไม่ใส่ใจ
James Kanze

ฉันคิดว่าคอมไพเลอร์ MSC บางเวอร์ชันใช้ความหมายของรั้วvolatileแต่ไม่มีรั้วในโค้ดที่สร้างขึ้นจากคอมไพเลอร์ใน Visual Studios 2012
James Kanze

@JamesKanze ซึ่งโดยพื้นฐานแล้วหมายความว่าพฤติกรรมพกพาเพียงอย่างเดียวvolatileคือระบุโดยเฉพาะตามมาตรฐาน ( setjmpสัญญาณและอื่น ๆ )
David Schwartz

7

คอมไพเลอร์จะแทรกรั้วหน่วยความจำบนสถาปัตยกรรม Itanium เท่านั้นเท่าที่ฉันรู้

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


1
เรียงลำดับจาก. 'the compiler' (msvc) แทรกรั้วหน่วยความจำเมื่อสถาปัตยกรรมอื่นที่ไม่ใช่ ARM ถูกกำหนดเป้าหมายและใช้สวิตช์ / volatile: ms (ค่าเริ่มต้น) ดูmsdn.microsoft.com/en-us/library/12a04hfd.aspx คอมไพเลอร์อื่น ๆ ไม่ได้แทรกรั้วตัวแปรระเหยให้กับความรู้ของฉัน ควรหลีกเลี่ยงการใช้สารระเหยเว้นแต่จะเกี่ยวข้องโดยตรงกับฮาร์ดแวร์ตัวจัดการสัญญาณหรือคอมไพเลอร์ที่ไม่ใช่ c ++ 11
Stefan

@ Stefan No. volatileมีประโยชน์อย่างยิ่งสำหรับการใช้งานมากมายที่ไม่เคยเกี่ยวข้องกับฮาร์ดแวร์ เมื่อใดก็ตามที่คุณต้องการดำเนินการเพื่อสร้างรหัส CPU ที่ตาม C / C ++ volatileรหัสอย่างใกล้ชิดการใช้งาน
ซอกแซก

7

ขึ้นอยู่กับว่า "คอมไพเลอร์" เป็นคอมไพเลอร์ใด Visual C ++ ทำตั้งแต่ปี 2548 แต่ Standard ไม่ต้องการดังนั้นคอมไพเลอร์อื่น ๆ จึงไม่ทำ


VC ++ 2012 ไม่ได้ดูเหมือนจะใส่รั้ว: สร้างหลักที่มีตรงสองคำแนะนำ:int volatile i; int main() { return i; } mov eax, i; ret 0;
James Kanze

@JamesKanze: รุ่นไหนกันแน่? และคุณกำลังใช้ตัวเลือกการคอมไพล์ที่ไม่ใช่ค่าเริ่มต้นหรือไม่? ฉันอาศัยเอกสาร(เวอร์ชันแรกที่ได้รับผลกระทบ)และ(เวอร์ชันล่าสุด)ซึ่งกล่าวถึงการรับและเผยแพร่ความหมายอย่างแน่นอน
Ben Voigt

cl /helpพูดว่าเวอร์ชัน 18.00.21005.1 C:\Program Files (x86)\Microsoft Visual Studio 12.0\VCไดเรกทอรีมันอยู่ในมี ส่วนหัวบนหน้าต่างคำสั่งบอก VS 2013 ดังนั้นด้วยการไปถึงรุ่น ... /c /O2 /Faตัวเลือกเดียวที่ฉันใช้ในการวิจัย (หากไม่มีการ/O2ตั้งค่าเฟรมสแต็กในเครื่อง แต่ก็ยังไม่มีคำแนะนำเกี่ยวกับรั้ว)
James Kanze

@JamesKanze: ฉันสนใจสถาปัตยกรรมมากกว่าเช่น "Microsoft (R) C / C ++ Optimizing Compiler เวอร์ชัน 18.00.30723 สำหรับ x64" บางทีอาจไม่มีรั้วเพราะ x86 และ x64 มีการรับประกันการเชื่อมโยงกันของแคชที่ค่อนข้างแข็งแกร่ง เหรอ?
Ben Voigt

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

5

ส่วนใหญ่มาจากหน่วยความจำและขึ้นอยู่กับ pre-C ++ 11 โดยไม่มีเธรด แต่เมื่อมีส่วนร่วมในการอภิปรายเกี่ยวกับเธรดในคอมมิตฉันสามารถพูดได้ว่าคณะกรรมการไม่เคยมีเจตนาที่volatileจะใช้สำหรับการซิงโครไนซ์ระหว่างเธรด Microsoft เสนอ แต่ข้อเสนอนั้นไม่เกิดขึ้น

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

เนื้อหา (และเป็นข้อโต้แย้งที่ฉันยอมรับ) สิ่งนี้ละเมิดเจตนาของมาตรฐานเนื่องจากเว้นแต่ฮาร์ดแวร์จะรับรู้ที่อยู่เป็นหน่วยความจำที่แมป IO และยับยั้งการเรียงลำดับใหม่ใด ๆ ฯลฯ คุณจะไม่สามารถใช้ volatile สำหรับ IO ที่แมปหน่วยความจำได้ อย่างน้อยก็ในสถาปัตยกรรม Sparc หรือ Intel ไม่น้อยไปกว่านั้นไม่มีนักแสดงตลกคนใดที่ฉันเคยดู (Sun CC, g ++ และ MSC) แสดงคำแนะนำเกี่ยวกับรั้วหรือเมมบาร์ (เกี่ยวกับเวลาที่ Microsoft เสนอให้ขยายกฎสำหรับ volatileฉันคิดว่าคอมไพเลอร์บางส่วนของพวกเขาใช้ข้อเสนอของพวกเขาและส่งคำแนะนำรั้วสำหรับการเข้าถึงที่ไม่แน่นอนฉันไม่ได้ตรวจสอบว่าคอมไพเลอร์ล่าสุดทำอะไร แต่ก็ไม่แปลกใจเลยถ้ามันขึ้น ในตัวเลือกคอมไพเลอร์บางตัวเวอร์ชันที่ฉันตรวจสอบ - ฉันคิดว่ามันเป็น VS6.0 - ไม่ได้ปล่อยรั้ว)


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

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

1
@DavidSchwartz: สำหรับ I / O ที่แมปหน่วยความจำพิเศษ (หรือ mutex'd) กับอุปกรณ์ต่อพ่วงvolatileความหมายนั้นเพียงพออย่างสมบูรณ์ โดยทั่วไปอุปกรณ์ต่อพ่วงดังกล่าวจะรายงานพื้นที่หน่วยความจำว่าไม่สามารถแคชได้ซึ่งจะช่วยในการจัดลำดับใหม่ในระดับฮาร์ดแวร์
Ben Voigt

@BenVoigt ฉันสงสัยเกี่ยวกับเรื่องนี้: ความคิดที่ว่าโปรเซสเซอร์ "รู้" อย่างใดว่าที่อยู่ที่จัดการคือหน่วยความจำที่แมป IO เท่าที่ฉันรู้ Sparcs ไม่มีการสนับสนุนใด ๆ สำหรับสิ่งนี้ดังนั้นจึงยังคงทำให้ Sun CC และ g ++ บน Sparc ไม่สามารถใช้งานได้สำหรับ IO ที่แมปหน่วยความจำ (เมื่อฉันดูสิ่งนี้ฉันสนใจ Sparc เป็นหลัก)
James Kanze

@JamesKanze: จากการค้นหาเพียงเล็กน้อยที่ฉันทำดูเหมือนว่า Sparc จะมีช่วงที่อยู่เฉพาะสำหรับ "มุมมองทางเลือก" ของหน่วยความจำที่ไม่สามารถแคชได้ ตราบใดที่จุดเชื่อมต่อที่เปลี่ยนแปลงได้ของคุณอยู่ในASI_REAL_IOส่วนของพื้นที่ที่อยู่ฉันคิดว่าคุณน่าจะโอเค (Altera NIOS ใช้เทคนิคที่คล้ายกันโดยมีบิตสูงของแอดเดรสควบคุม MMU บายพาสฉันแน่ใจว่ามีคนอื่นด้วย)
Ben Voigt

5

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

คำอธิบายเล็กน้อยเกี่ยวกับอุปสรรคด้านความจำ CPU ทั่วไปมีการเข้าถึงหน่วยความจำหลายระดับ มีไปป์ไลน์หน่วยความจำแคชหลายระดับแล้วก็ RAM เป็นต้น

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

โดยปกติแคชจะเชื่อมโยงกันโดยอัตโนมัติระหว่างซีพียู หากต้องการให้แน่ใจว่าแคชซิงค์กับ RAM จำเป็นต้องล้างแคช มันแตกต่างจาก membar มาก


1
คุณกำลังบอกว่ามาตรฐาน C ++ บอกว่าvolatileปิดการใช้งานการเพิ่มประสิทธิภาพคอมไพเลอร์ใช่หรือไม่? นั่นไม่สมเหตุสมผลเลย การเพิ่มประสิทธิภาพใด ๆ ที่คอมไพเลอร์สามารถทำได้อย่างน้อยโดยหลักการแล้ว CPU ก็ทำได้ดีเท่า ๆ กัน ดังนั้นหากมาตรฐานกล่าวว่าปิดใช้งานการเพิ่มประสิทธิภาพคอมไพเลอร์นั่นหมายความว่าจะไม่มีพฤติกรรมที่ใครสามารถพึ่งพาในโค้ดพกพาได้ แต่เห็นได้ชัดว่าไม่เป็นความจริงเนื่องจากโค้ดแบบพกพาสามารถพึ่งพาพฤติกรรมของมันเกี่ยวกับsetjmpและสัญญาณ
David Schwartz

1
@DavidSchwartz ไม่มาตรฐานบอกว่าไม่มีสิ่งนั้น การปิดใช้งานการเพิ่มประสิทธิภาพเป็นเพียงสิ่งที่ทำกันทั่วไปในการนำมาตรฐานไปใช้ มาตรฐานกำหนดให้พฤติกรรมที่สังเกตได้เกิดขึ้นในลำดับเดียวกับที่เครื่องจักรนามธรรมต้องการ เมื่อเครื่องจักรนามธรรมไม่ต้องการคำสั่งใด ๆ การใช้งานจะใช้คำสั่งใด ๆ หรือไม่มีคำสั่งใด ๆ เลย การเข้าถึงตัวแปรระเหยในเธรดที่แตกต่างกันจะไม่ได้รับคำสั่งเว้นแต่จะใช้การซิงโครไนซ์เพิ่มเติม
. 'สรรพนาม' ม.

1
@DavidSchwartz ฉันขอโทษสำหรับถ้อยคำที่ไม่ชัดเจน มาตรฐานไม่กำหนดให้ปิดใช้งานการเพิ่มประสิทธิภาพ ไม่มีแนวคิดเรื่องการเพิ่มประสิทธิภาพเลย แต่เป็นการระบุพฤติกรรมที่ในทางปฏิบัติต้องการให้คอมไพเลอร์ปิดการใช้งานการเพิ่มประสิทธิภาพบางอย่างในลักษณะที่ลำดับการอ่านและเขียนที่สังเกตได้นั้นเป็นไปตามมาตรฐาน
. 'สรรพนาม' ม.

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

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

4

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

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


2

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

ฉันทำสิ่งนี้สำหรับ RAM และ IO ที่แมปหน่วยความจำ

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

ในขณะที่เรารวบรวมสิ่งต่างๆไว้ใน "ระบบ" ที่รันโค้ดอ็อบเจ็กต์การเดิมพันเกือบทั้งหมดจะปิดลงอย่างน้อยนั่นคือวิธีที่ฉันอ่านการสนทนานี้ คอมไพเลอร์จะครอบคลุมฐานทั้งหมดได้อย่างไร?


0

ฉันคิดว่าความสับสนเกี่ยวกับความผันผวนและการเรียงลำดับคำสั่งใหม่เกิดจากแนวคิด 2 ประการของการจัดลำดับซีพียูใหม่:

  1. การดำเนินการนอกคำสั่ง
  2. ลำดับของการอ่าน / เขียนหน่วยความจำตามที่เห็นโดยซีพียูอื่น ๆ (เรียงลำดับใหม่ในแง่ที่ CPU แต่ละตัวอาจเห็นลำดับที่แตกต่างกัน)

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

การดำเนินการนอกคำสั่ง

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

ลำดับของการอ่าน / เขียนหน่วยความจำตามที่ซีพียูอื่นเห็น

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

แหล่งที่มา:


0

ในขณะที่ฉันทำงานผ่านวิดีโอสอนการดาวน์โหลดออนไลน์สำหรับการพัฒนากราฟิก 3D และ Game Engine ที่ทำงานร่วมกับ OpenGL ที่ทันสมัย เราใช้volatileในชั้นเรียนของเรา สามารถดูเว็บไซต์บทแนะนำได้ที่นี่และวิดีโอที่ใช้volatileคีย์เวิร์ดพบได้ในShader Engineวิดีโอชุด 98 งานเหล่านี้ไม่ใช่ของฉันเอง แต่ได้รับการรับรองMarek A. Krzeminski, MAScและนี่เป็นข้อความที่ตัดตอนมาจากหน้าดาวน์โหลดวิดีโอ

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

และหากคุณสมัครรับข้อมูลเว็บไซต์ของเขาและสามารถเข้าถึงวิดีโอของเขาในวิดีโอนี้ได้เขาอ้างอิงบทความนี้เกี่ยวกับการใช้งานVolatileกับmultithreadingการเขียนโปรแกรม

นี่คือบทความจากลิงค์ด้านบน: http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766

ระเหยได้: เพื่อนที่ดีที่สุดของโปรแกรมเมอร์มัลติเธรด

โดย Andrei Alexandrescu 1 กุมภาพันธ์ 2544

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

ฉันไม่อยากทำให้คุณเสียอารมณ์ แต่คอลัมน์นี้กล่าวถึงหัวข้อที่น่ากลัวของการเขียนโปรแกรมแบบมัลติเธรด ถ้า - ตามที่ Generic ภาคก่อนกล่าว - การเขียนโปรแกรมที่ปลอดภัยเป็นพิเศษนั้นยากการเล่นของเด็ก ๆ เมื่อเทียบกับการเขียนโปรแกรมแบบมัลติเธรด

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

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

คำหลักเพียงเล็กน้อย

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

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

พิจารณารหัสต่อไปนี้:

class Gadget {
public:
    void Wait() {
        while (!flag_) {
            Sleep(1000); // sleeps for 1000 milliseconds
        }
    }
    void Wakeup() {
        flag_ = true;
    }
    ...
private:
    bool flag_;
};

จุดประสงค์ของ Gadget :: รอด้านบนคือการตรวจสอบตัวแปร flag_ member ทุกวินาทีและส่งคืนเมื่อตัวแปรนั้นถูกตั้งค่าเป็น true โดยเธรดอื่น อย่างน้อยนั่นคือสิ่งที่โปรแกรมเมอร์ตั้งใจไว้ แต่อนิจจาการรอไม่ถูกต้อง

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

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

class Gadget {
public:
    ... as above ...
private:
    volatile bool flag_;
};

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

การใช้สารระเหยกับประเภทที่ผู้ใช้กำหนด

คุณสามารถระเหย - คุณสมบัติไม่เพียง แต่ประเภทดั้งเดิมเท่านั้น แต่ยังรวมถึงประเภทที่ผู้ใช้กำหนดด้วย ในกรณีนั้น volatile ปรับเปลี่ยนประเภทในลักษณะที่คล้ายกับ const (คุณสามารถใช้ const และ volatile กับประเภทเดียวกันได้พร้อมกัน)

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

ลองแสดงให้เห็นว่าการระเหยทำงานอย่างไรกับประเภทที่ผู้ใช้กำหนดเองในตัวอย่าง

class Gadget {
public:
    void Foo() volatile;
    void Bar();
    ...
private:
    String name_;
    int state_;
};
...
Gadget regularGadget;
volatile Gadget volatileGadget;

หากคุณคิดว่าวัตถุระเหยไม่เป็นประโยชน์กับวัตถุให้เตรียมรับมือกับความประหลาดใจ

volatileGadget.Foo(); // ok, volatile fun called for
                  // volatile object
regularGadget.Foo();  // ok, volatile fun called for
                  // non-volatile object
volatileGadget.Bar(); // error! Non-volatile function called for
                  // volatile object!

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

Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar(); // ok

คลาสที่มีคุณสมบัติระเหยได้ให้การเข้าถึงเฉพาะส่วนย่อยของอินเทอร์เฟซซึ่งเป็นส่วนย่อยที่อยู่ภายใต้การควบคุมของคลาสที่ใช้งาน ผู้ใช้สามารถเข้าถึงอินเทอร์เฟซประเภทนั้นได้โดยใช้ const_cast เท่านั้น นอกจากนี้เช่นเดียวกับ constness ความผันผวนจะแพร่กระจายจากคลาสไปยังสมาชิก (ตัวอย่างเช่น volatileGadget.name_ และ volatileGadget.state_ เป็นตัวแปรที่ระเหยได้)

ส่วนที่ผันผวนส่วนที่สำคัญและเงื่อนไขการแข่งขัน

อุปกรณ์ซิงโครไนซ์ที่ง่ายที่สุดและใช้บ่อยที่สุดในโปรแกรมมัลติเธรดคือ mutex mutex เปิดโปงการได้มาและปล่อยดั้งเดิม เมื่อคุณเรียก Acquire ในบางเธรดเธรดอื่น ๆ ที่เรียก Acquire จะบล็อก ต่อมาเมื่อเธรดนั้นเรียกรีลีสเธรดหนึ่งเธรดที่ถูกบล็อกอย่างแม่นยำในการรับการโทรจะถูกปลด กล่าวอีกนัยหนึ่งสำหรับ mutex ที่กำหนดเธรดเดียวเท่านั้นที่สามารถรับเวลาประมวลผลระหว่างการโทรไปยัง Acquire และการเรียกร้องให้ Release รหัสการดำเนินการระหว่างการเรียกร้องให้ได้มาและการเรียกร้องให้ปล่อยเรียกว่าส่วนสำคัญ (คำศัพท์ของ Windows ค่อนข้างสับสนเพราะมันเรียก mutex เองว่าเป็นส่วนที่สำคัญในขณะที่ "mutex" เป็น mutex ระหว่างกระบวนการมันจะดีถ้าพวกเขาถูกเรียกว่า thread mutex และ process mutex)

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

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

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

ในระยะสั้นข้อมูลที่ใช้ร่วมกันระหว่างเธรดมีความผันผวนตามแนวคิดภายนอกส่วนที่สำคัญและไม่ระเหยภายในส่วนที่สำคัญ

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

ล็อค

เราต้องการเครื่องมือที่รวบรวมการได้มาของ mutex และ const_cast มาพัฒนาเทมเพลตคลาส LockingPtr ที่คุณเริ่มต้นด้วย obj วัตถุระเหยและ mutex mtx ตลอดอายุการใช้งาน LockingPtr จะเก็บ mtx ที่ได้มา นอกจากนี้ LockingPtr ยังมีการเข้าถึง obj ที่ลบเลือน การเข้าถึงมีให้ในรูปแบบตัวชี้อัจฉริยะผ่านตัวดำเนินการ -> และตัวดำเนินการ * const_cast จะดำเนินการภายใน LockingPtr การแคสต์นั้นถูกต้องตามความหมายเนื่องจาก LockingPtr เก็บ mutex ที่ได้มาตลอดอายุการใช้งาน

ก่อนอื่นให้กำหนดโครงกระดูกของคลาส Mutex ซึ่ง LockingPtr จะทำงาน:

class Mutex {
public:
    void Acquire();
    void Release();
    ...    
};

ในการใช้ LockingPtr คุณสามารถใช้ Mutex โดยใช้โครงสร้างข้อมูลดั้งเดิมและฟังก์ชันดั้งเดิมของระบบปฏิบัติการของคุณ

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

คำจำกัดความของ LockingPtr นั้นง่ายมาก LockingPtr ใช้ตัวชี้อัจฉริยะที่ไม่ซับซ้อน โดยมุ่งเน้นไปที่การรวบรวม const_cast และส่วนที่สำคัญเท่านั้น

template <typename T>
class LockingPtr {
public:
    // Constructors/destructors
    LockingPtr(volatile T& obj, Mutex& mtx)
      : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) {    
        mtx.Lock();    
    }
    ~LockingPtr() {    
        pMtx_->Unlock();    
    }
    // Pointer behavior
    T& operator*() {    
        return *pObj_;    
    }
    T* operator->() {   
        return pObj_;   
    }
private:
    T* pObj_;
    Mutex* pMtx_;
    LockingPtr(const LockingPtr&);
    LockingPtr& operator=(const LockingPtr&);
};

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

สมมติว่าคุณมีสองเธรดที่แชร์วัตถุเวกเตอร์:

class SyncBuf {
public:
    void Thread1();
    void Thread2();
private:
    typedef vector<char> BufT;
    volatile BufT buffer_;
    Mutex mtx_; // controls access to buffer_
};

ภายในฟังก์ชันเธรดคุณเพียงแค่ใช้ LockingPtr เพื่อควบคุมการเข้าถึงตัวแปร buffer_ member:

void SyncBuf::Thread1() {
    LockingPtr<BufT> lpBuf(buffer_, mtx_);
    BufT::iterator i = lpBuf->begin();
    for (; i != lpBuf->end(); ++i) {
        ... use *i ...
    }
}

โค้ดเขียนและเข้าใจง่ายมาก - เมื่อใดก็ตามที่คุณต้องการใช้ buffer_ คุณต้องสร้าง LockingPtr ที่ชี้ไปที่มัน เมื่อคุณทำเช่นนั้นคุณจะสามารถเข้าถึงอินเทอร์เฟซทั้งหมดของเวกเตอร์ได้

ส่วนที่ดีคือถ้าคุณทำผิดคอมไพเลอร์จะชี้ให้เห็น:

void SyncBuf::Thread2() {
    // Error! Cannot access 'begin' for a volatile object
    BufT::iterator i = buffer_.begin();
    // Error! Cannot access 'end' for a volatile object
    for ( ; i != lpBuf->end(); ++i ) {
        ... use *i ...
    }
}

คุณไม่สามารถเข้าถึงฟังก์ชันของ buffer_ ได้จนกว่าคุณจะใช้ const_cast หรือใช้ LockingPtr ความแตกต่างคือ LockingPtr เสนอวิธีการสั่งใช้ const_cast กับตัวแปรที่ผันผวน

LockingPtr แสดงออกได้อย่างน่าทึ่ง หากคุณต้องการเรียกใช้เพียงฟังก์ชันเดียวคุณสามารถสร้างอ็อบเจ็กต์ LockingPtr ชั่วคราวที่ไม่มีชื่อและใช้งานได้โดยตรง:

unsigned int SyncBuf::Size() {
return LockingPtr<BufT>(buffer_, mtx_)->size();
}

กลับไปที่ประเภทดั้งเดิม

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

ลองพิจารณาตัวอย่างที่หลายเธรดแชร์ตัวแปรประเภท int

class Counter {
public:
    ...
    void Increment() { ++ctr_; }
    void Decrement() { —ctr_; }
private:
    int ctr_;
};

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

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

การดำเนินการสามขั้นตอนนี้เรียกว่า RMW (Read-Modify-Write) ในระหว่างการปรับเปลี่ยนส่วนของการดำเนินการ RMW โปรเซสเซอร์ส่วนใหญ่จะปล่อยบัสหน่วยความจำเพื่อให้โปรเซสเซอร์อื่นเข้าถึงหน่วยความจำได้

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

เพื่อหลีกเลี่ยงสิ่งนั้นคุณสามารถพึ่งพา LockingPtr ได้อีกครั้ง:

class Counter {
public:
    ...
    void Increment() { ++*LockingPtr<int>(ctr_, mtx_); }
    void Decrement() { —*LockingPtr<int>(ctr_, mtx_); }
private:
    volatile int ctr_;
    Mutex mtx_;
};

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

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

ฟังก์ชั่นสมาชิกที่ผันผวน

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

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

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

class Widget {
public:
    void Operation() volatile;
    void Operation();
    ...
private:
    Mutex mtx_;
};

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

เมื่อใช้ฟังก์ชันสมาชิกที่ระเหยได้การดำเนินการแรกมักจะล็อคสิ่งนี้ด้วย LockingPtr จากนั้นงานจะทำโดยใช้พี่น้องที่ไม่ลบเลือน:

void Widget::Operation() volatile {
    LockingPtr<Widget> lpThis(*this, mtx_);
    lpThis->Operation(); // invokes the non-volatile function
}

สรุป

เมื่อเขียนโปรแกรมหลายเธรดคุณสามารถใช้ volatile เพื่อประโยชน์ของคุณ คุณต้องปฏิบัติตามกฎต่อไปนี้:

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

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

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

กิตติกรรมประกาศ

ขอบคุณมากสำหรับ James Kanze และ Sorin Jianu ที่ช่วยให้มีความคิดที่ลึกซึ้ง


Andrei Alexandrescu เป็นผู้จัดการฝ่ายพัฒนาที่ RealNetworks Inc. (www.realnetworks.com) ซึ่งตั้งอยู่ใน Seattle, WA และเป็นผู้เขียนหนังสือ Modern C ++ Design ที่ได้รับการยกย่อง เขาอาจได้รับการติดต่อที่ www.moderncppdesign.com Andrei ยังเป็นหนึ่งในผู้สอนที่โดดเด่นของ The C ++ Seminar (www.gotw.ca/cpp_seminar)

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


0

คำหลักvolatileเป็นหลักวิธีการที่อ่านและเขียนวัตถุควรจะดำเนินการตรงตามที่เขียนโดยโปรแกรมและไม่เหมาะสมในทางใดทางหนึ่ง รหัสไบนารีควรเป็นไปตามรหัส C หรือ C ++: โหลดที่อ่านสิ่งนี้เก็บที่มีการเขียน

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

volatile int i;
i = 1;
int j = i; 
if (j == 1) // not assumed to be true

volatileอาจจะเป็นเครื่องมือที่สำคัญที่สุดใน "C เป็นภาษาระดับสูงประกอบ" กล่องเครื่องมือ

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

Atomic primitives ให้มุมมองระดับที่สูงขึ้นของออบเจ็กต์สำหรับมัลติเธรดซึ่งทำให้ง่ายต่อการให้เหตุผลเกี่ยวกับโค้ด โปรแกรมเมอร์เกือบทั้งหมดควรใช้ atomic primitives หรือ primitives ที่ให้การยกเว้นร่วมกันเช่น mutexes, read-write-locks, semaphores หรือ block primitives อื่น ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.