C++ - Race Condition

mohadang·2022년 10월 15일
0

C++

목록 보기
33/48
post-thumbnail
post-custom-banner

쓰레딩의 문제점

#include <thread>

void PrintMessage(const std::string& message)
{
  std::cout << message << std::endl;
}

int main()
{
  std::thread thread(PrintMessage, "message from a child thread.");

  PrintMessage("Message from a main thread");

  thread.join();

  return 0;
}
  • 출력이 섞인다...

  • 해결책) 공유자원 잠그기

##include <mutex>

void PrintMessage(const std::string& message)
{
  static std::mutex sMutex;

  sMutex.lock();// lock, unlock을 순수 호출하는 것은 나쁜 아이디어...
  std::cout << nessage << std::endl;
  sMutex.unlock();
}

int main()
{
  std::thread thread(PrintMessage, "message from a child thread.");

  PrintMessage("Message from a main thread");

  thread.join();

  return 0;
}

뮤텍스 생성자

constexpr mutex() noexcept;//뮤텍스를 만든다
mutex(const mutex&) = delete;//복사 생성자는 delete 처리됨.

std::mutext mutex;

  • std::mutex::lock()

    • 뮤텍스를 잠근다.
    • 동일한 쓰레드에서 두 번 잠그면 데드락(deadlock) 발생
      • 꼭 그렇게 해야 된다면, std::recursive_mutex를 사용, 재귀 함수 같은데에서 많이 사용
  • std::mutex::unlock()

    • 뮤텍스 잠금을 푼다
    • 현재 쓰레드에서 잠긴 적이 없을 때의 행동은 정의되지 않음.
  • 흔히 하는 실수

##include <mutex>

void PrintMessage(const std::string& message)
{
  static std::mutex sMutex;

  sMutex.lock();// lock, unlock을 순수 호출하는 것은 나쁜 아이디어...
  std::cout << nessage << std::endl;

  // 코드를 실수로 빼먹을 수 있다.
  // unlock 코드가 있더라도 위의 코드에서 중간에 return 되면 unlock을 못함
  //sMutex.unlock();
}

int main()
{
  std::thread thread(PrintMessage, "message from a child thread.");

  PrintMessage("Message from a main thread");

  thread.join();

  return 0;
}
  • 해결책 ) std::scoped_lock(C++17, 거의 언제나 해주어야 한다.)
void PrintMessage(const std::string& message)
{
  static std::mutex sMutex;

  std::scoped_lock<std::mutex> lock(sMutex);
  std::cout << nessage << std::endl;
}

std::scoped_lock

  • 매개변수로 전달된 뮤텍스(들)을 내포하는 개체를 만듬
    • 한꺼번에 lock을 걸 수 있음
    • 뮤텍스 상호 락 시도를 맊기 위헤서...
  • 개체 생성시에 뮤텍스를 잠그고 범위(scope)를 벗어나 소멸될 때 품
  • 데드락을 방지
  • C++14의 경우, std::lock_guard를 사용할 수 있으나 이 때는 뮤텍스는 하나만 전달 가능.
std::scoped_lock<std::mutex> lock(mutex);
std::scoped_lock<std::mutex, std::mutex> locks(mutex1, mutex2);
std::scoped_lock<std::mutex, std::mutex, std::mutex> locks(mutex1, mutex2, mutex3);

Ex)

##include <mutex>

void PrintMessage(const std::string& message)
{
  static std::mutex sMutex;

  {
    std::scoped_lock<std::mutex> lock(sMutex);
    std::cout << "Message from thread ID " >> std::this_thread::get_id() << std::endl;
  }

  {
    std::scoped_lock<std::mutex> lock(sMutex);
    std::cout << message << std::endl;
  }
}

int main()
{
  std::thread thread(PrintMessage, "message from a child thread.");

  PrintMessage("Message from a main thread");

  thread.join();

  return 0;
}

std::confition_variable

  • https://en.cppreference.com/w/cpp/thread/condition_variable
  • https://en.wikipedia.org/wiki/Monitor_(synchronization)
  • 이벤트 개체
  • 신호를 받을 때까지 현재 쓰레드의 실행을 멈춤(Autoset, Event)
  • notify_one(), notify_all()
    • 멈춰 놓은 쓰레드 하나 또는 전부를 다시 실행 시킴
  • wait(), wait_for(), wait_until()
    • 조건 변수의 조건을 충족시킬때까지 또는 일정 시간 동안 현재 쓰레드의 실행을 멈춤
  • std::unique_lock을 사용해야 함.
  • std::unique_lock
    • 기본적으로 scoped lock
    • 생성시에 lock을 잠그지 않을 수도 있음(두 번째 매개변수로 std::defer_lcok을 전달할 것)
    • std::recursive_mutex와 함께 써서 재귀적으로 잠글 수 있음
      • std::unique_lock<std::recursive_mutex> lock(m1);
      • std::unique_lock<std::recursive_mutex> lock2(m1);
    • 조건 변수에 쓸 수 있는 유일한 lock
  • std::condition_variable::wait(std::unique_lock<std::mutex>& lock);
    • 현재 쓰레드 뮤텍스의 잠금을 풀고 notify_one() 또는 notify_all()을 기다린다
    • 깨어나면, 뮤텍스를 다시 잠근다
    • 다시 말해, notify_xxx()가 wait()보다 먼저 호출 되 해당 쓰레드는 영원히 기다린다.
  • Ex) 하자 없어 보이는 예...
static std::mutex sQueueLock;
static std::condition_variable sEvent;
static std::queue<int> sQueue;

void Consume()
{
  while(true)
  {
    int val;
    {
      std::unique_lock<std::mutex> lock(sQueueLock);
      sEvent.wait(lock);// wait을 하는 순간 lock을 풀어준다. 그래야 다른 스레드가 event를 날리기 위해 동작 할 수 있으니까...

      val = sQueue.front();
      sQueue.pop();
    }

    std::cout << val << std::endl;
  }
}

void Produce()
{
  std::unique_lock<std::mutex> lock(sQueueLock);
  sQueue.push(1);

  // Produce가 먼저 실행 되었을 때 하자 발생, 이미 notify가 발생 되어서 Consume에서 wait이 평생 머물러 있게 됨.
  sEvent.notify_one();
}

int main()
{
  // 이 두 순서를 바뀐다고 하더라도 성공활 확률이 높을 뿐 근본적인 문제가 발생되지 않는다.
  std::thread producer(Produce);
  std::thread consumer(Consume);

  consumer.join();
  producer.join();

  return 0;
}
  • 해결책
static std::mutex sQueueLock;
static std::condition_variable sEvent;
static std::queue<int> sQueue;

void Consume()
{
  while(true)
  {
    int val;
    {
      // queue에 데이터가 있을 때에만 넘어가서 Lock이 안 걸린다.
      std::unique_lock<std::mutex> lock(sQueueLock);
      sEvent.wait(lock, []{return !sQueue.empty(); });

      val = sQueue.front();
      sQueue.pop();
    }

    std::cout << val << std::endl;
  }
}

void Produce()
{
  std::unique_lock<std::mutex> lock(sQueueLock);
  sQueue.push(1);

  sEvent.notify_one();
}

int main()
{
  std::thread producer(Produce);
  std::thread consumer(Consume);

  consumer.join();
  producer.join();

  return 0;
}
  • std::condition_variable::wait

    • void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
    • 아래와 같음
    while(!pred())
    {
      wait(lock);// 큐가 비어 있을 때만 잠든다.
    }
    
    while(sQueue.empty())
    {
      sEvent.wait(lock);
    }
    • 왜 while문일까???
    • OS 특성 탓.???
    • 두 가지 용도로 사용
      • 잘못 깨어날 위험을 줄임
      • pred()는 잠긴 두 쓰레드 모두에서 접근할 수 있는 bool 변수의 역할을 함.

Ex)

static mutex sMutex;
static condition_variable sEvent;
static int seconds;
constexpr int MAX_SECONDS = 10;

void Timer()
{
  while (true)
  {
    {
      scoped_lock<mutex> lock(sMutex);
      cout << seconds << endl;
    }

    this_thread::sleep_for(chrono::seconds(1));

    {
      unique_lock<mutex> lock(sMutex);
      seconds++;

      sEvent.notify_one();
    }
  }
}

void Resetter()
{
  while (true)
  {
    unique_lock<mutex> lock(sMutex);
    sEvent.wait(lock, [] { return seconds >= MAX_SECONDS; });

    seconds = 0;
    cout << "Reset: " << seconds << endl;
  }
}

void AutoResetTimerExample()
{
  thread timer(Timer);
  thread resetter(Resetter);

  resetter.join();
  timer.join();
}
profile
mohadang
post-custom-banner

0개의 댓글