ฉันต้องได้รับการล็อคก่อนที่จะเรียก condition_variable.notify_one () หรือไม่


90

ฉันสับสนเล็กน้อยเกี่ยวกับการใช้std::condition_variable. ฉันเข้าใจว่าฉันต้องสร้างunique_lockบนก่อนที่จะเรียกmutex condition_variable.wait()สิ่งที่ฉันไม่สามารถหาคือว่าฉันควรจะได้รับการล็อคที่ไม่ซ้ำกันก่อนที่จะเรียกหรือnotify_one()notify_all()

ตัวอย่างในcppreference.comขัดแย้งกัน ตัวอย่างเช่นหน้าแจ้งเตือนให้ตัวอย่างนี้:

#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>

std::condition_variable cv;
std::mutex cv_m;
int i = 0;
bool done = false;

void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cout << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cout << "...finished waiting. i == 1\n";
    done = true;
}

void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Notifying...\n";
    cv.notify_one();

    std::unique_lock<std::mutex> lk(cv_m);
    i = 1;
    while (!done) {
        lk.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
        lk.lock();
        std::cerr << "Notifying again...\n";
        cv.notify_one();
    }
}

int main()
{
    std::thread t1(waits), t2(signals);
    t1.join(); t2.join();
}

ที่นี่ไม่ได้รับล็อคสำหรับครั้งแรก notify_one()notify_one()แต่จะได้รับเป็นครั้งที่สอง เมื่อดูหน้าอื่น ๆ ที่มีตัวอย่างฉันเห็นสิ่งที่แตกต่างส่วนใหญ่ไม่ได้รับการล็อค

  • ฉันสามารถเลือกล็อค mutex ก่อนโทรได้หรือไม่ notify_one()หรือไม่และทำไมฉันถึงเลือกล็อคมัน
  • ในตัวอย่างที่ระบุเหตุใดจึงไม่มีการล็อกสำหรับครั้งแรกnotify_one()แต่มีการโทรในภายหลัง ตัวอย่างนี้ผิดหรือมีเหตุผลหรือไม่?

คำตอบ:


77

คุณไม่จำเป็นต้องล็อคกุญแจเมื่อโทรcondition_variable::notify_one()แต่ก็ไม่ผิดในแง่ที่ว่าพฤติกรรมนี้ยังคงกำหนดไว้อย่างดีและไม่ใช่ข้อผิดพลาด

อย่างไรก็ตามอาจเป็น "การมองในแง่ร้าย" เนื่องจากเธรดที่รอใด ๆ ที่ถูกทำให้รันได้ (ถ้ามี) จะพยายามหาล็อกที่เธรดการแจ้งเตือนเก็บไว้ในทันที ฉันคิดว่ามันเป็นกฎที่ดีของหัวแม่มือเพื่อหลีกเลี่ยงการถือครองล็อคที่เกี่ยวข้องกับตัวแปรสภาพในขณะที่โทรหรือnotify_one() notify_all()โปรดดูPthread Mutex: pthread_mutex_unlock () ใช้เวลามากสำหรับตัวอย่างที่คลายการล็อกก่อนที่จะเรียกเทียบเท่า pthread ของnotify_one()การวัดประสิทธิภาพที่ปรับปรุงแล้ว

โปรดทราบว่าlock()ในwhileบางจุดจำเป็นต้องมีการโทรในลูปเนื่องจากการล็อกจะต้องถูกระงับไว้ในระหว่างการwhile (!done)ตรวจสอบเงื่อนไขการวนซ้ำ notify_one()แต่ก็ไม่ได้ต้องการที่จะจัดขึ้นสำหรับการเรียกร้องให้


2016-02-27 : การอัปเดตขนาดใหญ่เพื่อตอบคำถามในความคิดเห็นว่ามีเงื่อนไขการแข่งขันหรือไม่การล็อกไม่ได้ช่วยในการnotify_one()โทร ฉันรู้ว่าการอัปเดตนี้ล่าช้าเนื่องจากคำถามถูกถามเมื่อเกือบสองปีที่แล้ว แต่ฉันต้องการตอบคำถามของ @ คุกกี้เกี่ยวกับสภาพการแข่งขันที่เป็นไปได้หากผู้ผลิต ( signals()ในตัวอย่างนี้) โทรnotify_one()มาก่อนผู้บริโภค ( waits()ในตัวอย่างนี้) คือ สามารถโทรwait().

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

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

มาตรฐาน C ++ กล่าวว่า condition_variable :: wait () จะทำงานดังต่อไปนี้เมื่อเรียกด้วยเพรดิเคต (เช่นในกรณีนี้):

while (!pred())
    wait(lock);

มีสองสถานการณ์ที่อาจเกิดขึ้นเมื่อผู้บริโภคตรวจสอบi:

  • ถ้าiเป็น 0 จากนั้นผู้บริโภคเรียกcv.wait()จากนั้นiจะยังคงเป็น 0 เมื่อwait(lock)ส่วนของการใช้งานถูกเรียกใช้ - การใช้ล็อคอย่างเหมาะสมช่วยให้มั่นใจ ในกรณีนี้โปรดิวเซอร์ไม่มีโอกาสที่จะเรียกcondition_variable::notify_one()ในไฟล์whileวงจนกระทั่งหลังจากที่ผู้บริโภคได้เรียกcv.wait(lk, []{return i == 1;}) (และwait()การโทรมีทุกสิ่งที่ทำมันต้องทำอย่างถูกต้อง 'จับ' a แจ้ง - wait()จะไม่ปล่อยล็อคจนกว่าจะได้ทำอย่างนั้น ). ดังนั้นในกรณีนี้ผู้บริโภคจะพลาดการแจ้งเตือนไม่ได้

  • ถ้าiเป็น 1 อยู่แล้วเมื่อผู้บริโภคโทรcv.wait()ที่wait(lock)ส่วนของการนำไปใช้งานจะไม่ถูกเรียกเนื่องจากการwhile (!pred())ทดสอบจะทำให้ลูปภายในสิ้นสุดลง ในสถานการณ์เช่นนี้ไม่สำคัญว่าเมื่อใดที่มีการโทรไปยัง alert_one () - ผู้บริโภคจะไม่ปิดกั้น

ตัวอย่างที่นี่มีความซับซ้อนเพิ่มเติมในการใช้doneตัวแปรเพื่อส่งสัญญาณกลับไปยังเธรดผู้ผลิตที่ผู้บริโภครับรู้สิ่งนั้นi == 1แต่ฉันไม่คิดว่าสิ่งนี้จะเปลี่ยนการวิเคราะห์เลยเพราะการเข้าถึงทั้งหมดdone (สำหรับทั้งการอ่านและการแก้ไข ) เสร็จสิ้นในขณะที่อยู่ในส่วนสำคัญเดียวกันที่เกี่ยวข้องiและcondition_variable.

หากคุณดูคำถามที่ @ eh9 ชี้ไป Sync นั้นไม่น่าเชื่อถือโดยใช้ std :: atomic และ std :: condition_variableคุณจะเห็นสภาวะการแข่งขัน อย่างไรก็ตามรหัสที่โพสต์ในคำถามนั้นละเมิดกฎพื้นฐานข้อหนึ่งของการใช้ตัวแปรเงื่อนไข: ไม่ได้มีส่วนสำคัญเพียงส่วนเดียวเมื่อทำการตรวจสอบและรอ

ในตัวอย่างนั้นโค้ดมีลักษณะดังนี้:

if (--f->counter == 0)      // (1)
    // we have zeroed this fence's counter, wake up everyone that waits
    f->resume.notify_all(); // (2)
else
{
    unique_lock<mutex> lock(f->resume_mutex);
    f->resume.wait(lock);   // (3)
}

คุณจะสังเกตเห็นว่าwait()ที่ # 3 f->resume_mutexจะดำเนินการในขณะที่การถือครอง แต่การตรวจสอบว่าwait()จำเป็นหรือไม่ในขั้นตอนที่ 1 นั้นไม่ได้ทำในขณะที่ถือล็อคนั้นเลย (น้อยกว่ามากสำหรับการตรวจสอบและรอ) ซึ่งเป็นข้อกำหนดสำหรับการใช้ตัวแปรเงื่อนไขอย่างเหมาะสม) ฉันเชื่อว่าผู้ที่มีปัญหากับข้อมูลโค้ดนั้นคิดว่าเนื่องจากf->counterเป็นstd::atomicประเภทนี้จะตอบสนองความต้องการได้ อย่างไรก็ตามค่าปรมาณูที่ให้มาstd::atomicจะไม่ขยายไปถึงการเรียกร้องในภายหลังf->resume.wait(lock)ไม่ขยายไปถึงการเรียกภายหลังในตัวอย่างนี้มีการแข่งขันระหว่างเวลาที่f->counterตรวจสอบ (ขั้นตอนที่ # 1) และเมื่อwait()มีการเรียกใช้ (ขั้นตอน # 3)

การแข่งขันนั้นไม่มีอยู่ในตัวอย่างของคำถามนี้


2
มันมีความหมายที่ลึกซึ้งกว่า: domaigne.com/blog/computing/…โดยเฉพาะอย่างยิ่งปัญหา pthread ที่คุณพูดถึงควรได้รับการแก้ไขโดยเวอร์ชันล่าสุดหรือเวอร์ชันที่สร้างขึ้นด้วยแฟล็กที่ถูกต้อง (เพื่อเปิดใช้งานwait morphingการเพิ่มประสิทธิภาพ) กฎของหัวแม่มืออธิบายไว้ในลิงค์นี้: การแจ้งเตือนด้วยการล็อกจะดีกว่าในสถานการณ์ที่มีเธรดมากกว่า 2 เธรดเพื่อให้ได้ผลลัพธ์ที่คาดเดาได้มากขึ้น
v.oddou

6
@Michael: the_condition_variable.wait(lock);ความเข้าใจของเราตอบสนองความต้องการของผู้บริโภคต่อการเรียกร้องในที่สุด หากไม่จำเป็นต้องล็อกเพื่อซิงโครไนซ์ผู้ผลิตและผู้บริโภค (กล่าวว่าพื้นฐานคือคิว spsc ที่ไม่มีการล็อก) การล็อกนั้นจะไม่มีจุดประสงค์ใด ๆ หากผู้ผลิตไม่ได้ล็อกไว้ สบายดีจากฉัน แต่ไม่มีความเสี่ยงสำหรับการแข่งขันที่หายาก? ถ้าผู้ผลิตไม่ได้ล็อกไว้เขาจะโทรแจ้ง alert_one ไม่ได้ในขณะที่ผู้บริโภคเพิ่งรอ จากนั้นผู้บริโภคก็กดรอและไม่ตื่น ...
คุกกี้

1
เช่นพูดในโค้ดด้านบนว่าลูกค้าอยู่ในstd::cout << "Waiting... \n";ขณะที่โปรดิวเซอร์ทำcv.notify_one();แล้วการโทรปลุกหายไป ... หรือฉันพลาดอะไรที่นี่?
คุกกี้

1
@คุกกี้. ใช่มีสภาพการแข่งขันอยู่ที่นั่น ดูstackoverflow.com/questions/20982270/…
เอ๊ะ

1
@ eh9: ประณามฉันเพิ่งพบสาเหตุของข้อผิดพลาดในการแช่แข็งรหัสของฉันเป็นครั้งคราวขอบคุณความคิดเห็นของคุณ มันเป็นเพราะกรณีนี้แน่นอนของสภาพการแข่งขัน การปลดล็อก mutex หลังจากการแจ้งเตือนสามารถแก้ไขปัญหาได้อย่างสมบูรณ์ ... ขอบคุณมาก!
galinette

10

สถานการณ์

การใช้ vc10 และ Boost 1.56 ฉันใช้คิวพร้อมกันค่อนข้างมากเหมือนที่บล็อกโพสต์นี้แนะนำ ผู้เขียนปลดล็อก mutex เพื่อลดความขัดแย้งกล่าวnotify_one()คือถูกเรียกด้วยการปลดล็อค mutex:

void push(const T& item)
{
  std::unique_lock<std::mutex> mlock(mutex_);
  queue_.push(item);
  mlock.unlock();     // unlock before notificiation to minimize mutex contention
  cond_.notify_one(); // notify one waiting thread
}

การปลดล็อก mutex ได้รับการสนับสนุนโดยตัวอย่างในเอกสาร Boost :

void prepare_data_for_processing()
{
    retrieve_data();
    prepare_data();
    {
        boost::lock_guard<boost::mutex> lock(mut);
        data_ready=true;
    }
    cond.notify_one();
}

ปัญหา

สิ่งนี้ยังนำไปสู่พฤติกรรมที่ไม่แน่นอนดังต่อไปนี้:

  • ในขณะที่notify_one()มีไม่ถูกเรียก แต่ยังcond_.wait()สามารถถูกขัดจังหวะผ่านboost::thread::interrupt()
  • ครั้งหนึ่งnotify_one()ถูกเรียกครั้งแรกcond_.wait()หยุดชะงักการรอคอยไม่สามารถสิ้นสุดลงboost::thread::interrupt()หรือboost::condition_variable::notify_*()อีกต่อไป

วิธีการแก้

การลบบรรทัดmlock.unlock()ทำให้รหัสทำงานได้ตามที่คาดไว้ (การแจ้งเตือนและการขัดจังหวะสิ้นสุดการรอ) โปรดทราบว่าnotify_one()มีการเรียกโดยที่ mutex ยังคงล็อกอยู่ระบบจะปลดล็อกทันทีเมื่อออกจากขอบเขต:

void push(const T& item)
{
  std::lock_guard<std::mutex> mlock(mutex_);
  queue_.push(item);
  cond_.notify_one(); // notify one waiting thread
}

นั่นหมายความว่าอย่างน้อยด้วยการใช้เธรดเฉพาะของฉันจะต้องไม่ปลดล็อก mutex ก่อนที่จะโทรboost::condition_variable::notify_one()แม้ว่าทั้งสองวิธีจะดูเหมือนถูกต้องก็ตาม


คุณได้รายงานปัญหานี้ไปยัง Boost.Thread หรือไม่? ไม่พบงานที่คล้ายกันที่นั่นsvn.boost.org/trac/boost/…
magras

@magras น่าเศร้าที่ฉันทำไม่ได้ไม่รู้ว่าทำไมฉันถึงไม่พิจารณาสิ่งนี้ และน่าเสียดายที่ฉันไม่ประสบความสำเร็จในการสร้างข้อผิดพลาดนี้ซ้ำโดยใช้คิวที่กล่าวถึง
Matthäus Brandl

ฉันไม่แน่ใจว่าการปลุกระบบเร็วอาจทำให้เกิดการหยุดชะงักได้อย่างไร โดยเฉพาะอย่างยิ่งถ้าคุณออกมาจาก cond_.wait () ในป๊อป () หลังจาก push () ปล่อยคิว mutex แต่ก่อนที่จะเรียกว่า alert_one () - Pop () ควรเห็นคิวไม่ว่างเปล่าและใช้รายการใหม่มากกว่า ที่รอคอย. หากคุณออกมาจาก cond_.wait () ในขณะที่ push () กำลังอัปเดตคิวควรจับล็อคโดย push () ดังนั้น pop () ควรปิดกั้นรอให้ล็อคคลายออก การปลุกก่อนกำหนดอื่น ๆ จะล็อกไว้ทำให้ push () ไม่แก้ไขคิวก่อนที่ pop () จะเรียกการรอถัดไป () ฉันพลาดอะไร?
Kevin

5

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

thread t;

void foo() {
    std::mutex m;
    std::condition_variable cv;
    bool done = false;

    t = std::thread([&]() {
        {
            std::lock_guard<std::mutex> l(m);  // (1)
            done = true;  // (2)
        }  // (3)
        cv.notify_one();  // (4)
    });  // (5)

    std::unique_lock<std::mutex> lock(m);  // (6)
    cv.wait(lock, [&done]() { return done; });  // (7)
}

void main() {
    foo();  // (8)
    t.join();  // (9)
}

สมมติว่ามีการสลับบริบทไปยังเธรดที่สร้างขึ้นใหม่tหลังจากที่เราสร้างขึ้น แต่ก่อนที่เราจะเริ่มรอตัวแปรเงื่อนไข (อยู่ระหว่าง (5) และ (6)) เธรดtได้รับการล็อก (1) ตั้งค่าตัวแปรเพรดิเคต (2) จากนั้นปลดล็อก (3) สมมติว่ามีสวิตช์บริบทอื่นอยู่ ณ จุดนี้ก่อนที่จะเรียกใช้งานnotify_one()(4) เธรดหลักได้รับการล็อก (6) และรันบรรทัด (7) เมื่อถึงจุดที่เพรดิเคตจะส่งกลับtrueและไม่มีเหตุผลที่จะต้องรอดังนั้นจึงคลายล็อกและดำเนินการต่อ fooผลตอบแทน (8) และตัวแปรในขอบเขต (รวมถึงcv) ถูกทำลาย ก่อนที่เธรดtจะเข้าร่วมเธรดหลัก (9) ได้เธรดจะต้องเสร็จสิ้นการดำเนินการดังนั้นจึงดำเนินการต่อจากจุดที่ค้างไว้เพื่อดำเนินการcv.notify_one() (4) ณ จุดcvนั้นถูกทำลายไปแล้ว!

การแก้ไขที่เป็นไปได้ในกรณีนี้คือการล็อคค้างไว้เมื่อโทรnotify_one(เช่นลบขอบเขตที่ลงท้ายด้วยบรรทัดที่ (3)) ด้วยการทำเช่นนี้เรามั่นใจว่าการtเรียกเธรดnotify_oneก่อนหน้าcv.waitนี้จะสามารถตรวจสอบตัวแปรเพรดิเคตที่ตั้งใหม่และดำเนินการต่อเนื่องจากจะต้องได้รับการล็อกซึ่งt กำลังถืออยู่เพื่อทำการตรวจสอบ ดังนั้นเราจึงมั่นใจว่าcvจะไม่ถูกเข้าถึงโดยเธรดtหลังจากfooส่งคืน

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


1

@ ไมเคิลเสี้ยนถูกต้อง condition_variable::notify_oneไม่ต้องการการล็อกตัวแปร ไม่มีอะไรป้องกันไม่ให้คุณใช้การล็อกในสถานการณ์นั้นดังตัวอย่างที่แสดงไว้

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

ล็อคใช้สำหรับสถานการณ์ใด ๆ ที่ต้องใช้การซิงโครไนซ์ฉันไม่คิดว่าเราจะระบุได้โดยทั่วไป


แน่นอน แต่ยิ่งไปกว่านั้นพวกเขายังต้องใช้ร่วมกับตัวแปรเงื่อนไขเพื่อให้รูปแบบทั้งหมดใช้งานได้จริง waitฟังก์ชั่นตัวแปรเงื่อนไขโดยเฉพาะกำลังปลดล็อคภายในการโทรและจะส่งคืนหลังจากที่ได้รับการล็อกอีกครั้งเท่านั้น หลังจากนั้นคุณสามารถตรวจสอบสภาพของคุณได้อย่างปลอดภัยเพราะคุณได้รับ "สิทธิ์ในการอ่าน" มาแล้ว waitถ้ายังไม่ได้สิ่งที่คุณกำลังรอคุณกลับไป นี่คือรูปแบบ btw ตัวอย่างนี้ไม่เคารพมัน
v.oddou

1

ในบางกรณีเมื่อเธรดอื่นอาจถูกครอบครอง (ล็อก) คุณต้องได้รับการล็อคและปล่อยก่อนที่จะแจ้ง _ * ()
ถ้าไม่เช่นนั้นการแจ้งเตือน _ * () อาจไม่ดำเนินการเลย


1

เพียงแค่เพิ่มคำตอบนี้เพราะฉันคิดว่าคำตอบที่ยอมรับอาจทำให้เข้าใจผิด ในทุกกรณีคุณจะต้องล็อค 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) // Reached -1000 ?
    cv.notify_one();
}

bool start()
{
  if (count.fetch_add(1) >= 0)
    return true;
  // Failure.
  stop();
  return false;
}

void cancel()
{
  if (count.fetch_sub(1000) == 0)  // Reached -1000?
    return;
  // Wait till count reached -1000.
  std::unique_lock<std::mutex> lk(cancel_mutex);
  cancel_cv.wait(lk);
}

คำเตือน : รหัสนี้มีจุดบกพร่อง

แนวคิดมีดังต่อไปนี้: threads call start () และ stop () เป็นคู่ แต่ตราบเท่าที่ start () ส่งคืนจริง ตัวอย่างเช่น:

if (start())
{
  // Do stuff
  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) // Reached -1000 ?
  {
    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 ก่อนที่จะโทรแจ้ง _ * () ในกรณีนั้น

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