엔지니어 동행하기

Concurrency Programming에서 std::mutex와 std::lock_guard를 통한 Data Race 해결법 본문

Modern C++/Concurrency & Parallel

Concurrency Programming에서 std::mutex와 std::lock_guard를 통한 Data Race 해결법

엔지니어 설리번 2022. 8. 7. 16:12
반응형
Concurrency Programming에서 생길 수 있는 대표적인 문제로 Data Race가 있습니다. 이를 해결하기 위해 std::mutex를 사용하게 되는데, mutex의 operations를 그대로 사용하는 경우 개발자가 실수를 하기 쉽습니다. 따라서 std::lock_guard를 사용하면 실수를 없앨 수 있습니다. ( 그런데 이는 Concurrency Programming의 또 다른 Dead Lock이라는 문제를 유발할 수 있습니다. 이를 개선한 여러 lock manager들-unique_lock, scoped_lock, shared_lock 또한 존재합니다.) 이에 대해 자세히 하나씩 설명드리겠습니다.

 

먼저 다음 포스팅을 참고하시면 좋습니다.

2022.08.07 - [Modern C++/Concurrency & Parallel] - Concurrency & Parallel Programming과 std::thread 설명

Data Race와 Race Condition

Multi thread로 다음과 같이 3번의 실행을 하였다고 가정해 봅시다.

출처: https://www.rapitasystems.com/blog/race-condition-testing
출처: https://www.rapitasystems.com/blog/race-condition-testing

  Race Condition이란 3번의 실행은 thread가 어떤 순서로 동작하는지에 따라, 즉 thread timing에 따라 Y 값이 12, 11, 6으로 달라지는 것을 의미합니다.

  Data Race란 두 개의 thread가 하나의 공유 메모리 (Global variable Y)에 동시에 접근할 때 발생합니다. 위에서 3rd Run의 경우로 Y 값을 READ만 하면 상관이 없지만, WRITE를 하게 되면 결과 값을 보장할 수 없습니다. Race Condition의 대표적인 예가 Data Race입니다. 이론적으로는 Race Condition이 더 큰 개념이지만, 실제 C++ 개발 시에는 두 용어를 구분하지 않고 섞어서 사용하기도 합니다. 

위의 상황을 코드로 구현하면 아래와 같습니다. 

#include <iostream>
#include <thread>

int Y =5;
void plus_one()
{
  Y++;
}

void multiply_by_two()
{
  Y *= 2;
}

int main()
{
  std::thread t1(plus_one);
  std::thread t2(multiply_by_two);
  t1.join();
  t2.join();
  std::cout << "Y: " << Y;  // 12 or 11 or 6
}

 

(참고) Data Race를 찾아내는 방법

1) Compiler sanitize 옵션 

https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual

 

GitHub - google/sanitizers: AddressSanitizer, ThreadSanitizer, MemorySanitizer

AddressSanitizer, ThreadSanitizer, MemorySanitizer - GitHub - google/sanitizers: AddressSanitizer, ThreadSanitizer, MemorySanitizer

github.com

2) Polyspace

https://m.blog.naver.com/matlablove/221913344393

 

동시성 (Concurrency) 이슈를 찾아 주는 Polyspace –1. Data Race 이슈

이어지는 3개의 포스트에서는 많은 어플리케이션에서 활용 중인 Concurrency(이하 동시성) 프로그래밍 과...

blog.naver.com

 

Mutex (Mutual Exclusion, 상호 배제)

Data race를 해결하는 Synchronization 방법이 Mutex를 사용하는 것입니다. 

출처 :&nbsp;https://www.geeksforgeeks.org/mutex-lock-for-linux-thread-synchronization/

Mutex로 shared memory에 여러 Thread가 동시에 접근하는 것을 보호합니다. 따라서 mutex lock을 획득한 Thread A만 shared memory에 접근할 수 있습니다. 이후에 접근한 Thread B는 block 상태로 대기하며, Thread A가 unlock을 하고 나가야 접근할 수 있습니다.

Mutex는 다음과 같은 특징이 있습니다.

- Mutex Object는 copy나 move가 불가능합니다.

- Block 상태에 있던 Thread가 mutex lock을 얻을 때 순서가 없습니다. 즉 먼저 기다린 Thread가 먼저 mutex lock을 획득하는 것이 보장되지 않습니다. (FIFO를 위해 다른 고차원적인 방법 필요)

- Critical section : Mutex로 하나의 Thread만 접근할 수 있도록 한 지역을 의미하며, 이 지역을 최소화해야 합니다. 

 

std::mutex의 member function

1) lock : mutex lock을 획득합니다. lock 상태에서 다시 lock을 하는 것은 정의돼 있지 않습니다. 

2) unlock : mutex lock을 해제합니다. lock이 돼 있어야 unlock을 할 수 있습니다. 

3) try_lock : mutex lock을 시도하고, 성공하면 true를 실패하면 false를 return 합니다. 이미 lock이 걸려 있어 mutex lock을 획득하지 못한 경우 block 상태가 되는 것이 아니라 false를 출력하고 다음 코드를 수행합니다. 

 

std::lock_guard (중요)

std::mutex의 lock과 unlock을 사용하면, lock 상태에서 unlock을 하지 않는 버그 (Critical section 안에서 return 혹은 예외를 throw하는 경우 등)가 발생하기 쉬워 std::lock_guard를 사용합니다. 

#include <iostream>
#include <thread>
#include <mutex>

struct MInt
{
  std::mutex mtx;
  int Y =5;
}

void plus_one(MInt & mi)
{
  {
    const std::lock_guard<std::mutex> lock(mi.mtx);
    mi.Y ++;
  }
  // you can implement more code here
}

void multiply_by_two(MInt & mi)
{
  {
    const std::lock_guard<std::mutex> lock(mi.mtx);
    mi.Y *= 2;
  }
  // you can implement more code here
}

int main()
{
  MInt mi;
  std::thread t1(plus_one, std::ref(mi));
  std::thread t2(multiply_by_two, std::ref(mi));
  t1.join();
  t2.join();
  std::cout << "Y: " << Y;  // 12 or 11 (How to control Thread-Timing?)
}

lock_guard Object를 생성함과 동시에 mutex Object를 넘겨줍니다. 그러면 자동으로 아래쪽 Scope가 Critical section으로 지정되고, 해당 Scope가 끝나면 mutex lock을 보장됩니다.


https://en.cppreference.com/w/cpp/thread/lock_guard

 

std::lock_guard - cppreference.com

template< class Mutex > class lock_guard; (since C++11) The class lock_guard is a mutex wrapper that provides a convenient RAII-style mechanism for owning a mutex for the duration of a scoped block. When a lock_guard object is created, it attempts to take

en.cppreference.com

 

반응형
Comments