본문 바로가기
공부

[C++] 스레드(thread) & 뮤텍스(mutex)

by MY블로그 2023. 8. 11.

스레드 ?

스레드는 프로세스 내에서 실행되는 작은 실행 단위 이며 한 프로세스는 여러개의 스레드를 가지고 있을 수 있습니다.

스레드는 독립적인 실행 경로를 가지고 프로세스 내의 자원을 공유합니다.

이런 특성으로 인해 프로세스의 실행 속도와 효율성을 향상 시킬 수 있습니다.

여러 작업들을 한번에 실행(병행)할 수 있으나 서로 다른 스레드 간의 동기화와 조율이 필요하게 됩니다.

동기화에 필요한 것이 뮤텍스 입니다.

하나의 운영체제에서 프로세스는 프로그램 단위 스레드는 프로그램의 작업단위

뮤텍스 ? 

뮤텍스는 상호 배제(Mutual Exclusion)를 의미하며 공유 자원에 대하여 동시 엑세스를 조절합니다.

*임계 구역(Critical Section)에 들어가는 스레드가 지원을 독점할 수 있도록 허용하며 다른 스레드는 임계 구역에 진입할 수 없도록 제어하는 기능을 합니다.(임계 구역에 대한 설명은 아래 접은글을 참조)

더보기

*임계 구역(Critical Section)

 

다중 스레드 프로그래밍에서 특정 부분의 코드를 동시에 실행하는 것이 안전하지 않은 경우를 의미합니다. 임계 구역은 공유 데이터에 대한 접근 또는 변경이 이루어지는 부분이므로 여러 스레드가 동시에 이 영역을 접근한다면 데이터의 무결성에 위험을 초래합니다.

 

임계 구역을 적절하게 보호하지 않을경우 "경쟁 상태(Race Condition)"의 문제가 발생 합니다.

경쟁 상태는 두개 이상의 스레드가 동시에 같은 자원에 접근하려하는 상황을 의미합니다.

이 문제로 인하여 데이터의 예상치 못한 변경이나 불일치가 발생할 수 있습니다.

 

간단한 예시로 변수A 가 있다가정하고 스레드1, 스레드2가 동시에 A에 접근하여 값을 변경한다고 할때에, 동시에 접근하여 변수A의 값을 가져가 스레드1에서 A의 값을 9로만들어버렸는데 나중에 작업이 끝난 스레드2에서 A의 값을 9가아닌 다른값으로 변경시켰을경우 이후 스레드1에서는 치명적인 문제가 될 수 있습니다.

 

위와 같은 상황이 없도록 임계 구역을 제어하기 위하여 뮤텍스, 세마포어, 조건 변수 등의 동기화 도구를 사용하여 여러 스레드가 동시에 접근하지 못하도록 보호하는 작업이 필요합니다.

이를 통해 임계 구역 내에서 하나의 스레드만 작업을 수행하도록 보장하며, 데이터의 무결성과 일관성을 유지할 수 있습니다.

 

즉, 어려스레드가 접근을 하더라도 순서대로 하나씩 작업할 수 있도록 정리하기위하여 뮤텍스를 사용!

뮤텍스를 사용하여 여러 스레드 간의 충돌을 방지하고 데이터 무결성을 보호할 수 있습니다.

표준 라이브러리는 std::mutex, std::recursisve_mutex, std::shared_mutex 세종류의 시간제약이 없는 mutex 클래스를제공 하고 있습니다.

mutex 클래스 에서 제공되는 메서드

  • lock() : 호출하는 측의 스레드가 락을 완전히 걸 때까지 대기합니다. 이때 대기시간에는 제한이 없으며 스레드가 막히게되는 시간을 정하려면 시간제한이 있는 mutex를 사용해야 합니다.(시간제한에 따른 mutex는 아래를 참고)
  • try_lock() : 호출하는 측의 스레드가 락을 걸도록 시도하며 만일 다른 스레드가 락을 걸었다면 호출이 즉시 리턴됩니다. 만일 락을 거는데 성공하였다면 true를 실패하였다면 false를 반환합니다.
  • unlock() : 호출하는 측의 스레드가 걸어둔 lock()을 해제 합니다. 작업이 끝난뒤 해제하지 않는다면 다른 스레드가 락을 걸 수 없게되니 꼭 필요합니다.

https://velog.io/@octo__/%EB%AE%A4%ED%85%8D%EC%8A%A4Mutex

시간제한에 따른 mutex 종류

시간 제한이 없는 뮤텍스 : std::mutex

가장 일반적인 뮤텍스입니다.

잠금(lock)을 획득한 스레드는 명시적으로 잠금을 해제하기 전까지 잠긴 상태를 계속 유지합니다.

스레드는 직접 잠금을 해제(unlock)해야 합니다.

시간 제한이 있는 뮤텍스 : std::timed_mutex, std::recursive_timed_mutex

시간 제한이 있는 뮤텍스는 잠금된 후 지정된 시간 내에 자동으로 잠금을 해제하도록 할 수 있습니다.

timed_mutex 는 lock_for, try_lock_for 의 메서드를 사용할 수 있습니다.

recursive_timed_mutex 는 재귀적인 잠금을 허용하면서도 시간제한을 둘 수 있습니다.

 

시간 제한이 있는 뮤텍스를 사용한다면 프로그래머가 미처 해제하지 못하여 무한정 잠금 상태가 되는 것을 방지하고 특정시간내에 잠금을 얻지 못한 스레드는 다른 작업을 수행할 수 있도록 할 수 있습니다.

시간 제한을 활용하는 것은 대기열 블록을 피하거나 잠금이 오래 지속 되는 것을 방지할 수 있습니다.


스레드와 뮤텍스 사용 예제

아래의 코드는 씬매니저(SceneManager)를 통하여 여러개의 씬을 관리하는 예제 코드 입니다.

#include <iostream>
#include <thread> // 스레드 사용을 위한 헤더
#include <mutex> // 뮤텍스 사용을 위한 헤더
#include <condition_variable> // 뮤텍스와 병행할 기능의 헤더
using namespace std;

class SceneManager 
{
private:
    mutex mtx;
    condition_variable cv;
    int currentScene;

public:
    SceneManager() : currentScene(1) {}

    void changeScene(int newScene) 
    {
        unique_lock<std::mutex> lock(mtx);

        cv.wait(lock, [this, newScene] 
        {
            return currentScene != newScene;
        });

        // 실제 씬 변경 로직구현 구간(예제는 단순 출력)
        cout << "Changing scene to Scene" << newScene << endl;

        currentScene = newScene;

        lock.unlock();
        cv.notify_all();
    }

    int getCurrentScene() 
    {
        lock_guard<mutex> lock(mtx);
        return currentScene;
    }
};

void playerThread(SceneManager& sceneManager, int targetScene) 
{
    while (true) 
    {
        int currentScene = sceneManager.getCurrentScene();
        if (currentScene == targetScene) 
        {
            this_thread::sleep_for(chrono::seconds(2)); // 씬 변경 시뮬레이션
            sceneManager.changeScene(targetScene % 3 + 1); // 다음 씬으로 변경
        }
    }
}

int main() 
{
    SceneManager sceneManager; // 모든씬을 관리할 씬매니저 생성

    thread thread1(playerThread, ref(sceneManager), 1);
    thread thread2(playerThread, ref(sceneManager), 2);
    thread thread3(playerThread, ref(sceneManager), 3);

    thread1.join();
    thread2.join();
    thread3.join();

    return 0;
}

SceneManager 클래스는 각 씬의 변경을 총괄하는 역할을 합니다.

 

changeScene 메서드에서는 뮤텍스의 조건 변수를 사용하여 현재의 씬과 변경하려는 씬을 비교하고 변경 되기 이전까지 대기하도록 합니다.

변경이 완료되면 다른 스레드들에게 변경을 알리기위하여 notify_all 을 호출 합니다.

notify_all 의 기능은 condition_variable로 사용할 수 있습니다.


condition_variable ?

C++ 포준 라이브러리에서 제공하는 동기화 도구 중의 하나입니다.(헤더 필요)

스레드 간의 상호작용을 조정하는 데 사용되는 클래스 이며 이를 통해 스레드들이 특정 조건을 충족할때까지 기다리거나 통지를 보낼 수 있습니다.

 

해당 기능을 사용한다면 스레드 간의 동기화를 진행 할 수 있으며, 특히 스레드 간의 작업 실행 순서를 제어하거나 데이터 공유를 조율하는데 유용한 기능입니다. 주로 뮤텍스와 함께 사용합니다.

 

condition_variable 의 주요 멤버 함수와 연산

  • wait(lock) : 다른 스레드에 의한 notify, notify_all 호출을 대기합니다. lock 객체가 전달되야하며, 해당 뮤텍스가 잠금 상태일 때 호출되어야 합니다.
  • wait_for(lock, duration) : 일정 시간동안 기다린 후 notify, notify_all 호출을 대기합니다.
  • wait_until(lock, time_point) : 특정 시간까지 기다린 후 notify, notify_all 호출을 대기합니다.
  • notify_one() : 대기 중인 스레드 중 하나를 깨웁니다.
  • notify_all() : 대기 중인 스레드 모두를 깨웁니다.

condition_variable 사용 예제

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
mutex mtx;
ondition_variable cv;
bool ready = false;

void workerThread() // read 변수가 true 가 반환될때까지 기다린 후 작업 수행
{
    unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready; });
    cout << "Worker thread is working." << endl;
}

int main() 
{
    thread thread(workerThread);

    this_thread::sleep_for(std::chrono::seconds(2)); // 메인 스레드가 일시적으로 대기

    {
        lock_guard<std::mutex> lock(mtx);
        ready = true; // ready 변수 true 설정
    }
    cv.notify_one(); // 대기중인 스레드 하나를 깨웁니다

    thread.join();

    return 0;
}

 

댓글