อะไรรับประกันกับ C ++ std :: atomic ในระดับโปรแกรมเมอร์?


9

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

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

[T1: thread1 T2: thread2 V1: ตัวแปรอะตอมมิกที่ใช้ร่วมกัน]

ฉันเข้าใจว่า std :: atomic รับประกันว่า

(1) ไม่มีการแย่งข้อมูลในตัวแปร (ต้องขอบคุณการเข้าถึงสายแคชเท่านั้น)

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

(3) หลังจากการเขียนอะตอมมิก (V1) บน T1 อะตอมมิก RMW (V1) บน T2 จะสอดคล้องกัน (บรรทัดแคชจะได้รับการอัพเดตด้วยค่าที่เขียนบน T1)

แต่เมื่อไพรเมอร์เชื่อมโยงกันถึงแคช

ความหมายของสิ่งเหล่านี้คือโดยค่าเริ่มต้นการโหลดสามารถดึงข้อมูลเก่า (ถ้าคำขอการตรวจสอบความถูกต้องที่สอดคล้องกันกำลังนั่งอยู่ในคิวการตรวจสอบความถูกต้อง)

ดังนั้นถูกต้องหรือไม่

(4) std::atomicไม่รับประกันว่า T2 จะไม่อ่านค่า 'เก่า' ในการอ่านอะตอมมิก (V) หลังจากการเขียนอะตอม (V) บน T1

คำถามถ้า (4) ถูกต้อง: ถ้าการเขียนอะตอมของ T1 ทำให้บรรทัดแคชไม่ว่าจะล่าช้าหรือไม่ทำไม T2 จึงรอให้การทำให้มีผลใช้งานไม่ได้เมื่อการดำเนินการ RMW ของอะตอม แต่ไม่ได้อ่านในอะตอม?

คำถามที่ (4) ไม่ถูกต้อง: เมื่อใดที่เธรดสามารถอ่านค่า 'เก่า' และ "สามารถมองเห็นได้" ในการดำเนินการ

ฉันขอขอบคุณคำตอบของคุณมาก

อัปเดต 1

ดังนั้นดูเหมือนว่าฉันผิดในวันที่ (3) ลองจินตนาการถึง interleave ต่อไปนี้สำหรับ V1 = 0 เริ่มต้น:

T1: W(1)
T2:      R(0) M(++) W(1)

แม้ว่า RMW ของ T2 จะรับประกันว่าจะเกิดขึ้นทั้งหมดหลังจาก W (1) ในกรณีนี้ แต่ก็ยังสามารถอ่านค่า 'ค้าง' (ฉันผิด) อะตอมมิกไม่รับประกันความสอดคล้องกันของแคชแบบเต็มเท่านั้น

อัปเดต 2

(5) ลองจินตนาการถึงตัวอย่างนี้ (x = y = 0 และเป็นอะตอม):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

ตามที่เราได้พูดคุยการเห็นข้อความ "msg" ที่ปรากฏบนหน้าจอจะไม่ให้ข้อมูลกับเราเกินกว่า T2 ที่ถูกเรียกใช้หลังจาก T1 ดังนั้นการประหารชีวิตอย่างใดอย่างหนึ่งต่อไปนี้อาจเกิดขึ้น:

  • T1 <T3 <T2
  • T1 <T2 <T3 (โดยที่ T3 เห็น x = 1 แต่ไม่ใช่ y = 1)

นั่นถูกต้องใช่ไหม?

(6) หากเธรดสามารถอ่านค่า 'เก่า' ได้จะเกิดอะไรขึ้นถ้าเราใช้สถานการณ์ "เผยแพร่" ทั่วไป แต่แทนที่จะส่งสัญญาณว่าข้อมูลบางอย่างพร้อมเราทำตรงกันข้าม (ลบข้อมูล)?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

โดยที่ T2 จะยังคงใช้ ptr ที่ถูกลบจนกว่าจะเห็นว่า is_enabled เป็นเท็จ

(7) นอกจากนี้ความจริงที่ว่าเธรดอาจอ่านค่า 'ค้าง' หมายความว่าmutexไม่สามารถใช้งานได้ด้วยสิทธิ์อะตอมมิกแบบล็อคเดียวใช่หรือไม่ มันจะต้องมีกลไกการซิงค์ระหว่างหัวข้อ มันจะต้องมีอะตอมที่ล็อคได้หรือไม่?

คำตอบ:


3
  1. ใช่ไม่มีการแข่งขันข้อมูล
  2. ใช่ด้วยmemory_orderค่าที่เหมาะสมคุณสามารถรับประกันความสอดคล้องตามลำดับ
  3. Atomic read-modified-write จะเกิดขึ้นทั้งหมดก่อนหรือหลังจากการเขียนอะตอมไปยังตัวแปรเดียวกันเสมอ
  4. ใช่ T2 สามารถอ่านค่าเก่าจากตัวแปรหลังจากการเขียนอะตอมมิกบน T1

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

เธรดสามารถอ่านค่าเก่า ๆ ได้เสมอยกเว้นเมื่อเกิดขึ้นก่อนที่จะรับประกันการสั่งซื้อที่เกี่ยวข้อง

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

อัปเดตตัวอย่างเช่น

หาก T1 เขียนx=1และ T2 ทำx++โดยที่x0 เริ่มต้นตัวเลือกจากมุมมองของหน่วยเก็บของxคือ:

  1. การเขียนของ T1 เป็นลำดับแรกดังนั้นการเขียน T1 x=1จึงอ่าน T2 x==1เพิ่มขึ้นเป็น 2 และเขียนกลับx=2เป็นการดำเนินการปรมาณูเดี่ยว

  2. การเขียนของ T1 นั้นเป็นที่สอง T2 อ่านx==0, การเพิ่มขึ้นไปที่ 1 และเขียนกลับx=1เป็นงานเดียวแล้ว T1 x=1เขียน

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

ดังนั้น T1 สามารถออกx=1แล้วดำเนินการกับสิ่งอื่น ๆ แม้ว่า T2 จะยังอ่านx==0(และเขียนx=1)

หากมีจุดอื่น ๆ ของการซิงโครไนซ์ก็จะกลายเป็นที่ชัดเจนซึ่งด้ายแก้ไขxครั้งแรกเพราะจุดประสานเหล่านั้นจะบังคับให้สั่งซื้อ

สิ่งนี้ชัดเจนที่สุดหากคุณมีเงื่อนไขเกี่ยวกับค่าที่อ่านจากการดำเนินการ RMW

อัปเดต 2

  1. หากคุณใช้memory_order_seq_cst(ค่าเริ่มต้น) สำหรับการปฏิบัติการปรมาณูทั้งหมดคุณไม่จำเป็นต้องกังวลเกี่ยวกับสิ่งนี้ จากมุมมองของโปรแกรมถ้าคุณเห็น "msg" จากนั้น T1 ก็วิ่งแล้วก็ T3 แล้วก็ T2

หากคุณใช้ลำดับหน่วยความจำอื่น ๆ (โดยเฉพาะmemory_order_relaxed) คุณอาจเห็นสถานการณ์อื่น ๆ ในรหัสของคุณ

  1. ในกรณีนี้คุณมีข้อบกพร่อง สมมติว่าis_enabledธงเป็นจริงเมื่อ T2 เข้าสู่whileวงของมันดังนั้นจึงตัดสินใจที่จะเรียกใช้ร่างกาย ตอนนี้ T1 จะลบข้อมูลและ T2 จากนั้นทำการตั้งค่าพอยน์เตอร์ซึ่งเป็นพอยเตอร์ห้อยต่องแต่งและพฤติกรรมที่ไม่ได้กำหนดจะตามมา อะตอมมิกส์ไม่ได้ช่วยหรือขัดขวางในทางใดทางหนึ่งนอกจากป้องกันการแข่งขันของข้อมูลบนธง

  2. คุณสามารถนำ mutex ไปใช้กับตัวแปรอะตอมเดี่ยว


ขอบคุณมาก @Anthony Wiliams สำหรับคำตอบด่วนของคุณ ฉันได้อัปเดตคำถามของฉันด้วยตัวอย่างของ RMW ที่อ่านค่า 'เก่า' ดูตัวอย่างนี้คุณหมายถึงอะไรโดยการเรียงลำดับสัมพัทธ์และ W (1) ของ T2 จะปรากฏให้เห็นก่อนการเขียนใด ๆ หมายความว่าเมื่อ T2 ได้เห็นการเปลี่ยนแปลงของ T1 มันจะไม่อ่าน W ของ T2 (1) อีกต่อไปหรือไม่
Albert Caldas

ดังนั้นหาก "เธรดสามารถอ่านค่าเก่าได้" หมายความว่าไม่มีการรับประกันความสอดคล้องกันของแคช (อย่างน้อยในระดับ c ++ โปรแกรมเมอร์) คุณช่วยดู update2 ของฉันหน่อยได้ไหม?
Albert Caldas

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

1

เกี่ยวกับ (3) - ขึ้นอยู่กับลำดับหน่วยความจำที่ใช้ หากทั้งสองร้านค้าและการใช้งาน RMW std::memory_order_seq_cstจะมีการสั่งซื้อทั้งสองอย่างในลักษณะใดวิธีหนึ่งนั่นคือร้านค้าจะเกิดขึ้นก่อนที่จะมี RMW หรืออีกทางหนึ่ง หากร้านค้าได้รับคำสั่งก่อน RMW จะรับประกันได้ว่าการดำเนินการ RMW "เห็น" ค่าที่เก็บไว้ หากร้านค้าได้รับคำสั่งหลังจาก RMW มันจะเขียนทับค่าที่เขียนโดยการดำเนินการ RMW

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

ในกรณีที่คุณต้องการที่จะอ่านบทความอื่น ๆ ที่ฉันจะนำคุณไปยังหน่วยความจำรุ่นสำหรับ C / C ++ โปรแกรมเมอร์


ขอบคุณสำหรับบทความฉันยังไม่ได้อ่าน แม้ว่ามันจะค่อนข้างเก่า แต่ก็มีประโยชน์ที่จะนำความคิดของฉันมารวมกัน
Albert Caldas

1
ดีใจที่ได้ยินเช่นนี้ - บทความนี้เป็นบทที่ขยายและแก้ไขเล็กน้อยจากวิทยานิพนธ์ปริญญาโทของฉัน :-) มันมุ่งเน้นไปที่รูปแบบหน่วยความจำตามที่แนะนำ C ++ 11; ฉันอาจอัปเดตเพื่อให้สอดคล้องกับการเปลี่ยนแปลง (เล็ก) ที่แนะนำใน C ++ 14/17 โปรดแจ้งให้เราทราบหากคุณมีความคิดเห็นหรือข้อเสนอแนะสำหรับการปรับปรุง!
mpoeter
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.