วิธีการบรรลุอุปสรรค StoreLoad ใน C ++ 11?


13

ฉันต้องการเขียนโค้ดแบบพกพา (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


1
โมเดลหน่วยความจำ C ++ ไม่มีแนวคิดของการจัดลำดับใหม่ของ StoreLoad เพียงซิงโครไนซ์ - กับและเกิดขึ้นก่อนหน้า (และ UB ในการแข่งขันข้อมูลบนออบเจ็กต์ที่ไม่ใช่อะตอมมิกซึ่งแตกต่างจาก asm สำหรับฮาร์ดแวร์จริง) ในการใช้งานจริงทั้งหมดที่ฉันตระหนักถึง std::atomic_thread_fence(std::memory_order_seq_cst)จะรวบรวมเป็นอุปสรรคเต็มรูปแบบ แต่เนื่องจากแนวคิดทั้งหมดเป็นรายละเอียดการใช้งานคุณจะไม่พบ พูดถึงมันในมาตรฐานใด ๆ (โดยปกติหน่วยความจำซีพียูรุ่นจะถูกกำหนดในแง่ของสิ่งที่ reorerings ได้รับอนุญาตเมื่อเทียบกับความสอดคล้องตามลำดับเช่น x86 คือ seq-cst + บัฟเฟอร์ร้านค้า w / ส่งต่อ)
Peter Cordes

@ PeterCordes ขอบคุณฉันอาจไม่ชัดเจนในการเขียนของฉัน ฉันต้องการถ่ายทอดสิ่งที่คุณเขียนในส่วน "ตัวเลือก A" ฉันรู้ว่าชื่อคำถามของฉันใช้คำว่า "StoreLoad" และ "StoreLoad" เป็นแนวคิดจากโลกที่แตกต่างอย่างสิ้นเชิง ปัญหาของฉันคือวิธีแมปแนวคิดนี้ลงใน C ++ หรือถ้ามันไม่สามารถแมปโดยตรงแล้ววิธีการบรรลุเป้าหมายที่ฉันวาง: ป้องกันfoo()และbar()จากการถูกเรียกทั้งสอง
qbolec

1
คุณสามารถใช้compare_exchange_*เพื่อดำเนินการ RMW บนอะตอมบูลโดยไม่ต้องเปลี่ยนค่าของมัน (เพียงตั้งค่าที่คาดไว้และใหม่เป็นค่าเดียวกัน)
mpoeter

1
@Fareanor และ qbolec: atomic<bool>มีและexchange compare_exchange_weakหลังสามารถใช้ในการทำ RMW ปลอมโดย (พยายาม) CAS (จริงจริง) หรือเท็จเท็จ ไม่ว่าจะล้มเหลวหรือแทนที่ค่าด้วยตนเอง (ใน x86-64 asm เคล็ดลับlock cmpxchg16bนั้นคือวิธีที่คุณทำการโหลดอะตอมขนาด 16 ไบต์แบบไม่มีการรับประกัน แต่ไม่มีประสิทธิภาพ แต่แย่กว่าการล็อคแบบแยกกัน)
Peter Cordes

1
@PeterCordes ใช่ฉันรู้ว่ามันสามารถเกิดขึ้นได้ว่าไม่ว่าfoo()มิได้bar()จะถูกเรียกว่า ฉันไม่ต้องการนำองค์ประกอบหลายอย่างของ "โลกแห่งความจริง" ของรหัสเพื่อหลีกเลี่ยง "คุณคิดว่าคุณมีปัญหา X แต่คุณมีปัญหา Y" การตอบสนองแบบ แต่ถ้าใครจริงๆต้องการที่จะรู้ว่าสิ่งที่เป็นชั้นพื้นหลัง: set()มันsome_mutex_exit(), check()เป็นtry_enter_some_mutex(), yคือ "มีบริกรบาง" foo()คือ "ทางออกโดยไม่ต้องตื่นขึ้นมาทุกคน" bar()คือ "รอ wakup" ... แต่ฉันปฏิเสธที่จะ พูดคุยเกี่ยวกับการออกแบบนี้ที่นี่ - ฉันไม่สามารถเปลี่ยนได้จริงๆ
qbolec

คำตอบ:


5

ตัวเลือก A และ B เป็นโซลูชั่นที่ถูกต้อง

  • ตัวเลือกก: มันไม่สำคัญว่าสิ่งที่รั้ว seq-cst แปลว่ามาตรฐาน C ++ กำหนดสิ่งที่รับประกันได้อย่างชัดเจน ฉันได้วางมันลงในโพสต์นี้: รั้ว memory_order_seq_cst จะมีประโยชน์เมื่อไหร่?
  • ตัวเลือก B: ใช่เหตุผลของคุณถูกต้อง การปรับเปลี่ยนทั้งหมดในวัตถุบางอย่างมีคำสั่งทั้งหมดเดียว (ลำดับการแก้ไข) ดังนั้นคุณสามารถใช้สิ่งนั้นเพื่อซิงโครไนซ์เธรดและตรวจสอบให้แน่ใจว่ามองเห็นผลข้างเคียงทั้งหมด

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

ปรับปรุง

ตัวเลือกก:
ฉันถือว่าset()และcheck()ทำงานกับค่าปรมาณู จากนั้นเรามีสถานการณ์ดังต่อไปนี้ (-> หมายถึงลำดับขั้นก่อนหน้า ):

  • set()-> fence1(seq_cst)->y.load()
  • y.store(true)-> fence2(seq_cst)->check()

ดังนั้นเราสามารถใช้กฎต่อไปนี้:

สำหรับการดำเนินงานของอะตอมและBในวัตถุอะตอมMที่ปรับเปลี่ยนMและBจะใช้เวลาคุ้มค่าถ้ามีรั้วXและYดังกล่าวว่าเป็นลำดับขั้นตอนก่อนที่จะX , Yเป็นลำดับขั้นตอนก่อนที่จะBและXแจ๋วYในS , จากนั้นBสังเกตว่าเอฟเฟกต์ของAหรือการดัดแปลงMในภายหลังตามลำดับการแก้ไขmemory_order_seq_cst

คือcheck()เห็นค่าที่เก็บไว้ในsetหรือy.load()เห็นค่าที่เขียนเป็นy.store()(การดำเนินการบนyสามารถใช้งานได้memory_order_relaxed)

ตัวเลือก C: C ++ 17 มาตรฐานรัฐ [32.4.3, p1347]:

จะต้องมีคำสั่งซื้อทั้งหมดเดียวSในmemory_order_seq_cstการดำเนินงานทั้งหมดสอดคล้องกับคำสั่ง "เกิดขึ้นก่อน" และคำสั่งการแก้ไขสำหรับสถานที่ได้รับผลกระทบทั้งหมด [... ]

คำสำคัญที่นี่คือ "สอดคล้อง" มันแสดงให้เห็นว่าถ้าดำเนินการเกิดขึ้นก่อนที่จะดำเนินการBแล้วต้องนำหน้าBในS แต่ความหมายตรรกะเป็นทางเดียวถนนดังนั้นเราจึงไม่สามารถสรุปผกผัน: เพียงเพราะการดำเนินการบางCนำหน้าการดำเนินการพัฒนาในSไม่ได้หมายความว่าCที่เกิดขึ้นก่อนที่จะพัฒนา

โดยเฉพาะอย่างยิ่งการดำเนินการ seq-cst สองรายการในวัตถุที่แยกกันสองรายการไม่สามารถใช้เพื่อสร้างสิ่งที่เกิดขึ้นก่อนความสัมพันธ์แม้ว่าการดำเนินการจะได้รับคำสั่งทั้งหมดใน Sหากคุณต้องการสั่งซื้อการดำเนินงานกับวัตถุแยกต่างหากคุณต้องอ้างถึง seq-cst - รั้ว (ดูตัวเลือก A)


ไม่ชัดเจนว่าตัวเลือก C ไม่ถูกต้อง การดำเนินการ seq-cst แม้ในวัตถุส่วนตัวยังคงสามารถสั่งการดำเนินการอื่น ๆ ในระดับหนึ่ง ตกลงกันว่าไม่มีการซิงโครไนซ์ - ด้วย แต่เราไม่สนใจว่า foo หรือ bar ใดที่ทำงาน (หรือเห็นได้ชัดว่าไม่มี) โดยที่พวกเขาไม่ได้ทำงานทั้งสองอย่าง ฉันคิดว่าให้ลำดับความสัมพันธ์ก่อนและลำดับทั้งหมดของการดำเนินการ seq-cst (ซึ่งต้องมีอยู่)
Peter Cordes

ขอบคุณ @mpoeter คุณช่วยอธิบายเกี่ยวกับตัวเลือกก. สามข้อในคำตอบของคุณได้ที่นี่ไหม IIUC หากy.load()ไม่เห็นผลของy.store(1)จากนั้นเราสามารถพิสูจน์ได้จากกฎที่ใน S, atomic_thread_fenceของ thread_a เป็นก่อนatomic_thread_fenceของ thread_b สิ่งที่ฉันไม่เห็นเป็นวิธีการที่จะได้รับจากนี้ไปสู่ข้อสรุปว่าผลข้างเคียงที่จะมองเห็นset() check()
qbolec

1
@ qbolec: ฉันได้อัปเดตคำตอบพร้อมรายละเอียดเพิ่มเติมเกี่ยวกับตัวเลือก A.
mpoeter เมื่อ

1
ใช่การปฏิบัติการโลคัล seq-cst จะยังคงเป็นส่วนหนึ่งของคำสั่งซื้อทั้งหมดSในการดำเนินการ seq-cst ทั้งหมด แต่Sเป็น "เท่านั้น" สอดคล้องกับที่เกิดขึ้นก่อนการสั่งซื้อและการปรับเปลี่ยนการสั่งซื้อเช่นถ้าเกิดขึ้นมาก่อนBแล้ว ต้องนำหน้าBในS แต่ตรงกันข้ามจะไม่รับประกันคือเพียงเพราะแจ๋วBในSเราไม่สามารถสรุปได้ว่าเกิดขึ้นมาก่อนB
mpoeter

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

1

ในตัวอย่างแรกy.load()การอ่าน 0 ไม่ได้หมายความถึงสิ่งที่y.load()เกิดขึ้นก่อนหน้าy.store(1)นี้

มันไม่ได้หมายความว่ามันอยู่ก่อนหน้านี้ในคำสั่งซื้อทั้งหมดเดียวขอบคุณกฎที่โหลด seq_cst ส่งกลับค่าของร้าน seq_cst ล่าสุดในการสั่งซื้อทั้งหมดหรือมูลค่าของร้านค้าที่ไม่ใช่ seq_cst ที่ไม่เคยเกิดขึ้นมาก่อน มัน (ซึ่งในกรณีนี้ไม่มีอยู่) ดังนั้นหากy.store(1)เร็วกว่าy.load()ลำดับทั้งหมดy.load()จะได้รับคืน 1

หลักฐานยังคงถูกต้องเพราะคำสั่งซื้อทั้งหมดเดียวไม่มีวงจร

วิธีการแก้ปัญหานี้?

std::atomic<int> x2{0},y{0};

void thread_a(){
  set();
  x2.store(1);
  if(!y.load()) foo();
}

void thread_b(){
  y.store(1);
  if(!x2.load()) bar();
}

ปัญหาของ OP คือฉันไม่สามารถควบคุม "X" - มันอยู่ด้านหลังมาโครของ wrapper หรือบางอย่างและอาจจะไม่ใช่ seq-cst store / load ฉันอัปเดตคำถามเพื่อเน้นว่าดีกว่า
Peter Cordes

@PeterCordes ความคิดคือการสร้าง "x" อีกครั้งที่เขาสามารถควบคุมได้ ฉันจะเปลี่ยนชื่อเป็น "x2" ในคำตอบเพื่อให้ชัดเจนยิ่งขึ้น ฉันแน่ใจว่าฉันขาดคุณสมบัติบางอย่าง แต่ถ้าข้อกำหนดเพียงอย่างเดียวคือตรวจสอบให้แน่ใจว่า foo () และ bar () ไม่ได้ถูกเรียกทั้งคู่สิ่งนี้จะเป็นไปตามนั้น
Tomek Czajka

ดังนั้นจะif(false) foo();แต่ฉันคิดว่า OP ไม่ต้องการที่ทั้ง: P จุดที่น่าสนใจ แต่ฉันคิดว่า OP ไม่ต้องการโทรเงื่อนไขให้เป็นไปตามเงื่อนไขที่ระบุพวกเขา!
Peter Cordes

1
สวัสดี @TomekCzajka ขอบคุณที่สละเวลาเพื่อเสนอวิธีแก้ปัญหาใหม่ มันไม่สามารถใช้งานได้ในกรณีเฉพาะของฉันเนื่องจากไม่check()เห็นผลข้างเคียงที่สำคัญของ(ดูความคิดเห็นของฉันที่มีต่อคำถามของฉันสำหรับความหมายที่แท้จริงของโลกset,check,foo,bar) ฉันคิดว่ามันสามารถใช้งานได้if(!x2.load()){ if(check())x2.store(0); else bar(); }แทน
qbolec

1

@mpoeter อธิบายว่าทำไมตัวเลือก A และ B จึงปลอดภัย

ในทางปฏิบัติเกี่ยวกับการใช้งานจริงฉันคิดว่าตัวเลือก A ต้องการเฉพาะstd::atomic_thread_fence(std::memory_order_seq_cst)ในเธรด A ไม่ใช่ B

ในทางปฏิบัติร้านค้า seq-cst รวมถึงกำแพงหน่วยความจำเต็มหรืออย่างน้อย AArch64 อย่างน้อยไม่สามารถสั่งซื้อใหม่ได้ในภายหลังหรือโหลด seq_cst ( stlrลำดับต่อเนื่องจะต้องระบายออกจากบัฟเฟอร์ร้านค้าก่อนldarจึงจะสามารถอ่านได้จากแคช)

C ++ -> การแมป asmมีตัวเลือกในการใส่ค่าใช้จ่ายในการระบายบัฟเฟอร์การจัดเก็บในร้านค้าอะตอมหรือโหลดอะตอม ตัวเลือกที่มีเหตุผลสำหรับการใช้งานจริงคือการทำให้โหลดของอะตอมราคาถูกดังนั้นร้าน seq_cst จึงมีกำแพงเต็มรูปแบบ (รวมถึง StoreLoad) ในขณะที่โหลด seq_cst จะเหมือนกับโหลดที่ได้รับมากที่สุด

(แต่ไม่ใช่ POWER แต่โหลดก็ต้องการการซิงค์ที่หนักหน่วง = กำแพงกั้นเต็มเพื่อหยุดการส่งต่อร้านค้าจากเธรด SMT อื่น ๆ บนแกนเดียวกันซึ่งอาจนำไปสู่การจัดเรียง IRIW ใหม่เนื่องจาก seq_cst ต้องการเธรดทั้งหมดจึงสามารถเห็นด้วยกับคำสั่งของ ops seq_cst ทั้งหมด. อะตอมมิกสองตัวจะถูกเขียนไปยังตำแหน่งที่ต่างกันในเธรดที่แตกต่างกันจะเห็นได้ในลำดับเดียวกันโดยเธรดอื่น ๆ หรือไม่? )

(แน่นอนว่าสำหรับการรับประกันความปลอดภัยอย่างเป็นทางการเราจำเป็นต้องมีรั้วกั้นทั้งในการส่งเสริมการรับ / ปล่อยชุด () -> check () ลงใน seq_cst ซิงโครไนซ์ - ด้วยมันจะทำงานสำหรับชุดผ่อนคลายฉันคิดว่า การตรวจสอบที่ผ่อนคลายสามารถเรียงลำดับใหม่ด้วยแถบจาก POV ของหัวข้ออื่น ๆ )


ฉันคิดว่าปัญหาที่แท้จริงของ Option C นั้นขึ้นอยู่กับผู้สังเกตการณ์บางคนที่สามารถซิงโครไนซ์กับyและการดำเนินการจำลองได้ และเราคาดหวังว่าคอมไพเลอร์จะรักษาลำดับนั้นไว้เมื่อทำการสร้าง asm สำหรับ ISA ที่อิงกับสิ่งกีดขวาง

นี่จะเป็นจริงในทางปฏิบัติบน ISAs จริง ทั้งสองเธรดมีสิ่งกีดขวางหรือสิ่งเทียบเท่าและคอมไพเลอร์จะไม่เพิ่มประสิทธิภาพอะตอมมิก แต่แน่นอน "การคอมไพล์ไปยัง ISA ที่ใช้สิ่งกีดขวาง" ไม่ใช่ส่วนหนึ่งของมาตรฐาน ISO C ++ Coache ที่ใช้ร่วมกันแคชเป็นผู้สังเกตการณ์สมมุติที่มีอยู่สำหรับการให้เหตุผล asm แต่ไม่ใช่สำหรับการให้เหตุผล ISO C ++

สำหรับตัวเลือก C ในการทำงานเราต้องสั่งซื้อเช่นdummy1.store(13);/ y.load()/ set();(เท่าที่เห็นจากกระทู้ B) เพื่อละเมิดบาง ISO c ++ กฎ

เธรดที่รันคำสั่งเหล่านี้จะต้องทำตัวราวกับว่า set()ถูกดำเนินการก่อน (เพราะ Sequenced Before) ไม่เป็นไรการสั่งซื้อหน่วยความจำรันไทม์และ / หรือเวลาในการรวบรวมการเรียงลำดับการดำเนินการใหม่ยังคงทำได้

สอง seq_cst ops d1=13และyสอดคล้องกับ Sequenced Before (ลำดับโปรแกรม) set()ไม่ได้มีส่วนร่วมในการสั่งซื้อทั่วโลกที่จำเป็นสำหรับ seq_cst ops เพราะไม่ใช่ seq_cst

เธรด B ไม่ซิงโครไนซ์กับ dummy1.store ดังนั้นจึงไม่มีสิ่งที่เกิดขึ้นก่อนข้อกำหนดที่setเกี่ยวข้องกับการd1=13ใช้งานแม้ว่าการกำหนดนั้นเป็นการดำเนินการเผยแพร่

ฉันไม่เห็นการละเมิดกฎที่เป็นไปได้อื่น ๆ ฉันไม่สามารถหาอะไรที่นี่ที่จะต้องสอดคล้องกับติดใจ-ก่อนsetd1=13

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

ซีพียู "ปกติ" ชนิดใดที่มีแคชที่ใช้ร่วมกันที่สอดคล้องกันซึ่งการเรียงลำดับใหม่นี้อาจเกิดขึ้นจริงขณะรันไทม์ไม่น่าเชื่อถือ ( แต่ถ้าคอมไพเลอร์สามารถลบdummy1และdummy2จากนั้นได้อย่างชัดเจนว่าเราจะมีปัญหาและผมคิดว่าที่ได้รับอนุญาตตามมาตรฐาน.)

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

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

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


@mpoeter แนะนำว่าคอมไพเลอร์สามารถลบโหลดดัมมี่และการดำเนินการจัดเก็บแม้ในวัตถุ seq_cst

ฉันคิดว่าอาจถูกต้องเมื่อพวกเขาสามารถพิสูจน์ได้ว่าไม่มีสิ่งใดสามารถซิงโครไนซ์กับการดำเนินการได้ เช่นคอมไพเลอร์ที่สามารถมองเห็นว่าdummy2ไม่หนีฟังก์ชันอาจลบ seq_cst ที่โหลด

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

แน่นอนว่าคอมไพเลอร์ในปัจจุบันจะไม่ปรับแต่งอะตอมมิกเลยแม้แต่น้อยถึงแม้ว่า ISO C ++ จะไม่ห้ามก็ตาม นั่นเป็นปัญหาที่ยังไม่แก้สำหรับคณะกรรมการมาตรฐาน

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


สรุปดี! ฉันยอมรับว่าในทางปฏิบัติมันอาจจะพอเพียงถ้าเธรด A เท่านั้นที่มีรั้ว seq-cst อย่างไรก็ตามตามมาตรฐาน C ++ เราจะไม่รับประกันว่าจำเป็นที่เราจะเห็นคุณค่าล่าสุดจากset()ดังนั้นฉันจะยังคงใช้รั้วในด้าย B เช่นกัน ฉันคิดว่าร้านค้าแบบสบาย ๆ ที่มีรั้ว seq-cst จะสร้างรหัสเกือบเหมือนกับ seq-cst-store ต่อไป
mpoeter

@mpoeter: ใช่ฉันแค่พูดถึงในทางปฏิบัติไม่ใช่เป็นทางการ เพิ่มโน้ตที่ส่วนท้ายของส่วนนั้น และใช่ในทางปฏิบัติเกี่ยวกับ ISAs ส่วนใหญ่ฉันคิดว่าร้าน seq_cst มักจะเป็นเพียงร้านค้าธรรมดา (ผ่อนคลาย) + อุปสรรค หรือไม่; บน POWER ที่เก็บ seq-cst ทำ (น้ำหนักมาก) sync หน้าร้านไม่มีอะไรหลังจากนั้น godbolt.org/z/mAr72P แต่การโหลด seq-cst ต้องการอุปสรรคบางอย่างทั้งสองด้าน
Peter Cordes

1

ในมาตรฐาน ISO std :: mutex รับประกันว่าจะได้รับและปล่อยการสั่งซื้อเท่านั้นไม่ใช่ seq_cst

แต่ไม่มีอะไรรับประกันว่าจะมี "การสั่งซื้อ seq_cst" เนื่องจากseq_cstไม่ใช่คุณสมบัติของการดำเนินการใด ๆ

seq_cstเป็นการรับประกันการดำเนินการทั้งหมดของการใช้งานที่กำหนดของstd::atomicหรืออะตอมมิกคลาสทางเลือก ดังนั้นคำถามของคุณไม่ปลอดภัย

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