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