본문 바로가기
C++

[프로그래밍/C++] 스레드(Thread) 사용하기 (2)

by code_pie 2024. 2. 4.

컨텍스트 스위치

 

 

스레드에서 컨텍스트 스위치란? 특정 스레드를 실행하다가 다른 스레드를 실행하는 과정을 의미한다.

그렇다면 왜 중간에 다른 스레드를 실행하는 걸까?

프로그램을 실행하다 보면 동시에 여러 프로그램이 실행되는 걸 볼 수 있다.

물론 여러 코어에서 동시에 프로그램을 실행해 작업을 할 수도 있지만, 하나의 코어에서 순간적으로 A작업, B작업을 번갈아가며 실행해 동시에 실행되는 것처럼 보이게 할 수도 있다.

 

즉, 여러 작업이 동시에 실행되도록 번갈아가며 실행하기 위해 컨텍스트 스위칭을 한다고 생각하면 된다.

 

여기서 멀티 스레드를 사용할 때, 주의해야 할 점이 있다.

 

예를 들어 A=10 이고, 스레드 1은  A+=1이라는 연산을 하려고 한다.

스레드 2는 A*=2라는 연산을 하려고 한다.

스레드 1을 실행하던 도중 중간에 컨텍스트 스위칭이 일어나게 되면, 작업을 멈추게 되고 스레드 2가 실행 된다.

이때, A의 값은 변경되지 않았고 스레드 2의 작업이 완료되면 A=20이 된다.

하지만 다음에 스레드 1이 마저 실행된다면, A=11이라는 값이 저장 될 수도 있다.

(물론 어느 작업 과정에서 컨텍스트 스위칭이 일어나느냐에 따라 다른 값이 나올 수도 있다.)

 

이처럼 같은 자원을 공유하는 경우 중간에 컨텍스트 스위칭이 일어나면 결과 값을 정확하게 예상할 수 없는 문제가 생길 수 도 있다.

 

 

뮤텍스 (mutex)

 

 

이러한 문제를 해결하기 위해 자원에 대한 접근을 막는 방법 중 mutex를 사용해 보겠다.

 

mutex는 mutex.lock()로 잠그고 mutex.unlock()로 자원에 대한 잠금을 관리하기도 하지만,

lock_guard<mutex>를 사용하면 자동으로 잠금을 해제해주는 Class를 제공한다.

 

int a; 
int n;
recursive_mutex a_mutex;
{
    lock_guard<recursive_mutex> a_lock(a_mutex);
    n = a;
    a++;
}

 

위와 같이 사용할 경우 lock_guard가 포함된 {}의 자원들에 대한 잠금이 걸리게 된다.

그리고 {}가 끝나면 자원들에 대한 잠금이 해제된다.

 

이를 활용해 소수를 멀티스레드로 구하는 코드를 작성해 보자

 

#include <iostream>
#include <vector>
#include <chrono>
#include <thread>
#include <memory>
#include <mutex>

using namespace std;

const int MaxCount = 100000;
const int ThreadCount = 4;

bool IsPrimeNumber(int num) 
{
    if (num == 1) 
        return false;
    else if (num == 2) 
        return true;
    for (int i = 2; i < num; i++) 
    {
        if (num % i == 0) 
        {
            return false;
        }
    }
    return true;
}	

void main()
{

    int num = 1;
    vector<int> primes;
    recursive_mutex num_mutex;
    recursive_mutex t_mutex;
    recursive_mutex primes_mutex;

    vector<thread> threads;
    for (int i = 0; i < ThreadCount; i++) {
        threads.push_back(thread([&] //람다 함수
        {
            while(num<=MaxCount) 
            {
                int n; 
                { //자원 잠금
                    lock_guard<recursive_mutex> num_lock(num_mutex);
                    n = num;
                    num++;
                }
                if (n >= MaxCount) 
                {
                    break;
                }
                if (IsPrimeNumber(n)) 
                { //자원 잠금
                    lock_guard<recursive_mutex> primes_lock(primes_mutex);
                    primes.push_back(n);
                }
            }
        }));
    }

    for (int i=0; i<threads.size(); i++)
    {
        threads[i].join();
    }

}

 

위 코드를 보면 자원에 대한 접근을 lock_guard로 잠궈 공유된 자원 num에 대한 접근을 막고 num이 1씩 증가할 수 있도록 했다.

 

+ 여기서 사용된 recursive_mutex와 mutex의 차이점은 recursive_mutex는 한 스레드가 뮤텍스를 여러 번 반복해서 잠그는 것을 처리해 준다.

예를 들어 lock(R_mutex)로 두번 잠궜다고 가정을 하면 unlock(R_mutex)로 한 번 잠금을 해제해도 잠금은 해제되지 않는다. 한번 더 unlcok(R_mutex)를 사용해 잠금을 해제해줘야 잠금이 해제된다.

 

그리고 thread에서 사용한 람다함수를 보면 []대신 [&]를 사용한 것을 볼 수 있는데, 이는 외부 변수를 참조값으로 사용 하겠다는 의미다.

 

Thread의 Count를 늘이거나 줄이면 실제로 코드의 완료 시간이 줄어들거나 늘어나는 모습을 확인할 수 있다.

 

 

 

 

 

 

 

반응형