Recursive Lock (Mutex) กับ Non-Recursive Lock (Mutex)


183

POSIX อนุญาตให้ mutexes วนซ้ำ นั่นหมายความว่าเธรดเดียวกันสามารถล็อค mutex เดียวกันสองครั้งและจะไม่หยุดชะงัก แน่นอนว่ามันต้องปลดล็อคสองครั้งมิฉะนั้นจะไม่มีเธรดอื่นที่สามารถรับ mutex ได้ ไม่ได้ทุกระบบสนับสนุน pthreads ยังสนับสนุน mutexes recursive แต่ถ้าพวกเขาต้องการที่จะPOSIX สอดคล้องพวกเขาจะต้อง

API อื่น ๆ (API ระดับสูงกว่า) มักจะเสนอ mutexes ซึ่งมักเรียกว่าล็อค ระบบ / ภาษาบางระบบ (เช่น Cocoa Objective-C) เสนอทั้ง mutex แบบเรียกซ้ำและไม่เรียกซ้ำ บางภาษาเสนอเพียงหนึ่งภาษาเท่านั้น เช่นใน Java mutexes จะเรียกซ้ำ (เธรดเดียวกันอาจสองครั้ง "ซิงโครไนซ์" บนวัตถุเดียวกัน) ขึ้นอยู่กับฟังก์ชันเธรดอื่น ๆ ที่พวกเขาเสนอการไม่มี mutexes แบบเรียกซ้ำอาจไม่มีปัญหาเนื่องจากพวกเขาสามารถเขียนได้อย่างง่ายดายด้วยตัวคุณเอง

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

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

คำตอบ:


154

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

อย่างไรก็ตามมีข้อควรพิจารณาอื่น ๆ ในการเล่นที่นี่เช่นกัน

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

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

หากคุณอ้างถึงเคอร์เนลคลาสสิกของ VxWorks RTOS พวกมันจะกำหนดกลไกสามอย่าง:

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

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


9
คำอธิบายของคุณเกี่ยวกับ mutex ที่ไม่เกิดซ้ำฟังดูเหมือนเซมาฟอร์มากขึ้น mutex (ไม่ว่าจะเรียกซ้ำหรือไม่เรียกซ้ำ) มีแนวคิดเกี่ยวกับความเป็นเจ้าของ
Jay D

@ JayD มันสับสนมากเมื่อผู้คนโต้เถียงกันเกี่ยวกับสิ่งต่าง ๆ เหล่านี้ .. แล้วใครคือคนที่กำหนดสิ่งเหล่านี้
Pacerier

13
@Pacerier มาตรฐานที่เกี่ยวข้อง คำตอบนี้เป็นข้อผิดพลาดสำหรับ posix (pthreads) ซึ่งการปลดล็อก mutex ปกติในเธรดอื่นที่ไม่ใช่เธรดที่ล็อกมันเป็นพฤติกรรมที่ไม่ได้กำหนดในขณะเดียวกันก็ทำเช่นเดียวกันกับการตรวจสอบข้อผิดพลาดหรือการเรียกซ้ำ mutex ในรหัสข้อผิดพลาด ระบบและมาตรฐานอื่น ๆ อาจทำงานแตกต่างกันมาก
เลขที่

บางทีนี่อาจเป็นความไร้เดียงสา แต่ฉันรู้สึกว่าแนวคิดหลักของ mutex คือการล็อกเธรดปลดล็อก mutex จากนั้นเธรดอื่นอาจทำเช่นเดียวกัน จากcomputing.llnl.gov/tutorials/pthreads :
user657862

2
@currguy - การออกอากาศจะปล่อยเธรดใด ๆ และทั้งหมดที่ถูกบล็อกในเซมาฟอร์โดยไม่ต้องให้มัน (ยังคงว่างเปล่า) ในขณะที่การให้ไบนารีปกติจะปล่อยเธรดที่หัวของคิวรอ (สมมติว่ามีบล็อกหนึ่ง)
Tall Jeff

123

คำตอบนั้นไม่ได้มีประสิทธิภาพ mutexes ที่ไม่ใช่ reentrant นำไปสู่รหัสที่ดีกว่า

ตัวอย่าง: A :: foo () ได้รับการล็อค จากนั้นเรียก B :: bar () สิ่งนี้ใช้ได้ดีเมื่อคุณเขียน แต่บางครั้งก็มีคนเปลี่ยน B :: bar () เพื่อโทรไปที่ A :: baz () ซึ่งยังได้รับการล็อค

ถ้าคุณไม่มี mutexes ซ้ำการหยุดชะงักนี้ หากคุณมีพวกเขามันจะทำงาน แต่มันอาจแตก A :: foo () อาจปล่อยให้วัตถุอยู่ในสถานะที่ไม่สอดคล้องกันก่อนที่จะเรียกแถบ () บนสมมติฐานที่ว่า baz () ไม่สามารถทำงานได้เพราะมันได้รับ mutex แต่มันก็ไม่ควรวิ่ง! คนที่เขียน A :: foo () สันนิษฐานว่าไม่มีใครสามารถโทร A :: baz () ในเวลาเดียวกัน - นั่นคือเหตุผลทั้งหมดที่ทั้งสองวิธีได้รับการล็อค

รูปแบบจิตที่ถูกต้องสำหรับการใช้ mutexes: Mutex ปกป้องค่าคงที่ เมื่อมีการ mutex ค่าคงที่อาจเปลี่ยนแปลง แต่ก่อนที่จะปล่อย mutex ค่าคงที่จะถูกสร้างขึ้นใหม่ การล็อก Reentrant อันตรายเนื่องจากครั้งที่สองที่คุณได้รับการล็อคคุณไม่สามารถมั่นใจได้ว่าค่าคงที่จะเป็นความจริงอีกต่อไป

หากคุณมีความสุขกับการล็อค reentrant เป็นเพียงเพราะคุณไม่ต้องแก้ไขปัญหาเช่นนี้มาก่อน Java มีล็อคที่ไม่ใช่ reentrant วันนี้ใน java.util.concurrent.locks โดยวิธี


4
ฉันใช้เวลาสักครู่เพื่อให้ได้สิ่งที่คุณพูดเกี่ยวกับค่าคงที่ที่ไม่ถูกต้องเมื่อคุณคว้ากุญแจเป็นครั้งที่สอง จุดดี! จะเกิดอะไรขึ้นถ้ามันเป็นล็อกการอ่าน - เขียน (เช่น ReadWriteLock ของ Java) และคุณได้รับล็อกการอ่านแล้วจากนั้นล็อคการอ่านอีกครั้งในครั้งที่สองในเธรดเดียวกัน คุณจะไม่ทำให้โมฆะคงตัวหลังจากได้รับล็อคการอ่านใช่ไหม? ดังนั้นเมื่อคุณได้รับล็อกการอ่านที่สองค่าคงที่ยังคงเป็นจริง
dgrant

1
@Jonathan Java มีการล็อคที่ไม่ใช่ reentrant ในวันนี้ใน java.util.concurrent.locks หรือไม่?
user454322

1
+1 ฉันเดาว่าการใช้ reentrant lock โดยทั่วไปนั้นอยู่ในชั้นเรียนเดียวซึ่งวิธีการบางอย่างสามารถเรียกได้จากทั้งโค้ดที่ได้รับการป้องกันและไม่ได้รับการป้องกัน สิ่งนี้สามารถแยกออกได้จริงเสมอ @ user454322 Semaphoreแน่นอน
maaartinus

1
ให้อภัยความเข้าใจผิดของฉัน แต่ฉันไม่เห็นว่าสิ่งนี้เกี่ยวข้องกับ mutex อย่างไร สมมติว่าไม่มี multithreading และล็อคที่เกี่ยวข้องอาจจะยังไม่เคยมีใครวัตถุในสถานะไม่สอดคล้องกันก่อนที่จะเรียกA::foo() A::bar()mutex เรียกซ้ำหรือไม่เกี่ยวข้องกับกรณีนี้คืออะไร
Siyuan Ren

1
@SiyuanRen: ปัญหาสามารถให้เหตุผลในท้องถิ่นเกี่ยวกับรหัส ผู้คน (อย่างน้อยฉัน) ได้รับการฝึกฝนให้รู้จักภูมิภาคที่ถูกล็อคเป็นการรักษาค่าคงที่นั่นคือในเวลาที่คุณได้รับการล็อคไม่มีเธรดอื่นกำลังแก้ไขสถานะดังนั้นค่าคงที่ในช่วงวิกฤตจะคงอยู่ นี่ไม่ใช่กฎที่ยากและคุณสามารถเขียนโค้ดด้วยค่าคงที่ที่ไม่ได้อยู่ในใจ แต่นั่นก็จะทำให้รหัสของคุณยากที่จะให้เหตุผลและรักษาไว้ สิ่งเดียวกันนี้เกิดขึ้นในโหมดเธรดเดี่ยวโดยไม่มี mutexes แต่เราไม่ได้รับการฝึกฝนให้ใช้เหตุผลในพื้นที่โดยรอบ
David Rodríguez - dribeas

92

ตามที่เขียนโดย Dave Butenhof ตัวเอง :

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


9
โปรดสังเกตส่วนสุดท้ายในการตอบกลับของ Butenhof: ...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
user454322

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

13

รูปแบบจิตที่ถูกต้องสำหรับการใช้ mutexes: Mutex ปกป้องค่าคงที่

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

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

นอกจากนี้หากคุณต้องการปกป้องค่าคงที่คุณยังอาจใช้สัญญาณเซมาฟอร์แบบไบนารีซึ่งไม่เคยเกิดขึ้นซ้ำ


จริง มีกลไกที่ดีกว่าในการปกป้องค่าคงที่
ActiveTrayPrntrTagDataStrDrvr

8
นี่ควรเป็นความเห็นต่อคำตอบที่เสนอข้อความนั้น Mutexes ไม่เพียง แต่ปกป้องข้อมูลเท่านั้น แต่ยังช่วยปกป้องค่าคงที่ ลองเขียนคอนเทนเนอร์แบบง่าย ๆ (ที่ง่ายที่สุดคือสแต็ก) ในแง่ของอะตอมมิกส์ (ซึ่งข้อมูลปกป้องตัวเอง) แทนที่จะเป็น mutexes และคุณจะเข้าใจคำสั่ง
David Rodríguez - dribeas

Mutexes ไม่ปกป้องข้อมูล แต่จะป้องกันค่าคงที่ ค่าคงที่นั้นสามารถใช้เพื่อปกป้องข้อมูลได้
Jon Hanna

4

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


4

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

หากวิธีการใช้วิธีการอื่น (เช่น: addNewArray () โทร addNewPoint () และจบด้วย recheckBounds ()) แต่ฟังก์ชั่นเหล่านั้นด้วยตนเองจำเป็นต้องล็อค mutex แล้ว recursive mutex เป็น win-win

สำหรับกรณีอื่น ๆ (การแก้ไขการเข้ารหัสที่ไม่ดีการใช้มันแม้ในวัตถุต่าง ๆ ) นั้นผิดอย่างชัดเจน!


1

สิ่งที่กลายพันธุ์ที่ไม่ใช่แบบเรียกซ้ำได้นั้นมีประโยชน์อะไร

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

pthread_mutex_t      g_mutex;

void foo()
{
    pthread_mutex_lock(&g_mutex);
    // Do something.
    pthread_mutex_unlock(&g_mutex);

    bar();
}

หากg_mutexไม่เป็น recursive รหัสดังกล่าวข้างต้นมีการประกันเพื่อการโทรbar()ด้วย mutex ปลดล็อค

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

หากg_mutexเรียกซ้ำจะไม่มีทางทำให้แน่ใจได้ว่าปลดล็อคก่อนโทรออก

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