안녕하세요,
재재입니다.

이번 포스팅에서는,
c++ mutex control 예제 코드를 다루었습니다.

지금까지 다루어본 mutex 관련 변수들을 활용하여,
c++ mutex 제어 예제 코드를 하나하나 자세히 설명해볼게요.

(1) 요구사항

요구사항은 다음과 같이 정해보았습니다.

  1. Simple Controller class 를 생성하고, 헤더와 소스를 나누시오. (h/cpp or cc)
  2. Simple Controller class 는 bool type 의 Start/Stop method 를 가지고,
    void 타입의 thread 함수인 Process method 를 가지고 있습니다.
  3. Simple Controller class 의 Start 함수를 통해,
    worker thread 를 생성하고 Process method 를 실행하시오.
    단, 쓰레드가 정상적으로 시작되기 전까지 값을 반환해서는 안됩니다.
  4. Simple Controller class 의 Stop 함수를 통해,
    이미 시작된 경우에만 Stop 함수를 실행 할 수 있도록 하고,
    Runtime Error 를 방지하기 위해 thread 가 처리하고 있지 않은 동안에 Stop 을 실행하시오.

mutex 를 효과적으로 제어해서 의도된 동작을 수행하도록 구현해야합니다.

https://en.cppreference.com/w/cpp/thread/mutex

(2) c++ mutex 제어를 위한 헤더 (simple_controller.h)

비교적 헤더에 class 를 정의하는 것은 간단합니다.

#include <atomic>
#include <conditional_variable>
#include <thread>
#include <mutex>
class SimpleController {
public:
    SimpleController();
    ~SimpleController();
private:
    void Process(); // thread function
    bool Start();
    bool Stop();
    mutable std::mutex simple_mutex, worker_mutex;
    std::atomic<bool> is_running;
    std::atomic<bool> is_started;
    std::atomic<bool> is_processing; // thread is working now
    std::unique_ptr<std::thread> simple_worker;
    std::conditional_variable start_condv;
    std::conditional_variable stop_condv;
};

thread 는 단일 스레드만 사용 할 수 있도록,
c++ smart pointer 인 unique_ptr 을 사용했고,
conditional_variable 을 2개 설정하여 Start 와 Stop method 가,
정상적으로 호출될 때 까지 기다릴 수 있도록 하였습니다.

또한 atomic 변수를 통해 원자적 성질을 보존 할 수 있도록 하였습니다.
각각 is_running (thread 가 시작했는지),
is_started (Start method 가 정상적으로 불렸는지),
is_processing (thread 가 작업을 수행중인지) 확인할 수 있도록 설정 하였습니다.

(3) c++ mutex 제어를 위한 소스 (simple_controller.cc)

먼저, 생성자와 Start method 를 추가했습니다.

#include "simple_controller.h"
#include <cstdio>
#include <chrono>
SimpleController::SimpleController() {
    this->is_running.store(false);
    this->is_started.store(false);
    this->is_procesing.store(false);
}
SimpleController::~SimpleController() {}
bool SimpleController::Start() {
    std::unique_lock<std::mutex> simple_lock(this->simple_mutex);
    if (simple_worker) {
        // if already thread exists, early return.
        return this->is_running.load(); 
    }
    this->simple_worker = std::make_unique<std::thread>([this] { 
        Process(); // thread call Process method.
    });
    
    this->start_condv.wait(simple_lock, [this] { 
        return this->is_started.load(); // wait until is_started is true.
    });
    return this->is_running.load();
}
  1. Start method 호출 중에는 std::unique_lock 을 통해 Start method 가 불리는 동안,
    또다른 Start method 가 불릴 수 없도록 하였습니다.
  2. simple_worker 가 이미 존재한다면 (thread 가 시작된 거고),
    다시 말해 이미 Start method 가 정상적으로 호출 됬다는 것을 의미합니다.
    따라서, is_running 의 값을 early return 합니다.
  3. simple_worker 가 존재하지 않으면 thread 를 생성해야 하고,
    start_condv 에서는 is_started 변수가 true 가 되기 전까지 기다립니다.
  4. 여기서 is_started 변수 값이 true 가 되고,
    기다리던 thread 를 깨우면 is_running 값을 반환하면서 Start method가 종료됩니다.
void SimpleController::Process() {
    // thread function
    if (!this->is_running.exchange(true)) {
        this->is_started.store(true);
        this->start_condv.notify_one(); // wake up start_cv
    }
    while (this->is_running.load()) {
        this->is_processing.store(true);
        // do your own job.
        printf("worker is processing, now.\n");
        std::this_thread::sleep_for(std::chrono::seconds(5));
        this->is_processing.store(false);
        printf("Stop can be called!\n");
        this->stop_condv.notify_all(); // wake up stop_cv
        std::this_thread::sleep_for(std::chrono::seconds(1));        
    }
}
  1. thread 함수인 Process method 가 실행되면,
    is_running 이 값이 false 인 경우 true 로 변경하고 false 를 반환한 뒤,
    is_started 가 true 가 됩니다. 이때, start_condv 를 사용해 thread 를 깨웁니다.
  2. is_running 의 값이 true 인 동안 thread 는 연산을 수행합니다.
    연산을 수행하기 전 is_processing 값을 true 로 설정하며,
    작업을 수행하고, 이후 작업이 끝난 뒤 is_processing 을 false 로 설정하며,
    stop_condv 를 깨워줍니다. (혹시, Stop method 가 불린 경우 끝날 수 있도록 하기 위함.)
bool SimpleController::Stop() {
    printf("Stop is called\n");
    if (this->is_started.load()) {
        if (this->is_running.load()) {
            std::unique_lock<std::mutex> lock(this->worker_mutex);
            this->stop_condv.wait(lock, [this] {
                return !this->is_processing.load(); // wait until is_procesing is false.
            });
            this->is_running.store(false); // thread will be stopped.
        }
        std::unique_lock<std::mutex> simple_lock(this->simple_mutex);
        // for release thread
        if (this->simple_worker) {
            if (this->simple_worker->joinable()) this->simple_worker->join();
            this->simple_worker.reset(); // release thread
        }
        return this->is_started.exchange(false); // return true and set false.
    } else {
        return false;
    }
}
  1. Stop method 가 최초 call 되면, is_started 값이 true 인지 확인합니다.
    마찬가지로, thread 가 실행 중인지 확인하며 실행중이라면 unique_lock 을 통해 제어합니다.
    여기서 사용되는 worker_mutex 는 Start method 에서 사용했던 simple_mutex 와 다른 것을 확인해야합니다.
    (worker_mutex 는 thread 를 Stop 시키는데 사용되는 mutex 입니다)
  2. unique_lock 을 통해, simple_mutex 에 대한 lock 을 수행합니다.
    이는, Start 와 Stop method 가 동시에 불리지 못하게 합니다.
    이후 thread 를 반환합니다.
  3. is_started 를 false 로 설정하면서, true 값을 반환합니다.

(4) 결론

비교적 복잡하지만, 또 간단한 형태의 Simple Controller 에 대한 코드를 살펴봤습니다.

conditional variable, atomic variable, mutable mutex 그리고 lock 을 모두 사용하는,
c++ mutex control 예제 코드를 통해 조금 더 mutex 에 대한 이해가 되었으면 하는 바램입니다.

c++ mutex 제어 더이상 어렵지 않아요!

c++ mutex control 예제 코드
태그:                                 

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다