#include <iostream>
#include <thread>
using namespace std;
int sharedResouce = 0;
void func()
{
for (int i = 0; i <100000; ++i) sharedResouce += 1;
}
int main()
{
int n;
std::thread tr1(func);
std::thread tr2(func);
tr1.join();
tr2.join();
cout << sharedResouce << endl;
return 0;
}
위의 코드는 스레드를 두개를 생성한뒤 각각의 스레드가 할 일을 func함수로 넣어주었습니다.
func함수는 sharedResouce의 값을 1씩 증가시키는 반복문을 10만번 반복하는 함수입니다.
그러면 sharedResouce는 값이 200,000이 되어야 할거같지만 실제로 실행하면 20만이 되지 않습니다!
왜 그런것인지 알아보기 전에 "동기화"의 개념부터 살펴보도록 하겠습니다.
동기화란? 메모리에 적재되어 있는 프로세스들 사이의 수행시기를 맞추는 것. 입니다.
프로세스들의 실행순서와 일관성을 보장하기 위해서는 "동기화"가 필수입니다.
이 프로세스들의 수행시기를 맞추는 것은 크게 두가지를 의미합니다.
- 실행 순서 제어
- 상호배제
이중에서도 일단 상호배제에 대해서 알아보도록 하겠습니다.
상호배제란? 공유가 불가능한 자원의 동시 사용을 피하기 위해 사용하는 알고리즘입니다.
예를들어 은행에 현재 10만원이 있고 프로세스 A는 현재 계좌 잔액에 2만원을 넣는 프로세스이고 프로세스 B는 현재 계좌 잔액에 5만원을 넣는 프로세스라고 가정을 해봅시다.
프로세스 A는 먼저 계좌의 잔액을 읽는다 => 읽어들인 잔액에 2만원을 더한다 => 더한 값을 저장한다.
프로세스 B는 계좌의 잔액을 읽는다 => 읽어들인 잔액에 5만원을 더한다 => 더한 값을 저장한다.
이렇게 볼 수 있습니다.
그런데 프로세스A가 먼저 실행되어 10만원이 있는 계좌 잔액을 읽은다음에 읽어들인 잔액에 2만원을 더했습니다.
그리고 더한 값을 저장하면 되는데 저장하기 전 프로세스 B가 현재 계좌 잔액을 읽어버리고(프로세스A가 아직 더한값을 저장 하지 않았기 때문에) 나서 현재 10만원 + 5만원을 진행하였습니다.
그리고 프로세스 A는 12만원을 저장하고 프로세스 B는 자신이 한 일을 하기위해 15만원을 계좌에 저장했습니다.
그러면 원래는 17만원이 저장되어야 하지만 15만원이 저장되는 상황이 발생한 것입니다.
이때문에 A와 B를 올바르게 실행하기 위해서 한 프로세스가 잔액에 접근한 경우 다른 프로세스는 기다리게 하는 것이 상호배제입니다. 그러면 프로세스A가 값을 읽고 2만원을 더하고 저장하기 전까지는 프로세스B는 현재 계좌의 잔액을 읽을 수 없습니다.
이처럼 프로세스A와 프로세스B는 "계좌의 잔액"이라는 "공동의 자원"을 두고 작업을 합니다.
이러한 자원을 "공유 자원(shared resource)"라고 합니다.
또한 이렇게 동시에 실행하면 문제가 발생하는 자원에 접근하는 코드 영역을 "임계 구역(critical section)" 이라고 합니다.
위의 예처럼 15만원이 저장되면 안되는데 잘못된 실행으로 인해 여러 프로세스가 동시 다발적으로 임계 구역의 코드를 실행하여 문제가 발생하는 것을 "레이스 컨디션(race condition)" 이라고 합니다.
이렇게 레이스 컨디션이 발생하면 데이터의 일관성이 깨지는 문제가 발생합니다.
레이스 컨디션이 발생하는 근본적인 이유는 CPU는 고급언어를 이해하지 못하고 저급 언어를 이해를 하는데
여기서 고급언어란 C++, C Python 등과같은 프로그래밍 언어이고 저급언어는 1,0 같은 이진 데이터, 어셈블리를 의미합니다.
mov eax, DWORD PTR [sharedResouce]
inc eax
mov DWORD PTR [sharedResouce], eax
ret
프로세스 A가 먼저 실행되어 sharedResouce라는 변수에 담긴 주소값을 eax레지스터에 옮긴후 eax를 inc(increase)시킨다음에 다시 mov하는 어셈블리 코드가 있다고 가정을 합니다.
그러면 이대로 실행하면 문제가 없겠지만 inc eax를 하고나서 프로세스 B가 실행되어 "문맥 교환"이 발생한다면 프로세스 B또한 sharedResouce의 주소값을 가져와 inc eax를 실행할 것입니다.
이런식으로 데이터의 일관성이 깨지는 레이스 컨디션이 발생하기 때문에 OS는 이러한 임계구역 문제를 해결하기 위해서 세가지 원칙하에 레이스 컨디션 문제를 해결합니다.
1. 상호배제 : 한 프로세스가 임계구역에 진입했다면 다른 프로세스는 임계구역에 들어갈 수 없다.
2. 진행 : 임계 구역에 어떤 프로세스도 진입하지 않았다면, 임계 구역에 진입하고자 하는 프로세스는 들어갈 수 있어야한다.
3. 유한 대기 : 한 프로세스가 임계구역에 진입하고 싶다면 그 프로세는 언젠가는 임계 구역에 들어올 수 있어야한다.
위의 세가지 원칙을 지키면서 동기화 할 수 있는 도구가 바로 "세마포(semaphore)", "뮤텍스 락(mutex lock)", "모니터(monitor)"입니다.
가장 널리 쓰이는 동기화 도구는 모니터 이지만 뮤텍스 락부터 살펴보도록 하겠습니다.
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int sharedResouce = 0;
std::mutex myMutex;
void acquire() { myMutex.lock(); }
void release() { myMutex.unlock(); }
void func()
{
// 뮤텍스 획득
acquire();
// 실행
for (int i = 0; i <100000; ++i) sharedResouce += 1;
// 뮤텍스 해제
release();
}
int main()
{
int n;
std::thread tr1(func);
std::thread tr2(func);
tr1.join();
tr2.join();
cout << sharedResouce << endl;
return 0;
}
위는 C++로 뮤텍스를 구현한 코드입니다. 조금 더 간단하게 func를 구현하면
void func()
{
std::lock_guard<std::mutex> lock(myMutex);
// std::unique_lock<std::mutex> lock(myMutex);
// 실행
for (int i = 0; i <100000; ++i) sharedResouce += 1;
// 뮤텍스 해제
release();
}
위처럼 구현할 수 있습니다. 위의 코드들을 실행하면 결과값이 20만이 나오며 예상한대로 나오게 됩니다.
여기서 acquire은 프로세스가 임계 구역에 진입하기 전에 호출하는 함수입니다. 임계구역이 잠겨있다면 임계구역이 열릴 때까지 임계구역을 반복적으로 확인합니다.
임계구역의 lock푸는 함수가 release입니다.
그런데 이렇게 미친듯이 반복하며 임계구역이 잠겼는지 안잠겼는지 확인하는 대기 방식을 "바쁜 대기"라고합니다.
바쁜대기 현상이 발생하면 성능에 당연히 안좋겠죠?
그래서 바쁜 대기 현상의 해결방법은 아니지만 뮤텍스 락의 조금더 일반화 된 버전인 세마포를 간단하게 알아보면
(바쁜 대기현상을 해결해줄 녀석(도구)는 모니터입니다.)
뮤텍스락은 하나의 공유 자원에 접근하는 프로세스를 상정한 방식이고. 세마포의 경우 여러개의 공유 자원이 있을 경우 사용할 수 있는 방법입니다.
(2개의 공유자원이 있을 경우 뮤텍스 락을 사용하는것 보다 세마포를 사용하는게 효율적입니다.)
세마포와 뮤텍스락은 비슷한데 wait, signal함수로 구현할 수 있습니다.
wait는 기다려라! signal은 가도좋다! 정도로 이해하시면 될거같습니다.
2개의 공유 자원이 있고 3개의 프로세스가 실행하려 들때 P1, P2는 실행할 수 있고 P3는 대기해야합니다.(세마포 또한 바쁜 대기현상이 발생합니다)
이를 C++로 구현하면
아래처럼 구현하여 하나의 공유자원에 대해 2개의 스레드만 통과 되도록 세마포를 구현할 수 있습니다.
공유자원이 여러개가 아니라 진정한 의미에서 세마포는 아니지만
두개의 스레드가 실행을 끝낸뒤(3초) 마지막 하나의 스레드가 실행하여 대략 6초의 실행시간이 걸리는 것을 확인할 수 있습니다.
#include <iostream>
#include <thread>
#include <mutex>
#include <semaphore>
#include <condition_variable>
using namespace std;
int sharedResource = 0;
const int max_count = 2;
std::mutex myMutex;
std::condition_variable cv;
int count = 0;
void func()
{
{
unique_lock<mutex> lock(myMutex);
cv.wait(lock, []() { return ::count < max_count; });
::count++;
}
this_thread::sleep_for(std::chrono::seconds(3));
{
unique_lock<mutex> lock(myMutex);
for (int i = 0; i < 100000; ++i)
{
sharedResource++;
}
auto tid = this_thread::get_id();
cout << tid << " 실행 완료" << endl;
::count--;
cv.notify_all();
}
}
int main()
{
std::thread t1(func);
std::thread t2(func);
std::thread t3(func);
t1.join();
t2.join();
t3.join();
cout << "sharedResource: " << sharedResource << endl;
return 0;
}
참고자료
https://velog.io/@ha0kim/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4
https://devtut.github.io/cpp/semaphore.html#semaphore-c-11
'CS' 카테고리의 다른 글
[CS] 가상 메모리와 페이징 (0) | 2023.08.15 |
---|---|
[CS] 스와핑(Swapping) (0) | 2023.08.09 |
[CS] 논리주소와 물리주소 (0) | 2023.08.07 |
[CS] Context Switching(문맥 교환) (0) | 2023.08.05 |
[CS] ALU, Control Unit, 레지스터 주소 지정방식 (0) | 2023.07.29 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!