ฉันต้องการเขียนโค้ดแบบพกพา (Intel, ARM, PowerPC ... ) ซึ่งแก้ปัญหาคลาสสิก:
Initially: X=Y=0
Thread A:
X=1
if(!Y){ do something }
Thread B:
Y=1
if(!X){ do something }
ซึ่งเป้าหมายคือการหลีกเลี่ยงสถานการณ์ที่หัวข้อทั้งสองจะทำ something(ไม่เป็นไรหากไม่มีสิ่งใดทำงานนี่ไม่ใช่กลไกที่ทำงานเหมือนครั้งเดียว) โปรดแก้ไขให้ฉันถ้าคุณเห็นข้อบกพร่องบางอย่างในการให้เหตุผลด้านล่าง
ฉันรู้ว่าฉันสามารถบรรลุเป้าหมายด้วยmemory_order_seq_cstอะตอมstoreและloads ดังต่อไปนี้:
std::atomic<int> x{0},y{0};
void thread_a(){
x.store(1);
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!x.load()) bar();
}
ซึ่งบรรลุเป้าหมายเพราะจะต้องมีคำสั่งซื้อทั้งหมดเพียงคำสั่งเดียวใน
{x.store(1), y.store(1), y.load(), x.load()}เหตุการณ์ซึ่งจะต้องเห็นด้วยกับ "ขอบ" ของคำสั่งโปรแกรม:
x.store(1)"ในถึงก่อน"y.load()y.store(1)"ในถึงก่อน"x.load()
และถ้าfoo()ถูกเรียกเราก็จะมีขอบเพิ่มเติม:
y.load()"อ่านค่าก่อน"y.store(1)
และถ้าbar()ถูกเรียกเราก็จะมีขอบเพิ่มเติม:
x.load()"อ่านค่าก่อน"x.store(1)
และขอบทั้งหมดเหล่านี้รวมกันจะก่อให้เกิดวงจร:
x.store(1)"ในถึงคือก่อน" y.load()"อ่านค่าก่อน" y.store(1)"ในถึงคือก่อน" x.load()"อ่านค่าก่อน"x.store(true)
ซึ่งเป็นการละเมิดความจริงที่ว่าคำสั่งซื้อไม่มีรอบ
ฉันจงใจใช้คำที่ไม่ได้มาตรฐาน "ใน TO คือก่อน" และ "อ่านค่ามาก่อน" ซึ่งตรงข้ามกับคำมาตรฐานเช่นhappens-beforeเพราะฉันต้องการที่จะขอความคิดเห็นเกี่ยวกับความถูกต้องของสมมติฐานของฉันที่ขอบเหล่านี้บ่งบอกถึงhappens-beforeความสัมพันธ์จริง ๆกราฟและวงจรในกราฟรวมดังกล่าวเป็นสิ่งต้องห้าม ฉันไม่แน่ใจเกี่ยวกับเรื่องนี้ สิ่งที่ฉันรู้คือรหัสนี้สร้างอุปสรรคที่ถูกต้องใน Intel gcc & clang และ ARM gcc
ตอนนี้ปัญหาที่แท้จริงของฉันนั้นซับซ้อนกว่าเล็กน้อยเนื่องจากฉันไม่สามารถควบคุม "X" ได้ - มันซ่อนอยู่หลังมาโครเทมเพลต ฯลฯ และอาจจะอ่อนแอกว่า seq_cst
ฉันไม่รู้ด้วยซ้ำว่า "X" เป็นตัวแปรเดียวหรือมีแนวคิดอื่น (เช่นเซมาฟอร์น้ำหนักเบาหรือ mutex) ทั้งหมดที่ผมรู้ก็คือว่าผมมีสองแมโครset()และcheck()ดังกล่าวว่าcheck()ผลตอบแทนtrue"หลัง" set()หัวข้ออื่นได้เรียกว่า ( เป็นที่รู้จักกันว่าsetและcheckมีความปลอดภัยต่อเธรดและไม่สามารถสร้าง UB ของ data-race ได้)
ดังนั้นคอนเซ็ปต์set()จึงค่อนข้างคล้ายกับ "X = 1" และcheck()ก็เหมือนกับ "X" แต่ฉันไม่มีสิทธิ์เข้าถึงอะตอมมิกส์ที่เกี่ยวข้องโดยตรงถ้ามี
void thread_a(){
set();
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!check()) bar();
}
ผมกังวลว่าset()อาจจะมีการดำเนินการภายในเป็นx.store(1,std::memory_order_release)และ / หรืออาจจะcheck() x.load(std::memory_order_acquire)หรือสมมุติstd::mutexว่าเธรดหนึ่งถูกปลดล็อกและอีกอันหนึ่งคือtry_lockไอเอ็นจี ในมาตรฐาน ISO std::mutexรับประกันว่าจะได้รับและอนุมัติการสั่งซื้อเท่านั้นไม่ใช่ seq_cst
หากเป็นกรณีนี้check()ถ้าร่างกายสามารถ "จัดลำดับใหม่" มาก่อนy.store(true)( ดูคำตอบของ Alexที่พวกเขาแสดงให้เห็นว่าสิ่งนี้เกิดขึ้นใน PowerPC )
สิ่งนี้จะไม่ดีจริง ๆ เนื่องจากตอนนี้ลำดับเหตุการณ์เป็นไปได้:
thread_b()ก่อนโหลดค่าเก่าของx(0)thread_a()ดำเนินการทุกอย่างรวมถึงfoo()thread_b()ดำเนินการทุกอย่างรวมถึงbar()
ดังนั้นทั้งคู่foo()และbar()ถูกเรียกซึ่งฉันต้องหลีกเลี่ยง ตัวเลือกของฉันในการป้องกันสิ่งนั้นคืออะไร?
ตัวเลือก A
ลองบังคับอุปสรรคโหลดร้านค้า ในทางปฏิบัตินี้สามารถทำได้โดยstd::atomic_thread_fence(std::memory_order_seq_cst);- ตามที่อธิบายโดยAlex ในคำตอบที่ต่างออกไปคอมไพเลอร์ที่ผ่านการทดสอบทั้งหมดที่ปล่อยออกมาเต็มรั้ว:
- x86_64: MFENCE
- PowerPC: hwsync
- Itanuim: mf
- ARMv7 / ARMv8: dmb ish
- MIPS64: ซิงค์
ปัญหาเกี่ยวกับวิธีการนี้คือฉันไม่สามารถหาการรับประกันใด ๆ ในกฎ C ++ ที่std::atomic_thread_fence(std::memory_order_seq_cst)ต้องแปลเป็นกำแพงหน่วยความจำเต็ม ที่จริงแล้วแนวคิดของatomic_thread_fences ใน C ++ ดูเหมือนว่าจะอยู่ในระดับที่แตกต่างกันของนามธรรมมากกว่าแนวคิดการชุมนุมของอุปสรรคหน่วยความจำและจัดการกับสิ่งต่าง ๆ เช่น "การดำเนินการปรมาณูประสานกับสิ่งที่" มีข้อพิสูจน์ทางทฤษฎีใดบ้างที่การดำเนินการด้านล่างบรรลุเป้าหมายหรือไม่
void thread_a(){
set();
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!y.load()) foo();
}
void thread_b(){
y.store(true);
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!check()) bar();
}
ตัวเลือก B
ใช้การควบคุมที่เรามีมากกว่า Y เพื่อให้เกิดการซิงโครไนซ์โดยใช้การดำเนินการ read-modified-write memory_order_acq_rel บน Y:
void thread_a(){
set();
if(!y.fetch_add(0,std::memory_order_acq_rel)) foo();
}
void thread_b(){
y.exchange(1,std::memory_order_acq_rel);
if(!check()) bar();
}
แนวคิดในที่นี้คือการเข้าถึงอะตอมเดียว ( y) จะต้องเป็นคำสั่งเดียวที่ผู้สังเกตการณ์ทุกคนเห็นด้วยดังนั้นทั้งfetch_addก่อนexchangeหรือในทางกลับกัน
หากfetch_addก่อนหน้านี้exchangeส่วน "ปล่อย" ของการfetch_addซิงโครไนซ์กับส่วน "ได้รับ" ของexchangeและดังนั้นผลข้างเคียงทั้งหมดของset()จะต้องมองเห็นได้ในการดำเนินการรหัสcheck()ดังนั้นbar()จะไม่ถูกเรียก
มิฉะนั้นexchangeก่อนfetch_addแล้วfetch_addจะเห็นและไม่เรียก1 foo()ดังนั้นจึงเป็นไปไม่ได้ที่จะเรียกทั้งสองและfoo() bar()เหตุผลนี้ถูกต้องหรือไม่
ตัวเลือก C
ใช้ dummy atomics เพื่อแนะนำ "edge" ซึ่งป้องกันภัยพิบัติ พิจารณาแนวทางต่อไปนี้:
void thread_a(){
std::atomic<int> dummy1{};
set();
dummy1.store(13);
if(!y.load()) foo();
}
void thread_b(){
std::atomic<int> dummy2{};
y.store(1);
dummy2.load();
if(!check()) bar();
}
หากคุณคิดว่าปัญหาของที่นี่เป็นatomicของท้องถิ่นลองจินตนาการถึงการย้ายไปสู่ขอบเขตทั่วโลกด้วยเหตุผลดังต่อไปนี้มันไม่ปรากฏว่ามีความสำคัญสำหรับฉันและฉันจงใจเขียนโค้ดในลักษณะที่แสดงว่ามันตลกอย่างไร และ dummy2 นั้นแยกจากกันโดยสิ้นเชิง
ทำไมบนโลกนี้อาจใช้งานได้? ต้องมีคำสั่งรวมทั้งหมดบางคำสั่ง{dummy1.store(13), y.load(), y.store(1), dummy2.load()}ซึ่งจะต้องสอดคล้องกับ "ขอบ" ของคำสั่งโปรแกรม:
dummy1.store(13)"ในถึงก่อน"y.load()y.store(1)"ในถึงก่อน"dummy2.load()
(seq_cst store + load หวังว่าจะสร้าง C ++ เทียบเท่ากับกำแพงหน่วยความจำเต็มรวมทั้ง StoreLoad เหมือนที่ทำใน asm บน ISAs จริงรวมถึง AArch64 ที่ไม่จำเป็นต้องมีคำสั่งกีดกันแยกต่างหาก)
ขณะนี้เรามีสองกรณีที่ต้องพิจารณา: อาจy.store(1)เป็นก่อนy.load()หรือหลังตามลำดับทั้งหมด
ถ้าy.store(1)เป็นก่อนy.load()แล้วfoo()จะไม่ถูกเรียกและเรามีความปลอดภัย
ถ้าy.load()เป็นy.store(1)เช่นนั้นให้รวมกับขอบทั้งสองที่เรามีอยู่ในลำดับของโปรแกรมเราอนุมานว่า:
dummy1.store(13)"ในถึงก่อน"dummy2.load()
ตอนนี้dummy1.store(13)คือการดำเนินการปล่อยซึ่งปล่อยผลกระทบของset()และdummy2.load()เป็นการดำเนินการที่ได้รับดังนั้นcheck()ควรเห็นผลกระทบของset()และbar()จะไม่ถูกเรียกและเรามีความปลอดภัย
มันถูกต้องที่นี่ที่จะคิดว่าcheck()จะเห็นผลของset()? ฉันสามารถรวม "ขอบ" ของชนิดต่าง ๆ ("ลำดับของโปรแกรม" หรือที่เรียกว่า Sequenced Before, "รวมทั้งหมด", "ก่อนที่จะปล่อย", "หลังจากได้รับ") แบบนั้นได้หรือไม่? ฉันมีข้อสงสัยอย่างจริงจังเกี่ยวกับเรื่องนี้: กฎ C ++ ดูเหมือนจะพูดถึงความสัมพันธ์ระหว่าง "การซิงโครไนซ์ - กับ" ระหว่างร้านค้าและโหลดในตำแหน่งเดียวกัน - ที่นี่ไม่มีสถานการณ์ดังกล่าว
โปรดทราบว่าเรากำลังกังวลเพียง แต่เกี่ยวกับกรณีที่dumm1.storeเป็นที่รู้จักกัน (ผ่านเหตุผลอื่น ๆ ) ให้เป็นก่อนdummy2.loadในการสั่งซื้อรวม seq_cst ดังนั้นหากพวกเขาเข้าถึงตัวแปรเดียวกันโหลดจะเห็นค่าที่เก็บไว้และซิงโครไนซ์กับมัน
(เหตุผลของหน่วยความจำ - อุปสรรค / การเรียงลำดับเหตุผลใหม่สำหรับการใช้งานที่โหลดอะตอมและจัดเก็บรวบรวมอย่างน้อยหนึ่งอุปสรรคหน่วยความจำทางเดียว (และการดำเนินงาน seq_cst ไม่สามารถจัดลำดับใหม่: เช่นที่เก็บ seq_cst ไม่สามารถผ่านโหลด seq_cst) ร้านค้าหลังจากdummy2.loadแน่นอนจะปรากฏให้เห็นกระทู้อื่น ๆหลังจาก y.storeและในทำนองเดียวกันสำหรับหัวข้ออื่น ๆ ... ก่อนy.load.)
คุณสามารถเล่นกับการใช้งานตัวเลือก A, B, C ของฉันได้ที่https://godbolt.org/z/u3dTa8
foo()และbar()จากการถูกเรียกทั้งสอง
compare_exchange_*เพื่อดำเนินการ RMW บนอะตอมบูลโดยไม่ต้องเปลี่ยนค่าของมัน (เพียงตั้งค่าที่คาดไว้และใหม่เป็นค่าเดียวกัน)
atomic<bool>มีและexchange compare_exchange_weakหลังสามารถใช้ในการทำ RMW ปลอมโดย (พยายาม) CAS (จริงจริง) หรือเท็จเท็จ ไม่ว่าจะล้มเหลวหรือแทนที่ค่าด้วยตนเอง (ใน x86-64 asm เคล็ดลับlock cmpxchg16bนั้นคือวิธีที่คุณทำการโหลดอะตอมขนาด 16 ไบต์แบบไม่มีการรับประกัน แต่ไม่มีประสิทธิภาพ แต่แย่กว่าการล็อคแบบแยกกัน)
foo()มิได้bar()จะถูกเรียกว่า ฉันไม่ต้องการนำองค์ประกอบหลายอย่างของ "โลกแห่งความจริง" ของรหัสเพื่อหลีกเลี่ยง "คุณคิดว่าคุณมีปัญหา X แต่คุณมีปัญหา Y" การตอบสนองแบบ แต่ถ้าใครจริงๆต้องการที่จะรู้ว่าสิ่งที่เป็นชั้นพื้นหลัง: set()มันsome_mutex_exit(), check()เป็นtry_enter_some_mutex(), yคือ "มีบริกรบาง" foo()คือ "ทางออกโดยไม่ต้องตื่นขึ้นมาทุกคน" bar()คือ "รอ wakup" ... แต่ฉันปฏิเสธที่จะ พูดคุยเกี่ยวกับการออกแบบนี้ที่นี่ - ฉันไม่สามารถเปลี่ยนได้จริงๆ
std::atomic_thread_fence(std::memory_order_seq_cst)จะรวบรวมเป็นอุปสรรคเต็มรูปแบบ แต่เนื่องจากแนวคิดทั้งหมดเป็นรายละเอียดการใช้งานคุณจะไม่พบ พูดถึงมันในมาตรฐานใด ๆ (โดยปกติหน่วยความจำซีพียูรุ่นจะถูกกำหนดในแง่ของสิ่งที่ reorerings ได้รับอนุญาตเมื่อเทียบกับความสอดคล้องตามลำดับเช่น x86 คือ seq-cst + บัฟเฟอร์ร้านค้า w / ส่งต่อ)