การทำความเข้าใจ std :: atomic :: Compare_exchange_weak () ใน C ++ 11


88
bool compare_exchange_weak (T& expected, T val, ..);

compare_exchange_weak()เป็นหนึ่งในการเปรียบเทียบแบบดั้งเดิมของการแลกเปลี่ยนที่มีให้ใน C ++ 11 มันอ่อนแอexpectedในแง่ที่ว่ามันกลับเท็จแม้ว่ามูลค่าของวัตถุที่มีค่าเท่ากับ นี่เป็นเพราะความล้มเหลวปลอมในบางแพลตฟอร์มที่มีการใช้ลำดับของคำสั่ง (แทนที่จะเป็นหนึ่งใน x86) เพื่อใช้งาน บนแพลตฟอร์มดังกล่าวการสลับบริบทการโหลดที่อยู่เดียวกัน (หรือบรรทัดแคช) ซ้ำโดยเธรดอื่น ฯลฯ อาจทำให้ระบบดั้งเดิมล้มเหลว มันspuriousไม่ใช่ค่าของวัตถุ (ไม่เท่ากับexpected) ที่ทำให้การดำเนินการล้มเหลว แต่มันเป็นปัญหาเรื่องเวลา

แต่สิ่งที่ทำให้ฉันไขปริศนาคือสิ่งที่พูดใน C ++ 11 Standard (ISO / IEC 14882)

29.6.5 .. ผลที่ตามมาของความล้มเหลวปลอมคือการใช้การเปรียบเทียบและแลกเปลี่ยนที่อ่อนแอเกือบทั้งหมดจะวนเวียน

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

คำถามอื่นที่เกี่ยวข้อง ในหนังสือของเขา "C ++ Concurrency In Action" Anthony กล่าวว่า

//Because compare_exchange_weak() can fail spuriously, it must typically
//be used in a loop:

bool expected=false;
extern atomic<bool> b; // set somewhere else
while(!b.compare_exchange_weak(expected,true) && !expected);

//In this case, you keep looping as long as expected is still false,
//indicating that the compare_exchange_weak() call failed spuriously.

ทำไมจึงอยู่!expectedในเงื่อนไขการวนซ้ำ? มีไว้เพื่อป้องกันไม่ให้เธรดทั้งหมดอดอาหารและไม่มีความคืบหน้าในบางครั้งหรือไม่?

แก้ไข: (คำถามสุดท้าย)

บนแพลตฟอร์มที่ไม่มีคำสั่ง CAS ฮาร์ดแวร์ทั้งเวอร์ชันที่อ่อนแอและแข็งแกร่งจะถูกนำไปใช้โดยใช้ LL / SC (เช่น ARM, PowerPC เป็นต้น) ดังนั้นจึงมีความแตกต่างระหว่างสองลูปต่อไปนี้หรือไม่? ทำไมถ้ามี? (สำหรับฉันแล้วพวกเขาควรมีประสิทธิภาพใกล้เคียงกัน)

// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_weak(..))
{ .. }

// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_strong(..)) 
{ .. }

ฉันมาพร้อมกับคำถามสุดท้ายที่พวกคุณพูดถึงว่าอาจมีความแตกต่างด้านประสิทธิภาพในลูป นอกจากนี้ยังกล่าวถึงโดยมาตรฐาน C ++ 11 (ISO / IEC 14882):

เมื่อการเปรียบเทียบและแลกเปลี่ยนอยู่ในวงรอบเวอร์ชันที่อ่อนแอจะให้ประสิทธิภาพที่ดีกว่าในบางแพลตฟอร์ม

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


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

2
CAS ที่อ่อนแอและแข็งแกร่งถูกนำมาใช้ "โดยใช้ LL / SC" ในลักษณะเดียวกับที่ใช้ทั้งการเรียงฟองและ Quicksort "โดยใช้ swap"; นั่นคือในแง่ที่เป็นการดำเนินการแบบดั้งเดิมที่ใช้ในการทำงานให้ลุล่วง สิ่งที่พวกเขาล้อมรอบ LL / SC นั้นแตกต่างกันมาก CAS ที่อ่อนแอเป็นเพียง LL / SC Strong CAS คือ LL / SC พร้อมของอื่น ๆ อีกมากมาย
Sneftel

1
forums.manning.com/posts/list/33062.page ช่วยได้ไหม
Tu Xiaomi

@TuXiaomi พร้อมคำตอบในลิงค์นั้นฉันไม่เห็นว่าทำไม "เวอร์ชันที่อ่อนแอจะให้ประสิทธิภาพที่ดีกว่าในบางแพลตฟอร์ม" ตามที่ระบุไว้ใน Standard
Deqing

@Deqing สำหรับผู้อื่น Compare_exchange_weak อาจล้มเหลวอย่างไม่เป็นธรรมเนื่องจากการขัดจังหวะหรือการกระทำของโปรเซสเซอร์หรือเธรดอื่น บนแพลตฟอร์มเหล่านั้น Compare_exchange_strong เป็นลูปอย่างมีประสิทธิภาพบน Compare_exchange_weak - หากล้มเหลวอย่างผิดปกติมันจะวนซ้ำอีกครั้ง มันช่วย? บางทีฉันอาจจะผิด
Tu Xiaomi

คำตอบ:


75

ทำไมต้องแลกเปลี่ยนแบบวนซ้ำ?

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

โปรดทราบว่าcompare_exchange_strongมักใช้ในการวนซ้ำ ไม่ล้มเหลวเนื่องจากความล้มเหลวปลอม แต่ล้มเหลวเนื่องจากการเขียนพร้อมกัน

ทำไมต้องใช้weakแทนstrong?

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

ดังนั้นจึงweakใช้เพราะเร็วกว่าstrongในบางแพลตฟอร์มมาก

คุณควรใช้weakเมื่อไรและเมื่อไรstrong?

การอ้างอิงระบุว่าจะใช้weakเมื่อใดและควรใช้เมื่อใดstrong:

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

ดังนั้นคำตอบจึงค่อนข้างง่ายที่จะจำ: หากคุณต้องแนะนำลูปเพียงเพราะความล้มเหลวปลอมอย่าทำ ใช้strong. weakถ้าคุณมีห่วงอยู่แล้วจากนั้นใช้

ทำไมอยู่!expectedในตัวอย่าง

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

มันเป็นเพียงการติดตามอย่างรวดเร็วเมื่อเธรดอื่นเขียนtrue: จากนั้นเราก็ยกเลิกแทนที่จะพยายามเขียนtrueอีกครั้ง

เกี่ยวกับคำถามสุดท้ายของคุณ

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

จากWikipedia :

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

ดังนั้น LL / SC จะล้มเหลวอย่างปลอมแปลงในการสลับบริบทตัวอย่างเช่น ตอนนี้รุ่นที่แข็งแกร่งจะนำ "วงเล็ก ๆ ของตัวเอง" มาตรวจจับความล้มเหลวปลอมนั้นและปกปิดมันโดยลองอีกครั้ง โปรดทราบว่าลูปของตัวเองนี้ซับซ้อนกว่าลูป CAS ทั่วไปเช่นกันเนื่องจากต้องแยกความแตกต่างระหว่างความล้มเหลวปลอม (และปิดบัง) และความล้มเหลวเนื่องจากการเข้าถึงพร้อมกัน (ซึ่งส่งผลให้ได้รับค่าตอบแทนfalse) รุ่นที่อ่อนแอไม่มีลูปของตัวเอง

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

โปรดทราบว่าข้อโต้แย้งของคุณ (LL / SC) เป็นเพียงความเป็นไปได้อย่างหนึ่งที่จะนำสิ่งนี้ไปใช้ มีแพลตฟอร์มจำนวนมากขึ้นที่มีชุดคำสั่งที่แตกต่างกัน นอกจากนี้ (และที่สำคัญกว่า) โปรดทราบว่าstd::atomicต้องรองรับการดำเนินการทั้งหมดสำหรับประเภทข้อมูลที่เป็นไปได้ทั้งหมดดังนั้นแม้ว่าคุณจะประกาศโครงสร้างสิบล้านไบต์คุณก็สามารถใช้compare_exchangeกับสิ่งนี้ได้ แม้ว่าจะใช้ CPU ที่มี CAS แต่คุณก็ไม่สามารถ CAS สิบล้านไบต์ได้ดังนั้นคอมไพเลอร์จะสร้างคำสั่งอื่น ๆ (อาจเป็นการล็อคการได้รับตามด้วยการเปรียบเทียบและการแลกเปลี่ยนแบบไม่ใช้อะตอมตามด้วยการปลดล็อก) ทีนี้ลองคิดดูว่าจะเกิดอะไรขึ้นได้บ้างในขณะที่แลกเปลี่ยนสิบล้านไบต์ ดังนั้นในขณะที่ข้อผิดพลาดปลอมอาจหายากมากสำหรับการแลกเปลี่ยนขนาด 8 ไบต์ แต่ในกรณีนี้อาจพบได้บ่อยกว่า

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


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

4
@Voo: คำตอบที่อัปเดต ตอนนี้คำแนะนำจากการอ้างอิงรวมอยู่ด้วย อาจมีอัลกอริทึมที่สร้างความแตกต่าง ตัวอย่างเช่นพิจารณาความหมาย "ต้องอัปเดต": การอัปเดตบางสิ่งต้องทำครั้งเดียวดังนั้นเมื่อเราล้มเหลวเนื่องจากการเขียนพร้อมกันเรารู้ว่ามีคนอื่นทำเช่นนั้นและเราสามารถยกเลิกได้ หากเราล้มเหลวเนื่องจากความล้มเหลวปลอมเกินกว่าที่ไม่มีใครอัปเดตดังนั้นเราต้องลองอีกครั้ง
gexicide

8
" ทำไม! คาดหวังในตัวอย่างมันไม่จำเป็นสำหรับความถูกต้องการละเว้นมันจะให้ความหมายเดียวกัน" - ไม่ได้ ... ถ้าบอกว่าการแลกเปลี่ยนครั้งแรกล้มเหลวเนื่องจากพบว่าbมีอยู่แล้วtrueแล้ว - มีexpectedตอนนี้true- ไม่&& !expectedมัน loops และพยายามที่อื่น (โง่) การแลกเปลี่ยนtrueและtrueซึ่งอาจจะดี "ประสบความสำเร็จ" นิด ๆ หมดจากwhileวงแต่สามารถแสดง พฤติกรรมที่แตกต่างกันอย่างมีความหมายหากในbขณะเดียวกันก็เปลี่ยนกลับไปfalseซึ่งในกรณีนี้การวนซ้ำจะดำเนินต่อไปและในที่สุดอาจตั้งค่าb true อีกครั้งก่อนที่จะแตก
Tony Delroy

@TonyD: ใช่ฉันควรชี้แจงว่า
gexicide

ขออภัยฉันเพิ่มคำถามสุดท้ายอีกหนึ่งคำถาม;)
Eric Z

18

ฉันพยายามตอบคำถามนี้ด้วยตัวเองหลังจากอ่านแหล่งข้อมูลออนไลน์ต่างๆ (เช่นอันนี้และอันนี้ ) มาตรฐาน C ++ 11 รวมถึงคำตอบที่ให้ไว้ที่นี่

คำถามที่เกี่ยวข้องจะรวมเข้าด้วยกัน (เช่น " why! expected? " ถูกรวมเข้ากับ"why put Compare_exchange_weak () ในลูป? ")


เหตุใด Compare_exchange_weak () จึงต้องวนซ้ำในการใช้งานเกือบทั้งหมด

รูปแบบทั่วไปก

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

expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));

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

อีกตัวอย่างหนึ่งคือการใช้ mutex โดยใช้std::atomic<bool>. มากที่สุดคนหนึ่งด้ายสามารถป้อนส่วนที่สำคัญในช่วงเวลาซึ่งขึ้นอยู่กับด้ายชุดแรกcurrentไปtrueและออกจากวง

รูปแบบทั่วไป B

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

expected = false;
// !expected: if expected is set to true by another thread, it's done!
// Otherwise, it fails spuriously and we should try again.
while (!current.compare_exchange_weak(expected, true) && !expected);

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

ที่กล่าวว่าน่าจะหายากที่จะใช้compare_exchange_weak()นอกลูป ในทางตรงกันข้ามมีหลายกรณีที่มีการใช้งานเวอร์ชันที่แข็งแกร่ง เช่น,

bool criticalSection_tryEnter(lock)
{
  bool flag = false;
  return lock.compare_exchange_strong(flag, true);
}

compare_exchange_weak ไม่เหมาะสมที่นี่เพราะเมื่อมันกลับมาเนื่องจากความล้มเหลวปลอมเป็นไปได้ว่ายังไม่มีใครครอบครองส่วนสำคัญ

อดด้าย?

ประเด็นหนึ่งที่น่ากล่าวถึงคือจะเกิดอะไรขึ้นหากความล้มเหลวปลอมยังคงเกิดขึ้นดังนั้นจึงอดด้าย? ในทางทฤษฎีอาจเกิดขึ้นบนแพลตฟอร์มเมื่อcompare_exchange_XXX()มีการใช้งานตามลำดับคำสั่ง (เช่น LL / SC) การเข้าถึงบรรทัดแคชเดียวกันระหว่าง LL และ SC บ่อยๆจะทำให้เกิดความล้มเหลวปลอมอย่างต่อเนื่อง ตัวอย่างที่เป็นจริงมากขึ้นเกิดจากการจัดตารางเวลาแบบโง่ ๆ โดยที่เธรดที่ทำงานพร้อมกันทั้งหมดจะแทรกสลับกันด้วยวิธีต่อไปนี้

Time
 |  thread 1 (LL)
 |  thread 2 (LL)
 |  thread 1 (compare, SC), fails spuriously due to thread 2's LL
 |  thread 1 (LL)
 |  thread 2 (compare, SC), fails spuriously due to thread 1's LL
 |  thread 2 (LL)
 v  ..

จะเกิดขึ้นได้หรือไม่?

มันจะไม่เกิดขึ้นตลอดไปโชคดีด้วยสิ่งที่ C ++ 11 ต้องการ:

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

ทำไมเราถึงต้องใช้ Compare_exchange_weak () และเขียนลูปด้วยตัวเอง เราสามารถใช้ Compare_exchange_strong ()

มันขึ้นอยู่กับ.

กรณีที่ 1: เมื่อจำเป็นต้องใช้ทั้งสองอย่างภายในลูป C ++ 11 พูดว่า:

เมื่อการเปรียบเทียบและแลกเปลี่ยนอยู่ในวงรอบเวอร์ชันที่อ่อนแอจะให้ประสิทธิภาพที่ดีกว่าในบางแพลตฟอร์ม

บน x86 (อย่างน้อยในขณะนี้. บางทีมันอาจจะหันไปใช้รูปแบบคล้ายกันเป็น LL / SC วันหนึ่งสำหรับประสิทธิภาพการทำงานเมื่อแกนมากขึ้นจะนำ) cmpxchgรุ่นที่อ่อนแอและแข็งแรงเป็นหลักเดียวกันเพราะพวกเขาทั้งต้มลงไปที่คำสั่งเดียว บนแพลตฟอร์มอื่น ๆ ที่compare_exchange_XXX()ไม่ได้ใช้งานแบบอะตอม (ในที่นี้หมายถึงไม่มีฮาร์ดแวร์ดั้งเดิมที่มีอยู่) เวอร์ชันที่อ่อนแอในลูปอาจชนะการต่อสู้ได้เนื่องจากแพลตฟอร์มที่แข็งแกร่งจะต้องจัดการกับความล้มเหลวปลอมและลองใหม่ตามนั้น

แต่,

ไม่ค่อยเราอาจชอบcompare_exchange_strong()มากกว่าcompare_exchange_weak()แม้จะวนซ้ำ เช่นเมื่อมีหลายสิ่งที่ต้องทำระหว่างตัวแปรอะตอมถูกโหลดและค่าใหม่ที่คำนวณได้จะถูกแลกเปลี่ยนออกไป (ดูfunction()ด้านบน) หากตัวแปรอะตอมไม่เปลี่ยนแปลงบ่อยเราก็ไม่จำเป็นต้องคำนวณซ้ำสำหรับความล้มเหลวปลอมทุกครั้ง แต่เราอาจหวังว่าจะcompare_exchange_strong()"ดูดซับ" ความล้มเหลวดังกล่าวและเราจะคำนวณซ้ำก็ต่อเมื่อล้มเหลวเนื่องจากการเปลี่ยนแปลงมูลค่าที่แท้จริง

กรณีที่ 2: เมื่อ compare_exchange_weak() จำเป็นต้องใช้ภายในลูปเท่านั้น C ++ 11 ยังพูดว่า:

เมื่อการเปรียบเทียบและแลกเปลี่ยนที่อ่อนแอจะต้องใช้ลูปและอันที่แข็งแกร่งจะไม่เป็นที่ต้องการตัวที่แข็งแกร่งจะดีกว่า

โดยทั่วไปจะเป็นกรณีนี้เมื่อคุณวนซ้ำเพียงเพื่อกำจัดความล้มเหลวปลอมจากเวอร์ชันที่อ่อนแอ คุณลองอีกครั้งจนกว่าการแลกเปลี่ยนจะสำเร็จหรือล้มเหลวเนื่องจากการเขียนพร้อมกัน

expected = false;
// !expected: if it fails spuriously, we should try again.
while (!current.compare_exchange_weak(expected, true) && !expected);

ที่ดีที่สุดก็ reinventing compare_exchange_strong()ล้อและดำเนินการเช่นเดียวกับ แย่กว่านั้น? วิธีการนี้จะล้มเหลวในการใช้ประโยชน์จากเครื่องที่ให้ไม่ใช่ปลอมเปรียบเทียบและการแลกเปลี่ยนในฮาร์ดแวร์

สุดท้ายหากคุณวนลูปสำหรับสิ่งอื่น ๆ (เช่นดู "รูปแบบทั่วไป A" ด้านบน) มีโอกาสดีที่compare_exchange_strong()จะถูกใส่ในลูปด้วยซึ่งจะนำเรากลับไปที่กรณีก่อนหน้า


17

เหตุใดจึงต้องวนซ้ำในการใช้งานเกือบทั้งหมด ?

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

นั่นหมายความว่าเราจะวนซ้ำเมื่อมันล้มเหลวเพราะความล้มเหลวปลอม ๆ ?

ใช่.

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

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

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

ทำไมจึงอยู่!expectedในเงื่อนไขการวนซ้ำ?

ค่านี้อาจถูกตั้งค่าtrueโดยเธรดอื่นดังนั้นคุณจึงไม่ต้องการที่จะพยายามตั้งค่าแบบวนซ้ำ

แก้ไข:

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

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

รูปแบบที่อ่อนแอเพียงแค่กลับมาจากความล้มเหลวปลอมมันไม่ได้ลองใหม่


2
+1 ถูกต้องตามความเป็นจริงในการนับทั้งหมด (ซึ่ง Q ต้องการอย่างยิ่ง)
Tony Delroy

เกี่ยวกับyou don't know what its current value isจุดที่ 1 เมื่อความล้มเหลวของปลอมจะเกิดขึ้นไม่ควรค่าปัจจุบันเท่ากับมูลค่าที่คาดว่าทันทีที่? มิฉะนั้นจะเป็นความล้มเหลวอย่างแท้จริง
Eric Z

IMO ทั้งเวอร์ชันที่อ่อนแอและเวอร์ชันที่แข็งแกร่งถูกนำไปใช้โดยใช้ LL / SC บนแพลตฟอร์มที่ไม่มีฮาร์ดแวร์ดั้งเดิมของ CAS เพียงตัวเดียว สำหรับฉันแล้วทำไมถึงมีความแตกต่างของประสิทธิภาพระหว่างwhile(!compare_exchange_weak(..))และwhile(!compare_exchange_strong(..))?
Eric Z

ขออภัยฉันเพิ่มคำถามสุดท้ายอีกหนึ่งคำถาม
Eric Z

1
@ Jonathan: เพียงแค่ nitpick แต่คุณทำรู้ค่าปัจจุบันถ้ามันล้มเหลว spuriously (แน่นอนไม่ว่าจะยังคงมูลค่าปัจจุบันตามเวลาที่คุณอ่านตัวแปรเป็นปัญหาอื่นอย่างสิ้นเชิง แต่ที่ไม่คำนึงถึงความอ่อนแอ / strong) ตัวอย่างเช่นฉันเคยใช้สิ่งนี้เพื่อพยายามตั้งค่าตัวแปรโดยสมมติว่าค่าของมันเป็นโมฆะและหากล้มเหลว (ปลอมหรือไม่) ให้พยายามต่อไป แต่ขึ้นอยู่กับค่าที่แท้จริงเท่านั้น
Cameron

13

เอาล่ะฉันต้องการฟังก์ชั่นที่ทำหน้าที่เปลี่ยนอะตอมไปทางซ้าย โปรเซสเซอร์ของฉันไม่มีการทำงานแบบเนทีฟสำหรับสิ่งนี้และไลบรารีมาตรฐานไม่มีฟังก์ชันสำหรับสิ่งนี้ดังนั้นดูเหมือนว่าฉันกำลังเขียนของตัวเอง ที่นี่:

void atomicLeftShift(std::atomic<int>* var, int shiftBy)
{
    do {
        int oldVal = std::atomic_load(var);
        int newVal = oldVal << shiftBy;
    } while(!std::compare_exchange_weak(oldVal, newVal));
}

ตอนนี้มีสองเหตุผลที่อาจมีการดำเนินการวนซ้ำมากกว่าหนึ่งครั้ง

  1. มีคนอื่นเปลี่ยนตัวแปรในขณะที่ฉันกะทางซ้าย ผลลัพธ์ของการคำนวณของฉันไม่ควรนำไปใช้กับตัวแปรอะตอมเพราะมันจะลบการเขียนของคนอื่นได้อย่างมีประสิทธิภาพ
  2. CPU ของฉันเรอและ CAS ที่อ่อนแอล้มเหลวอย่างผิดปกติ

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

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

และอย่างที่บอกไปก่อนหน้านี้ฉันไม่สนใจผลงานนักสืบนั่นหรอก! ไม่ว่าจะด้วยวิธีใดฉันจะทำ CAS ซ้ำ ดังนั้นการใช้ CAS ที่แข็งแกร่งทำให้ฉันไม่มีอะไรแน่นอนและทำให้ฉันเสียประสิทธิภาพไปเพียงเล็กน้อย แต่วัดผลได้

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


0

ฉันคิดว่าคำตอบส่วนใหญ่ข้างต้นระบุว่า "ความล้มเหลวปลอม" เป็นปัญหาบางอย่างประสิทธิภาพเทียบกับความถูกต้องแลกกัน

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

สำหรับฉันความแตกต่างที่สำคัญคือสองเวอร์ชันนี้จัดการกับปัญหา ABA อย่างไร:

เวอร์ชันที่อ่อนแอจะประสบความสำเร็จก็ต่อเมื่อไม่มีใครแตะเส้นแคชระหว่างโหลดและจัดเก็บดังนั้นจะตรวจพบปัญหา ABA ได้ 100%

เวอร์ชันที่แข็งแกร่งจะล้มเหลวก็ต่อเมื่อการเปรียบเทียบล้มเหลวดังนั้นจึงไม่พบปัญหา ABA หากไม่มีมาตรการเพิ่มเติม

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

แต่บน x86 (สถาปัตยกรรมที่ได้รับการสั่งซื้อที่แข็งแกร่ง) เวอร์ชันที่อ่อนแอและเวอร์ชันที่แข็งแกร่งจะเหมือนกันและทั้งคู่ประสบปัญหา ABA

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

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

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

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