C , C++, C#

[C/C++] Mutex

vhxpffltm 2020. 2. 22. 22:51

CPU는 컴퓨터의 모든 연산이 발생하는 두뇌와 같은 엳할을 한다.

CPU 에서 연산을 수행하기 위해서는, CPU의 레지스터(register) 라는 곳에 데이터를 기록한 다음에 연산을 수행해야 합니다.

64 비트 컴퓨터의 경우, 레지스터의 크기들이 8 바이트에 비해 불과합니다. 뿐만 아니라 레지스터의 개수는 그리 많지않으며 일반적인 연산에서 사용되는 범용 레지스터의 경우 불과 16개이다.

 

즉, 모든 데이터들은 메모리에 저장되어 있고, 연산 할 때 할 때 마다 메모리에서 레지스터로 값을 가져온 뒤에, 빠르게 연산을 하고, 다시 메모리에 가져다 놓는 식으로 작동한다.

 

예를들면, 메모리는 냉장고 이고 CPU 의 레지스터는 도마 라고 생각하자. 냉장고 (RAM) 에서 재료를 도마 위에 하나 (레지스터) 꺼내서 썰고 (연산) 다시 냉장고로 가져다 놓는 작업이다.

 

Mutex(뮤텍스)

 

한 번에 한 스레드에서만 실행하는 개념이다. 뮤텍스는 '상호배제'의 개념에서 가져온 것이다.

다음의 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
#include<thread>
#include<vector>
#include<mutex>
 
using namespace std;
 
void f(int& count, mutex& m) {
    for (int i = 1; i <= 10000; i++) {
        m.lock(); //m을 내가 쓰게 해줌 한번에 한 스레드에서만 m을 사용할 권한을 얻음
        count += 1;
        m.unlock();
    }
    
    //뮤텍스를 취득한 쓰레드가 unlock 을 하지 않으면, 다른 모든 쓰레드들이 기다리게 됩니다.
    //심지어 본인도 마찬가지로 m.lock() 을 다시 호출하게 되고, unlock 을 하지 않았기에 본인 역시 기다리게 되죠.
    //결국 아무 쓰레드도 연산을 진행하지 못하게 됩니다. 이러한 상황을 데드락(deadlock) 이라고 합니다. 
    
}
 
int main()
{
    int counter = 0;
    mutex m,m2,m3;
    //상호배제의 의미, 
    vector<thread> work;
    for (int i = 0; i < 4; i++) work.push_back(thread(f, ref(counter),ref(m))); 
    //reference로 보내기 위해 ref를 사용
    for (int i = 0; i < 4; i++) work[i].join();
    cout << "Value : " << counter << endl;
    return 0;
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
 

 

m.lock은 'm을 내가 쓰도록 한다'의 의미이고 이 코드는 한번에 한 스레드에서만 m을 사용할 권한을 얻게된다. lock()을 사용하면 m을 소유한 스레드가 unlock() 함수로 m을 반환할때까지 기다리게 된다.

 

즉 'count += 1;' 코드는 한 스레드에서만 사용되고 lock()과 unlock() 사이를 한 스레드만 유일하게 사용하기 때문에 임계 영역 이라고도 한다.

 

뮤텍스를 unlock() 하지 않으면 다른 모든 쓰레드들이 기다리게 되며 심지어 본인도 m.lock() 을 다시 호출하게 되고, unlock 을 하지 않았기에 본인 역시 기다리게 된다.

 

결국 아무 쓰레드도 연산을 진행하지 못하게 됩니다. 이러한 상황을 데드락(deadlock) 이라고한다.. 위와 같은 문제를 해결하기 위해서는 취득한 뮤텍스는 사용이 끝나면 반드시 반환을 해야한다. 

 

unique_ptr을 통해 우리는 메모리를 할당했으면 반드시 해제해야하므로 unique_ptr의 소멸자로 처리할 수 있다. 실행하면 40000의 출력값을 얻을 수 있다.

 

데드락 피하기

 

이번엔 아래의 코드를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<iostream>
#include<thread>
#include<vector>
#include<mutex>
 
using namespace std;
 
void f2(mutex& m1, mutex& m2) {
    for (int i = 1; i <= 5; i++) {
        //lock_guard<mutex> lock1(m1);
        //lock_guard<mutex> lock2(m2);
        m1.lock();
        m2.lock();
        cout << "f2!!!!!!!!!\n";
        m2.unlock();
        m1.unlock();
    }
}
void f3(mutex& m1, mutex& m2) {// 이 함수를 조정한다.
    for (int i = 1; i <= 5; i++) {
        //lock_guard<mutex> lock2(m2);
        //lock_guard<mutex> lock1(m1);
        while (true) {
            m2.lock();
            if (!m1.try_lock()) {// m1이 lock이라면 '차 뺴' 를 수행
                m2.unlock();
                continue;
            }
            cout << "f3.........\n";
            m1.unlock();
            m2.unlock();
            break;
        }//m1을 lock하는 것이 문제이다. f2가 m1을 lock한 상태에서 f3이 m1을 lock하는것이 문제이다.
    }    //try_lock() 함수를 이용하는데 lock할 수 있다면 lock을 하고 true를 리턴 그렇지 않으면 기다리는것이 아니라 false를 리턴 
}
//lock_guard<mutex> lock(m) 은 생성자에서 뮤텍스를 lock함 소멸될 때 알아서 unlock 실행
 
int main()
{
    int counter = 0;
    mutex m,m2,m3;
    thread t1(f2, ref(m2), ref(m3));    thread t2(f3, ref(m2), ref(m3));
    //기아상태의 코드, f2 에서 m2 를 lock 하기 위해서는 f3 에서 m2 를 unlock, 그러기 위해서는 f3 에서 m1 을 lock f2와 f3이 이러지도 저러지도 못함
    
    cout << "Value : " << counter << endl;
    return 0;}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
 

 

lock_guard<mutex> lock1(m1)는 뮤텍스를 인자로 받아서 생성하게 되는데, 이 때 생성자에서 뮤텍스를 lock하고 lock_guard<> 가 소멸될 때 lock 했던 뮤텍스를 unlock 하는 객체이다.

 

우선 위 코드는 lock_guard<>를 사용할때의 문제를 해결한 코드이다. lock_guard<>만으로 t1, t2를 실행하게 되면 프로그램이 끝나지 않는 결과를 초래한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void f2(mutex& m1, mutex& m2) {
    for (int i = 1; i <= 5; i++) {
        lock_guard<mutex> lock1(m1);
        lock_guard<mutex> lock2(m2);
        
    }
}
void f3(mutex& m1, mutex& m2) {// 이 함수를 조정한다.
    for (int i = 1; i <= 5; i++) {
        lock_guard<mutex> lock2(m2);
        lock_guard<mutex> lock1(m1);
    } 
}
//lock_guard<mutex> lock(m) 은 생성자에서 뮤텍스를 lock함 소멸될 때 알아서 unlock 실행
 

 

이 코드만 실행됐다고 가정해보자. 

함수 f2의 경우 m1 을 먼저 lock 한 후 m2  lock 하고 반면에 함수 f3 의 경우 m2 를 먼저 lock 한 후 m1  lock한다.

 

f2에서 m2  lock 하기 위해서는 f3 에서 m2  unlock 해야한다. 하지만 이를 위해서는 f3 에서 m1  lock 해야 한다. 그런데 이 역시 f2에서 m1  lock 하고 있기 때문에 불가능하다.

이를 해결하기 위해 lock_guard<>를 쓰지 않은 위의 코드를 실행해보자.

 

 

결과가 잘 나왔다. f2함수는 심플하게 뮤텍스를 lock하고 unlock하는 과정이다. 

f3의 경우 좀 복잡한데. 일단 m2는 문제없이 lock 할 수 있다. 하지만 m1을 lock 하는 과정이 복잡하다.

만약에 f2가 m1  lock하고 있다면 m1.lock 을 호출한 순간 서로 이도저도 못하는 상황이된다.

 

try_lock() 이라는 함수를 사용하는데, 이 함수는 만일 m1을 lock할 수 있다면 lock 을 하고 true 를 리턴합니다. 그런데 lock() 함수와는 다르게, lock 을 할 수 없다면 기다리지 않고 그냥 false 를 리턴합니다.

 

따라서 m1.try_lock()  true 를 리턴하였다면 f3 m1  m2 를 성공적으로 lock 한 상황이므로 그대로 처리하면 된다.

반면에 m1.try_lock()  false 를 리턴하였다면 f2가 이미 m1  lock 했다는 의미이고, 이 경우 f2에서 우선권을 줘야 하기 때문에 자신이 이미 얻은 m2 역시 f2 에게 제공해야 한다.

 

그 후에 while을 통해 m1  m2 모두 lock 하는 것을 성공할 때 까지 계속 시도하게 되며, 성공하게 되면 while을 종료한다.

 

이렇듯 C++에서 스레드를 관리하고 사용하는것은 복잡하며 프로그램을 잘 설계하는것이 중요하다.

 

scoped_lock

 

데드락 회피하기 위한 방법으로 C++17의 scoped_lock을 통해 아래와 같이 해결할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<iostream>
#include<thread>
#include<vector>
#include<mutex>
#include <shared_mutex>
#include <condition_variable>  // std::condition_variable
 
using namespace std;
using namespace chrono_literals;
 
mutex mut_a;
mutex mut_b;
 
static void sane_func_1()
{
    scoped_lock l {mut_a, mut_b};
    cout << "sane f1 got both mutexes" << endl;
}
 
static void sane_func_2()
{
    scoped_lock l {mut_b, mut_a};
    cout << "sane f2 got both mutexes" << endl;
}//scoped_lock은 lock_guard, unique_lock과 같게 동작, 생성자는 뮤텍스의 잠금을 수행하고 소멸자는 잠금을 해제
// 복수의 뮤텍스로 이를 처리
 
int main()
{
    {
        thread t1 {sane_func_1};
        thread t2 {sane_func_2};
 
        t1.join();
        t2.join();
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
 

 

 

다음엔 멀티 스레드에서 자주 사용되는 '생산자 소비자 패턴'에 대해 알아보자.

Refernce

https://modoocode.com/252

c++17 STL 프로그래밍

'C , C++, C#' 카테고리의 다른 글

[C#] WinForm을 이용한 계산기  (0) 2020.03.23
[C/C++] mutex 응용과 condition_value  (0) 2020.03.01
[C/C++] Thread  (0) 2020.01.19
[C/C++] Callable, std::function  (0) 2020.01.14
[C/C++] weak_ptr  (0) 2020.01.09