이전 시간에는 C++11부터 제공되는 std::thread에 관해 알아보았었는데, C++20부터는 std::thread의 개선 버전인 std::jthread (joining thread)가 등장하였다. 해당 게시글에서는 std::jthread가 기존 std::thread에 비해 어떤 기능이 추가되었는지, 그리고 이를 활용해 간단한 예시 코드를 작성해볼 예정이다.
std::jthread는 std::thread의 확장된 interface와 RAII( Resource acquisition is initialization ) 모델이 적용된 개체이다. jthread는 private 멤버로 std::stop_source( C++20 )라는 개체를 가지고 있는데 이는 후에 설명하도록 하겠다.
안전한 자원 사용과 해제 - RAII
먼저 RAII 개념에 관해 간단하게 설명하자면 개체 수명에 따른 리소스 관리를 자동으로 해주는 C++ 설계 패턴을 의미한다. 예를 들어 잠금객체의 lock 과 unlock 기능, 동적할당의 new 와 delete 키워드 등 자원을 다 활용한 후 반드시 해제를 수행해줘야 하는 개체들에 관해 소멸자에서 자동으로 해제시켜준다.
RAII가 적용된 개체로는 unique_lock, lock_guard 등이 대표적이다.
그렇다면 jthread에서는 어떤 부분에 대해 RAII 패턴이 적용이 된 것인가?
바로 join()이다. thread의 경우 join()을 호출해주지 않는다면, terminate()를 호출해 프로세스가 비정상적으로 종료되도록 구현되어있다. 이 부분에 관해 jthread는 소멸자에서 이를 호출하도록 구현을 해놓았다.
아래는 jthread의 소멸자의 코드를 간략하게 표현한 것이다.
~jthread()
{
if (joinable())
{
request_stop();
join();
}
}
이 코드에서 알 수 있 듯, jthread는 내부 멤버로 thread를 가지고 있으며 소멸자에서 join()을 호출하고 있음을 알 수 있다.
그렇기 때문에 jthread를 사용할 경우, 특수한 경우를 제외하고 join(), detach()를 별도로 호출할 필요가 없다.
이를 Automatically Joining 이라고 한다.
협력적인 스레드 중단 - Cooperative Interruption (std::stop_source)
그런데 여기서 처음보는 함수가 보인다. 바로 request_stop()이다.
jthread::request_stop()를 내부적으로 살펴보면 앞서 언급했던 std::stop_source의 request_stop()을 호출한다.
일단 함수명으로 보아, 무언가에 대해 중지 요청을 하라는 것 같다.
en.cppreference에 따르면 해당 함수의 기능이 다음과 같이 언급되어 있다.
Issues a stop request to the internal stop-state, if it has not yet already had stop requested.
이를 해석해보면 다음과 같다. 아직 중지 요청을 받지 않았을 경우, 내부 stop-state에게 중지 요청을 보낸다.
여기서 stop-state가 무엇일까?
stop-state에 관한 필자의 주관적인 해석은 비동기 작업의 취소와 관련된 개념으로 추정된다. 해당 작업이 취소 요청을 받은 상태인가? 아닌가? 정도로 생각하면 편할 것 같다.
std::stop_source는 C++20부터 등장한 thread에서 시행되는 작업의 취소와 관련해 기능을 제공해주는 개체이다.
std::stop_source와 std::stop_token는 하나의 작업에 관해 연결되어 있으며, 조건 변수(Condition Variable)와 비슷하게 동작한다.
std::stop_source & std::stop_token
stop_source와 stop_token의 핵심 함수들은 다음과 같다.
stop_source.request_stop() : 실행 작업에 관한 중지 요청을 발생
stop_source.stop_requested(): stop-state에 중지 요청을 전달했는가? 를 확인
stop_source.get_token(): 해당 stop_source와 연결된 stop_token을 반환
stop_token.stop_requested(): stop-state가 중지 요청을 전달받은 상태인가?를 확인
jthread는 본인이 전달 받은 Callable-Unit에 관해 첫 번째 인자로 자신이 멤버로 가진 stop_resource 개체와 연결된stop_token을 자동으로 넘겨준다. 그렇기 때문에 아래 예시 코드의 InterruptibleWorker 처럼 stop_token을 파라미터로 선언하면 이를 활용해 스레드 작업 도중에 안전하게 종료시킬 수 있다.
NonInterruptibleWorker처럼 stop_token을 파라미터로 선언하지 않는 경우에는 해당 기능에 관해 동작되지 않는다.
예시 코드
#include "pch.h"
#include <sstream>
using namespace std;
void NonInterruptibleWorker()
{
int32 count(0);
std::stringstream ss;
ss << std::this_thread::get_id();
uint64_t tid = std::stoull(ss.str());
while (10 > count)
{
printf(t_set t_bold t_g "[%lld]"
t_set t_bold t_y " Still Working... %d\n" t_reset, tid, count);
this_thread::sleep_for(0.5s);
++count;
}
printf(t_set t_bold t_g "[%lld]"
t_set t_bold t_c " Job Finished...\n" t_reset, tid);
}
void InterruptibleWorker(stop_token stoken)
{
int32 count(0);
std::stringstream ss;
ss << std::this_thread::get_id();
uint64_t tid = std::stoull(ss.str());
while (10 > count )
{
if (stoken.stop_requested())
{
printf(t_set t_g "[%lld]"
t_set t_bold t_r " Stop Requested\n",tid);
return;
}
printf( t_set t_g "[%lld]"
t_set t_bold t_y " Still Working... " t_reset
t_set t_bold t_p "& Stoken is possible: " t_reset "%s\n",
tid,
stoken.stop_possible() ? "True" : "False");
this_thread::sleep_for(0.5s);
++count;
}
printf(t_set t_bold t_g "[%lld]"
t_set t_bold t_c " Job Finished... %d\n" t_reset, tid,count);
}
int main()
{
jthread jthr([]
{
printf(t_set t_y "Joining Thread" t_set t_c " (C++20)\n" t_reset);
this_thread::sleep_for(5s);
});
// *** C++20 부터 지원하는 joining thread
// RAII 기능 및 확장된 인터페이스를 제공하는 특징을 지님.
// 소멸자에서 join()을 호출한다.
// join()을 호출하지 않아도 program이 terminate 되지 않는다.
// jthread는 멤버 변수로 std::stop_resource라는 친구를 가지는데, 이를 활용해
// 스레드 동작을 제어할 수 있음. Interrupt 발생 가능
//
// 소멸자에서 join을 호출하기 때문에 main 스레드 진행 동안에는 True를 리턴한다.
printf( t_set t_y "jthr.joinable: "
t_set t_p "%s\n", (jthr.joinable() ? "(True)" : "(False)"));
jthread it(InterruptibleWorker);
// 여기 보면 stop_token을 명시적으로 전달하지 않음에도 불구하고, 코드에 문제가 없다.
// 그 이유는 jthread에선 넘겨진 callable_unit에게 stop_source::get_stop_token을 내부적으로 호출해 넘겨주기 때문이다.
jthread nit(NonInterruptibleWorker);
this_thread::sleep_for(1s);
nit.request_stop(); // No Effect
it.request_stop(); // Yes Effect
// jthread::request_stop() ==> private member인 stop_resource의 request_stop 호출
}
t_set , t_c는 ASCII_ESCAPE_CODE를 매크로로 구현한 것이므로 지워서 사용하길 바란다.
실행 결과 사진
3920번 스레드(InterruptibleWorker)는 작업이 완료되지 않았음에도 불구하고 작업 중지 요청으로 인해 더 이상 카운트를 증가시키지 않는 것을 볼 수 있다.
'Modern C++' 카테고리의 다른 글
[C++] C++의 Exception Handling과 Exception Mechanism (0) | 2024.12.13 |
---|---|
[C++] chrono vs ctime (0) | 2024.11.26 |
[C++11] std::thread의 Callable Unit의 매개변수 전달 방식 (0) | 2024.07.16 |
[C++11] std::thread의 생성과 생명 주기 (0) | 2024.07.16 |