안녕하세요,
재재입니다.
이번 포스팅에서는,
c++ mutex control 예제 코드를 다루었습니다.
지금까지 다루어본 mutex 관련 변수들을 활용하여,
c++ mutex 제어 예제 코드를 하나하나 자세히 설명해볼게요.
(1) 요구사항
요구사항은 다음과 같이 정해보았습니다.
- Simple Controller class 를 생성하고, 헤더와 소스를 나누시오. (h/cpp or cc)
- Simple Controller class 는 bool type 의 Start/Stop method 를 가지고,
void 타입의 thread 함수인 Process method 를 가지고 있습니다. - Simple Controller class 의 Start 함수를 통해,
worker thread 를 생성하고 Process method 를 실행하시오.
단, 쓰레드가 정상적으로 시작되기 전까지 값을 반환해서는 안됩니다. - 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();
}
- Start method 호출 중에는 std::unique_lock 을 통해 Start method 가 불리는 동안,
또다른 Start method 가 불릴 수 없도록 하였습니다. - simple_worker 가 이미 존재한다면 (thread 가 시작된 거고),
다시 말해 이미 Start method 가 정상적으로 호출 됬다는 것을 의미합니다.
따라서, is_running 의 값을 early return 합니다. - simple_worker 가 존재하지 않으면 thread 를 생성해야 하고,
start_condv 에서는 is_started 변수가 true 가 되기 전까지 기다립니다. - 여기서 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));
}
}
- thread 함수인 Process method 가 실행되면,
is_running 이 값이 false 인 경우 true 로 변경하고 false 를 반환한 뒤,
is_started 가 true 가 됩니다. 이때, start_condv 를 사용해 thread 를 깨웁니다. - 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;
}
}
- Stop method 가 최초 call 되면, is_started 값이 true 인지 확인합니다.
마찬가지로, thread 가 실행 중인지 확인하며 실행중이라면 unique_lock 을 통해 제어합니다.
여기서 사용되는 worker_mutex 는 Start method 에서 사용했던 simple_mutex 와 다른 것을 확인해야합니다.
(worker_mutex 는 thread 를 Stop 시키는데 사용되는 mutex 입니다) - unique_lock 을 통해, simple_mutex 에 대한 lock 을 수행합니다.
이는, Start 와 Stop method 가 동시에 불리지 못하게 합니다.
이후 thread 를 반환합니다. - is_started 를 false 로 설정하면서, true 값을 반환합니다.
(4) 결론
비교적 복잡하지만, 또 간단한 형태의 Simple Controller 에 대한 코드를 살펴봤습니다.
conditional variable, atomic variable, mutable mutex 그리고 lock 을 모두 사용하는,
c++ mutex control 예제 코드를 통해 조금 더 mutex 에 대한 이해가 되었으면 하는 바램입니다.
c++ mutex 제어 더이상 어렵지 않아요!