Modern C++

[C++20] std::jthread 와 std::stop_source & std::stop_token

dev-ohdam 2024. 8. 7. 22:46

 이전 시간에는 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_sourcestop_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)는 작업이 완료되지 않았음에도 불구하고 작업 중지 요청으로 인해 더 이상 카운트를 증가시키지 않는 것을 볼 수 있다.