เพียงแค่เพิ่มคำตอบนี้เพราะฉันคิดว่าคำตอบที่ยอมรับอาจทำให้เข้าใจผิด ในทุกกรณีคุณจะต้องล็อค mutex ก่อนที่จะโทรแจ้งเตือน _one () ที่ไหนสักแห่งเพื่อให้รหัสของคุณปลอดภัยแม้ว่าคุณจะปลดล็อกอีกครั้งก่อนที่จะโทรแจ้ง _ * ()
เพื่อความชัดเจนคุณต้องทำการล็อคก่อนเข้าสู่ wait (lk) เนื่องจาก wait () ปลดล็อก lk และจะเป็นพฤติกรรมที่ไม่ได้กำหนดหากล็อคไม่ได้ล็อก นี่ไม่ใช่กรณีของ inform_one () แต่คุณต้องแน่ใจว่าคุณจะไม่โทรแจ้ง _ * () ก่อนเข้าสู่ wait () และให้สายนั้นปลดล็อก mutex ซึ่งเห็นได้ชัดว่าสามารถทำได้โดยการล็อค mutex เดียวกันก่อนที่คุณจะโทรแจ้ง _ * ()
ตัวอย่างเช่นพิจารณากรณีต่อไปนี้:
std::atomic_int count;
std::mutex cancel_mutex;
std::condition_variable cancel_cv;
void stop()
{
if (count.fetch_sub(1) == -999)
cv.notify_one();
}
bool start()
{
if (count.fetch_add(1) >= 0)
return true;
stop();
return false;
}
void cancel()
{
if (count.fetch_sub(1000) == 0)
return;
std::unique_lock<std::mutex> lk(cancel_mutex);
cancel_cv.wait(lk);
}
คำเตือน : รหัสนี้มีจุดบกพร่อง
แนวคิดมีดังต่อไปนี้: threads call start () และ stop () เป็นคู่ แต่ตราบเท่าที่ start () ส่งคืนจริง ตัวอย่างเช่น:
if (start())
{
stop();
}
เธรดหนึ่ง (อื่น ๆ ) ในบางจุดจะเรียกยกเลิก () และหลังจากกลับมาจากการยกเลิก () จะทำลายอ็อบเจ็กต์ที่ต้องการใน 'Do stuff' อย่างไรก็ตามการยกเลิก () ไม่ควรส่งคืนในขณะที่มีเธรดอยู่ระหว่าง start () และ stop () และเมื่อทำการยกเลิก () บรรทัดแรกแล้ว start () จะส่งคืนเท็จเสมอดังนั้นจะไม่มีเธรดใหม่เข้าสู่ 'Do พื้นที่สิ่งของ
ได้ผลใช่ไหม
การให้เหตุผลมีดังนี้:
1) หากเธรดใด ๆ ดำเนินการบรรทัดแรกของการเริ่มต้นสำเร็จ () (และจะคืนค่าจริง) แสดงว่ายังไม่มีเธรดใดดำเนินการบรรทัดแรกของการยกเลิก () (เราคิดว่าจำนวนเธรดทั้งหมดน้อยกว่า 1,000 เธรดมากโดย ทาง).
2) นอกจากนี้ในขณะที่เธรดดำเนินการบรรทัดแรกของการเริ่มต้น () สำเร็จ แต่ยังไม่ถึงบรรทัดแรกของการหยุด () จึงเป็นไปไม่ได้ที่เธรดใด ๆ จะดำเนินการบรรทัดแรกของการยกเลิก () ได้สำเร็จ (โปรดทราบว่าเธรดเดียวเท่านั้น เคยโทรยกเลิก ()): ค่าที่ส่งคืนโดย fetch_sub (1000) จะมากกว่า 0
3) เมื่อเธรดดำเนินการบรรทัดแรกของการยกเลิก () บรรทัดแรกของ start () จะแสดงผลเท็จเสมอและการเรียกเธรด start () จะไม่เข้าสู่พื้นที่ 'Do stuff' อีกต่อไป
4) จำนวนการโทรเพื่อเริ่มต้น () และหยุด () จะสมดุลกันเสมอดังนั้นหลังจากที่บรรทัดแรกของการยกเลิก () ดำเนินการไม่สำเร็จจะมีช่วงเวลาที่การเรียก (ครั้งสุดท้าย) หยุด () ทำให้นับ ไปถึง -1000 ดังนั้นจึงจะโทรแจ้งเตือน () โปรดทราบว่าจะเกิดขึ้นได้ก็ต่อเมื่อบรรทัดแรกของการยกเลิกส่งผลให้เธรดนั้นหลุดออกไป
นอกเหนือจากปัญหาความอดอยากที่มีเธรดจำนวนมากเรียก start () / stop () ที่นับไม่ถึง -1000 และยกเลิก () ไม่ส่งคืนซึ่งอาจยอมรับว่า
เป็นไปได้ว่ามีหนึ่งเธรดอยู่ในพื้นที่ 'Do stuff' สมมติว่ามันเป็นแค่การเรียก stop (); ในขณะนั้นเธรดจะเรียกใช้บรรทัดแรกของการยกเลิก () ที่อ่านค่า 1 ด้วย fetch_sub (1000) และผ่านไป แต่ก่อนที่จะใช้ mutex และ / หรือทำการโทรเพื่อรอ (lk) เธรดแรกจะรันบรรทัดแรกของ stop () อ่าน -999 และเรียกใช้ cv.notify_one ()!
จากนั้นการโทรไปยัง alert_one () จะเสร็จสิ้นก่อนที่เราจะรอ () - เข้าสู่ตัวแปรเงื่อนไข! และโปรแกรมจะล็อคตายไปเรื่อย ๆ
ด้วยเหตุนี้เราจึงไม่สามารถโทรไปยัง alert_one () ได้จนกว่าเราจะโทรไปที่ wait () โปรดสังเกตว่าพลังของตัวแปรเงื่อนไขอยู่ที่นั่นเพื่อที่จะสามารถปลดล็อก mutex แบบอะตอมได้ตรวจสอบว่ามีการโทรไปยัง alert_one () และเข้าสู่โหมดสลีปหรือไม่ คุณไม่สามารถหลอก แต่คุณทำจำเป็นที่จะต้องให้ mutex ล็อคเมื่อใดก็ตามที่คุณทำการเปลี่ยนแปลงตัวแปรที่อาจจะเปลี่ยนสภาพจาก false เป็นจริงและให้มันล็อคในขณะที่โทร notify_one () เนื่องจากสภาพการแข่งขันเช่นอธิบายไว้ที่นี่
ในตัวอย่างนี้ไม่มีเงื่อนไขอย่างไรก็ตาม เหตุใดฉันจึงไม่ใช้เป็นเงื่อนไข 'count == -1000' เพราะนั่นไม่น่าสนใจเลยที่นี่: ทันทีที่ถึง -1000 เลยเรามั่นใจว่าจะไม่มีเธรดใหม่เข้าสู่พื้นที่ "สิ่งที่ต้องทำ" ยิ่งไปกว่านั้นเธรดยังสามารถเรียก start () และจะเพิ่มขึ้นนับ (ถึง -999 และ -998 เป็นต้น) แต่เราไม่สนใจเรื่องนั้น สิ่งเดียวที่สำคัญคือถึง -1000 - เพื่อให้เรารู้ว่าไม่มีเธรดอีกต่อไปในพื้นที่ "Do stuff" เราแน่ใจว่าเป็นกรณีนี้เมื่อมีการเรียกใช้ inform_one () แต่จะแน่ใจได้อย่างไรว่าเราไม่โทรไปยัง alert_one () ก่อนที่จะยกเลิก () ล็อก mutex เพียงแค่ล็อคยกเลิก _mutex ก่อนที่จะแจ้งเตือน () ไม่นานจะไม่ช่วยแน่นอน
ปัญหาคือว่าแม้จะมีที่เราไม่ได้รอให้สภาพที่ยังคงเป็นเงื่อนไขและเราต้องล็อค mutex
1) ก่อนที่เงื่อนไขนั้นจะถึง 2) ก่อนที่เราจะโทรแจ้งเตือน _ วัน
รหัสที่ถูกต้องจึงกลายเป็น:
void stop()
{
if (count.fetch_sub(1) == -999)
{
cancel_mutex.lock();
cancel_mutex.unlock();
cv.notify_one();
}
}
[... เริ่มต้นเหมือนกัน () ... ]
void cancel()
{
std::unique_lock<std::mutex> lk(cancel_mutex);
if (count.fetch_sub(1000) == 0)
return;
cancel_cv.wait(lk);
}
แน่นอนว่านี่เป็นเพียงตัวอย่างเดียว แต่กรณีอื่น ๆ ก็เหมือนกันมาก ในเกือบทุกกรณีที่คุณใช้ตัวแปรตามเงื่อนไขคุณจะต้องล็อก mutex นั้น (ในไม่ช้า) ก่อนที่จะโทรแจ้งเตือน () มิฉะนั้นคุณอาจเรียกใช้ก่อนที่จะโทรไปที่ wait ()
โปรดทราบว่าฉันปลดล็อก mutex ก่อนที่จะโทรแจ้งเตือน () ในกรณีนี้เพราะมิฉะนั้นจะมีโอกาส (เล็กน้อย) ที่การโทรไปยัง alert_one () ปลุกเธรดที่รอตัวแปรเงื่อนไขซึ่งจะพยายามรับ mutex และ ก่อนที่เราจะปล่อย mutex อีกครั้ง ช้ากว่าที่จำเป็นเล็กน้อย
ตัวอย่างนี้ค่อนข้างพิเศษตรงที่บรรทัดที่เปลี่ยนเงื่อนไขจะถูกดำเนินการโดยเธรดเดียวกับที่เรียกใช้ wait ()
ปกติมากขึ้นคือกรณีที่เธรดหนึ่งเพียงแค่รอให้เงื่อนไขกลายเป็นจริงและเธรดอื่นจะทำการล็อกก่อนที่จะเปลี่ยนตัวแปรที่เกี่ยวข้องกับเงื่อนไขนั้น (ทำให้อาจกลายเป็นจริง) ในกรณีนั้น mutex จะถูกล็อคทันทีก่อน (และหลัง) เงื่อนไขจะกลายเป็นจริง - ดังนั้นจึงเป็นเรื่องปกติที่จะปลดล็อก mutex ก่อนที่จะโทรแจ้ง _ * () ในกรณีนั้น
wait morphing
การเพิ่มประสิทธิภาพ) กฎของหัวแม่มืออธิบายไว้ในลิงค์นี้: การแจ้งเตือนด้วยการล็อกจะดีกว่าในสถานการณ์ที่มีเธรดมากกว่า 2 เธรดเพื่อให้ได้ผลลัพธ์ที่คาดเดาได้มากขึ้น