developer tip

condition_variable.notify_one ()을 호출하기 전에 잠금을 획득해야합니까?

copycodes 2020. 11. 2. 08:07
반응형

condition_variable.notify_one ()을 호출하기 전에 잠금을 획득해야합니까?


사용에 대해 약간 혼란 스럽습니다 std::condition_variable. 전화하기 전에 unique_lock를 만들어야한다는 것을 이해 합니다. 내가 찾을 수없는 것은 나는 또한 호출하기 전에 고유 잠금을 획득해야하는지 여부입니다 또는 .mutexcondition_variable.wait()notify_one()notify_all()

cppreference.com의는 상충됩니다. 예를 들어, notify_one 페이지 는 다음 예를 제공합니다.

#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(). 예제가있는 다른 페이지를 살펴보면 대부분 잠금을 획득하지 않고 다른 것을 볼 수 있습니다.

  • 을 호출하기 전에 뮤텍스를 잠그도록 선택할 수 notify_one()있으며 왜 잠그도록 선택해야합니까?
  • 주어진 예에서 왜 첫 번째에 대한 잠금이 없지만 notify_one()후속 호출에 대한 잠금이 있습니다 . 이 예가 잘못되었거나 근거가 있습니까?

를 호출 할 때 잠금을 유지할 필요는 없지만 condition_variable::notify_one()여전히 잘 정의 된 동작이고 오류가 아니라는 점에서 잘못된 것은 아닙니다.

그러나 대기중인 스레드가 실행 가능하게되면 (있는 경우) 알림 스레드가 보유하는 잠금을 즉시 획득하려고 시도하므로 "비관적"일 수 있습니다. notify_one()또는 을 호출하는 동안 조건 변수와 관련된 잠금을 유지하지 않는 것이 좋은 경험 규칙이라고 생각합니다 notify_all(). Pthread Mutex를 참조하십시오 : pthread_mutex_unlock ()은notify_one() 성능 향상에 해당하는 pthread를 호출하기 전에 잠금을 해제하는 예제에 대해 많은 시간소비 합니다.

루프 상태 확인 중에 잠금을 유지해야하므로 루프 lock()호출이 while어느 시점에서 필요하다는 점 을 명심 while (!done)하십시오. 그러나에 대한 호출을 위해 보류 할 필요는 없습니다 notify_one().


2016-02-27 : 경쟁 조건이 있는지 여부에 대한 의견의 일부 질문을 해결하기위한 대규모 업데이트는 잠금이 notify_one()호출에 도움이되지 않습니다 . 거의 2 년 전에 질문을 받았기 때문에이 업데이트가 늦었다는 것을 알고 있지만 생산자 ( signals()이 예에서)가 notify_one()소비자 ( waits()이 예에서)가 전화 를하기 직전에 호출 하는 경우 가능한 경쟁 조건에 대한 @Cookie의 질문을 해결하고 싶습니다. 전화 할 수 wait()있습니다.

핵심은 무슨 일이 일어나는가입니다. 그것은 i소비자가 할 일이 있는지 여부를 실제로 나타내는 객체입니다. condition_variable소비자가 효율적으로의 변경을 기다릴 수 있도록 단지 메커니즘입니다 i.

생산자는 업데이트 할 때 잠금을 유지 i해야하며 소비자는 확인 i하고 호출 하는 동안 잠금을 유지해야합니다 (아예 condition_variable::wait()기다려야하는 경우). 이 경우 핵심은 소비자가이 확인 및 대기를 수행 할 때 잠금을 유지하는 동일한 인스턴스 (종종 중요 섹션이라고 함) 여야 한다는 입니다. 중요 섹션은 생산자가 업데이트 i할 때와 소비자가를 확인하고 대기 할 때 유지되므로 소비자가를 확인하는 시점과 호출하는 시점 사이에 변경할 i기회가 없습니다 . 이것이 조건 변수의 적절한 사용을위한 핵심입니다.iicondition_variable::wait()

C ++ 표준에 따르면 condition_variable :: wait ()는 조건 자와 함께 호출 될 때 다음과 같이 작동합니다 (이 경우).

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

소비자가 확인할 때 발생할 수있는 두 가지 상황이 있습니다 i.

  • 경우 i다음 소비자 호출 0 cv.wait(), 다음, i여전히 0이 될 것이다 wait(lock)구현의 일부를 호출 - 그 잠금 보장하지만의 적절한 사용. 이 경우 생산자는 소비자가 호출 할 때까지 루프 condition_variable::notify_one()에서 를 호출 할 기회가 없습니다 ( 호출이 알림을 제대로 '잡기'위해 수행해야하는 모든 작업을 수행했습니다.이를 완료 할 때까지 잠금을 해제하지 않습니다.) ). 따라서이 경우 소비자는 알림을 놓칠 수 없습니다.whilecv.wait(lk, []{return i == 1;})wait()wait()

  • 경우는 i소비자의 호출이 때 이미 1 cv.wait()wait(lock)때문에 구현의 일부가 호출되지 않습니다 while (!pred())테스트는 내부 루프가 종료하게됩니다. 이 상황에서는 notify_one () 호출이 언제 발생하는지는 중요하지 않습니다. 소비자는 차단하지 않습니다.

여기에있는 예제 done는 소비자가 그것을 인식했음을 생산자 스레드에 다시 알리기 위해 변수를 사용하는 추가적인 복잡성이 i == 1있지만, done(읽기 및 수정을 위해) 모든 액세스 권한이 있기 때문에 이것이 분석을 전혀 변경하지 않는다고 생각 합니다. ) icondition_variable.

당신이 지적 @ eh9하는 질문을 보면, 동기화 표준 : 원자 및 표준 : : 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()을 누른 상태에서 at # 3이 수행되는 것을 알 수 f->resume_mutex있습니다. 그러나 wait()1 단계에서 필요한지 여부에 대한 확인은 해당 잠금을 전혀 유지하는 동안 수행 되지 않습니다 (확인 및 대기의 경우 훨씬 덜 연속적 임). 이는 조건 변수의 적절한 사용을위한 요구 사항입니다. 나는 그 코드 스 니펫에 문제가있는 사람이 그 이후 f->counterstd::atomic이것이 요구 사항을 충족시킬 것이라고 생각했다고 믿습니다 . 그러나에서 제공하는 원자 성은 std::atomic후속 호출로 확장되지 않습니다 f->resume.wait(lock). 이 예 f->counter에서는가 체크 된 시점 (1 단계)과 wait()호출 된 시점 (3 단계 ) 사이에 경쟁이 있습니다.

이 질문의 예에는 그 인종이 존재하지 않습니다.


상태

vc10 및 Boost 1.56을 사용하여이 블로그 게시물에서 제안하는 것과 거의 유사한 동시 대기열을 구현했습니다 . 작성자는 경합을 최소화하기 위해 뮤텍스를 잠금 해제합니다. 즉, notify_one()뮤텍스가 잠금 해제 된 상태로 호출됩니다.

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
}

뮤텍스 잠금 해제는 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()범위를 떠날 때 여전히 잠겨 뮤텍스로 호출, 그것은 바로 이후에 잠금 해제 :

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

즉, 적어도 내 특정 스레드 구현에서는 뮤텍스를 호출하기 전에 잠금을 해제해서는 안됩니다 boost::condition_variable::notify_one().


다른 사람들이 지적했듯이 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_onecv.waittcvtfoo

요약하면이 특정 경우의 문제는 실제로 스레딩에 관한 것이 아니라 참조로 캡처 된 변수의 수명에 관한 것입니다. cv스레드를 통해 참조로 캡처 t되므로 cv스레드 실행 기간 동안 활성 상태를 유지 해야 합니다. 여기에 제시된 다른 예는이 문제가 발생하지 않습니다. 왜냐하면 condition_variablemutex객체는 전역 범위에 정의되어 있으므로 프로그램이 종료 될 때까지 활성 상태로 유지됩니다.


@Michael Burr가 맞습니다. condition_variable::notify_one변수에 대한 잠금이 필요하지 않습니다. 예제에서 보여 주듯이 그 상황에서 잠금을 사용하는 것을 막는 것은 없습니다.

주어진 예에서 잠금은 변수의 동시 사용에 의해 동기가 부여됩니다 i. signals스레드 가 변수를 수정 하기 때문에 해당 시간 동안 다른 스레드가 변수에 액세스하지 못하도록해야합니다.

잠금은 동기화가 필요한 모든 상황에 사용됩니다 . 좀 더 일반적인 방법으로 설명 할 수는 없다고 생각합니다.


어떤 경우에는 cv가 다른 스레드에 의해 점유 (잠김) 될 수 있습니다. notify _ * () 전에 잠금을 설정하고 해제해야합니다.
그렇지 않은 경우 notify _ * ()는 전혀 실행되지 않을 수 있습니다.


받아 들인 대답이 오해의 소지가 있다고 생각하기 때문에이 대답을 추가하십시오. 실제로 notify _ * ()를 호출하기 전에 다시 잠금을 해제 할 수 있지만, 모든 경우에 스레드로부터 안전한 코드를 위해 notify_one ()을 어딘가 에서 호출하기 전에 뮤텍스를 잠 가야 합니다.

명확히하기 위해 wait ()가 lk를 잠금 해제하고 잠금이 잠기지 않은 경우 정의되지 않은 동작이되기 때문에 wait (lk)에 들어가기 전에 잠금을 가져와야합니다. 이 notify_one ()의 경우 아니지만, 당신은 당신이 전화를하지 않습니다 만들 필요가 통보 _ * () 대기 ()를 입력하기 전에 뮤텍스 잠금을 해제하는 전화를 가진; 이는 notify _ * ()를 호출하기 전에 동일한 뮤텍스를 잠 가야만 수행 할 수 있습니다.

예를 들어 다음과 같은 경우를 고려하십시오.

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);
}

경고 :이 코드에는 버그가 있습니다.

The idea is the following: threads call start() and stop() in pairs, but only as long as start() returned true. For example:

if (start())
{
  // Do stuff
  stop();
}

One (other) thread at some point will call cancel() and after returning from cancel() will destroy objects that are needed at 'Do stuff'. However, cancel() is supposed not to return while there are threads between start() and stop(), and once cancel() executed its first line, start() will always return false, so no new threads will enter the 'Do stuff' area.

Works right?

The reasoning is as follows:

1) If any thread successfully executes the first line of start() (and therefore will return true) then no thread did execute the first line of cancel() yet (we assume that the total number of threads is much smaller than 1000 by the way).

2) Also, while a thread successfully executed the first line of start(), but not yet the first line of stop() then it is impossible that any thread will successfully execute the first line of cancel() (note that only one thread ever calls cancel()): the value returned by fetch_sub(1000) will be larger than 0.

3) Once a thread executed the first line of cancel(), the first line of start() will always return false and a thread calling start() will not enter the 'Do stuff' area anymore.

4) The number of calls to start() and stop() are always balanced, so after the first line of cancel() is unsuccessfully executed, there will always be a moment where a (the last) call to stop() causes count to reach -1000 and therefore notify_one() to be called. Note that can only ever happen when the first line of cancel resulted in that thread to fall through.

Apart from a starvation problem where so many threads are calling start()/stop() that count never reaches -1000 and cancel() never returns, which one might accept as "unlikely and never lasting long", there is another bug:

It is possible that there is one thread inside the 'Do stuff' area, lets say it is just calling stop(); at that moment a thread executes the first line of cancel() reading the value 1 with the fetch_sub(1000) and falling through. But before it takes the mutex and/or does the call to wait(lk), the first thread executes the first line of stop(), reads -999 and calls cv.notify_one()!

Then this call to notify_one() is done BEFORE we are wait()-ing on the condition variable! And the program would indefinitely dead-lock.

For this reason we should not be able to call notify_one() until we called wait(). Note that the power of a condition variable lies there in that it is able to atomically unlock the mutex, check if a call to notify_one() happened and go to sleep or not. You can't fool it, but you do need to keep the mutex locked whenever you make changes to variables that might change the condition from false to true and keep it locked while calling notify_one() because of race conditions like described here.

In this example there is no condition however. Why didn't I use as condition 'count == -1000'? Because that isn't interesting at all here: as soon as -1000 is reached at all, we are sure that no new thread will enter the 'Do stuff' area. Moreover, threads can still call start() and will increment count (to -999 and -998 etc) but we don't care about that. The only thing that matters is that -1000 was reached - so that we know for sure that there are no threads anymore in the 'Do stuff' area. We are sure that this is the case when notify_one() is being called, but how to make sure we don't call notify_one() before cancel() locked its mutex? Just locking cancel_mutex shortly prior to notify_one() isn't going to help of course.

The problem is that, despite that we're not waiting for a condition, there still is a condition, and we need to lock the mutex

1) before that condition is reached 2) before we call notify_one.

The correct code therefore becomes:

void stop()
{
  if (count.fetch_sub(1) == -999) // Reached -1000 ?
  {
    cancel_mutex.lock();
    cancel_mutex.unlock();
    cv.notify_one();
  }
}

[...same start()...]

void cancel()
{
  std::unique_lock<std::mutex> lk(cancel_mutex);
  if (count.fetch_sub(1000) == 0)
    return;
  cancel_cv.wait(lk);
}

Of course this is just one example but other cases are very much alike; in almost all cases where you use a conditional variable you will need to have that mutex locked (shortly) before calling notify_one(), or else it is possible that you call it before calling wait().

Note that I unlocked the mutex prior to calling notify_one() in this case, because otherwise there is the (small) chance that the call to notify_one() wakes up the thread waiting for the condition variable which then will try to take the mutex and block, before we release the mutex again. That's just slightly slower than needed.

This example was kinda special in that the line that changes the condition is executed by the same thread that calls wait().

More usual is the case where one thread simply wait's for a condition to become true and another thread takes the lock before changing the variables involved in that condition (causing it to possibly become true). In that case the mutex is locked immediately before (and after) the condition became true - so it is totally ok to just unlock the mutex before calling notify_*() in that case.

참고URL : https://stackoverflow.com/questions/17101922/do-i-have-to-acquire-lock-before-calling-condition-variable-notify-one

반응형