본문 바로가기
프로그래밍

C++20 동시성: part-3 request_stop 및 std::jthread의 stop_token

by it-view 2022. 1. 13.
반응형

이 기사에서는 특정 상황에서 이미 실행 중인 스레드에 중지 또는 취소 신호를 보낼 수 있는 C++20의 std::jthread의 최신 기능을 살펴본다. cpp 참조에서 첫 번째 줄을 다시 인용합니다.

아티클을 읽을 수 있도록 std::를 생략하겠습니다. 그리고 라이브러리 구조의 모든 발생은 별도로 명시되지 않는 한 C++의 std 네임스페이스에 속한다는 것을 의미합니다.

1. 소개: 스레드를 공동으로 중지하는 두 가지 방법

jthread는 실행 스레드를 중지하기 위한 협력 수단을 제공하며, 이는 스레드가 중단되거나 killed²가 될 수 없으며 중지하라는 신호만 표시될 수 있음을 의미한다. 스레드에는 두 가지 방법이 있으며 두 방법 모두 std:stop_source 유형의 공유 중지 상태를 사용한다.

 

공유 정지 상태 (stop_source )와 std::stop_token의 도움으로 정지 요청이 만들어졌는지 여부를 확인할 수 있다. 만약 그것이 만들어졌다면, 스레드가 실행하는 메소드는 즉시 반환될 수 있고, 중지 요청에 대한 구현을 제공하는 책임은 프로그래머에게 있다. 그러나 stop_token이 정지 신호를 보낼 수 있는 메커니즘은 다양할 수 있으며 크게 두 가지 범주로 분류할 수 있다.

  • jthread의 내부 공유 중지 상태에 의존하여.
  • 외부 공유 정지 상태를 사용하여.

이 문서에서는 jthread의 내부 공유 중지 상태에 의존하여 jthread:request_stop 또는 jthread를 사용하여 실행 중인 jthread에 대해 중지/취소 신호를 보내는 방법을 설명합니다.:~jthread(또는 jthread의 소멸자) 따라서 이 문서에서는 jthread 에 의해 내부적으로 관리되므로 std::stop_source에 대해 설명하지 않겠다.

2. std::jthread 내부 정지 상태 사용

cpp 참조 페이지의 두 번째 단락의 첫 번째 줄은 jthread의 내부 정지 상태에 대해 다음과 같이 말한다.

 

함수 형태의 작업이 jthread의 생성자에게 전달될 때, 내부 공유 중지-sate의 중지 토큰(std::stop_token)은 함수가 stop_token을 첫 번째 매개 변수로 받아들이면 함수로 전달된다. 그러므로 jthread::request_stop()에 반응하기 위해서는 jthread가 실행하는 함수가 첫 번째 파라미터를 stop_token 로 받아들여야 한다. 이것은 정확히 cpp 참조 페이지에서 jthread에 할당되는 작업에 대해 말하는 것입니다.

2.1 함수의 첫 번째 파라미터로서 std::stop_token이 없는 첫 번째 예

jthread::request_stop 메서드가 실행 중이지만 thread t1이 완료되기 전에 메서드 request_stop이 호출되더라도 스레드가 멈추지 않는 다음 코드를 생각해 보십시오.

#include <iostream>
  #include <thread>
  #include <syncstream>
  void foo(){
      std::osyncstream syncout{std::cout};
      //execute for approx 6 seconds
      for(int secs=0; secs<6; ++secs){
                std::this_thread::sleep_for(1s);
                syncout <<"from foo: " << secs+1
                        << " seconds elapsed\n" 
                        <<   std::flush_emit;
      }
}
void stop_foo(){
      std::jthread t1{foo};               //1:
      std::this_thread::sleep_for(2s);    //2:

      for(int secs=0; secs<2; ++secs){
                std::this_thread ::sleep_for(1s);
                std::cout << "stop_foo ran for << secs+1 << " seconds\n";
      }
      t1.request_stop();                 //3:
}
int main(){stop_foo();}

위의 코드 조각에서 stop_foo 함수는 stop_foo 함수에서 1, 2, 3으로 표시된 다음 점을 주목해야 한다.

 

1: 먼저 약 6초간 실행될 수 있는 나사산 t1에서 작업 푸가 시작됩니다.

2: 다음으로 주 나사산을 2초간 정지시키고 2초간 더 작동합니다.

3: 마지막으로 실행 중인 동안 t1이 정지하라는 신호가 표시됩니다(약 4초 후).

콘솔의 출력은 다음과 같습니다.

from foo: 1 seconds elapsed
from foo: 2 seconds elapsed
from stop_foo: 1 seconds elapsed
from foo: 3 seconds elapsed
from stop_foo: 2 seconds elapsed
from foo: 4 seconds elapsed
from foo: 5 seconds elapsed
from foo: 6 seconds elapsed
 

함수 foo가 stop_token을 첫 번째 매개 변수로 사용하지 않기 때문에 t1.request_stop()에 대한 호출은 t1에서 실행되는 foo에 영향을 미치지 않기 때문에 예상된다.

2.2 std:stop_token을 함수의 첫 번째 파라미터로 사용한 첫 번째 예제

이제 같은 함수를 가지지만 약간 수정해 보겠습니다.

  • foo 함수에 대한 첫 번째 매개 변수로 stop_messages를 전달합니다.
  • 정지 토큰에서 정지 신호가 요청되었는지 쿼리하도록 foo 본문을 수정합니다.

코드의 수정된 부분은 굵게 표시되며 새 수정사항으로 출력되는 내용은 다음과 같습니다.

 
void foo(std::stop_token token){
      ...
          for(int secs=0; secs<6; ++secs){
                    ...
                         //query from time to time if stop has been requested
                            if (token.stop_requested()) {
                                         std::cout << "foo requested to stop\n";
                                         return;
                            }
          }
}
output
------
from foo: 1 seconds elapsed
from foo: 2 seconds elapsed
from stop_foo: 1 seconds elapsed
from foo: 3 seconds elapsed
from stop_foo: 2 seconds elapsed
from foo: 4 seconds elapsed
foo requested to stop

출력에서 foo는 초기 6카운트 대신 4카운트만 인쇄했으며 마지막 인쇄는 메인 스레드에서 약 4초 후에 트리거된 if -condition 내에서 이루어졌습니다.

foo의 새로운 수정으로 메인 스레드는 stop_tokenin thread t1로 신호를 보낼 수 있고, foo에서 조건이 foo, 또는 다시 말해서 foo로 반환되는지의 본체는 협력하고 일찍 반환된다. 작업 예는 컴파일러 탐색기의 이 링크에서 액세스할 수 있습니다.

2.3 jthread 소멸자의 실행 결과 정지 신호

스레드 정지 신호를 수동으로 보내는 것 외에도, 강력한 RAII는 스레드 소멸기에 구현되어 스레드가 조인할 수 있는 경우 소멸자가 정지 신호를 함수에 전송한다.

 

foo의 경우 약 6초 동안 실행하도록 프로그래밍되어 있지만 funciton stop_foo 내에서 foo를 호출하는 스레드는 약 4초 동안만 "alive"된다. 따라서 t1.request_stop()이 실행되지 않더라도 t1은 ~4초 후에 스코프를 벗어나 소멸자가 실행되어 foo가 조기에 중지됩니다. stop_foo에 대한 수정된 코드는 t1.reques_stop이 단순히 주석 처리되는 아래에 있습니다.

void stop_foo(){
      ...

          for(int secs=0; secs<2; ++secs){
                    ...
                            ...
          }
                        //t1.request_stop();
                        //t1 goes out of scope after ~ 4 seconds
                        // if(t1.joinable()) then t1.~jthread sends a stop signal to foo
          }

그리고 출력은 섹션 2.2와 동일하게 유지됩니다. 마지막 예는 컴파일러 탐색기에서 볼 수 있다.

2.4 함수 내 stop_token에 대한 stop_callback 부착

cpp 참조에 따르면 std::stop_callback은 함수를 실행하는 스레드가 request_stop(섹션 2.2 참조)을 수신하거나 스코프를 벗어날 때마다(섹션 2.3 참조) 함수 내에 콜백을 등록하는 RAII 객체 유형입니다. 중지 칼라백은 어디서나 등록할 수 있지만 동일한 작업 스레드의 공유 중지 상태와 연결되어야 합니다. 중지 콜백은 명시적으로 request_stop을 호출하는 스레드에서 호출되거나, 중지가 아직 요청되지 않은 경우 jthread의 파괴로 인해 호출된다.

 

foo 함수(비동기적으로 시작되는 작업) 내에 stop_callback을 등록함으로써 이것이 어떻게 동작하는지 간략히 살펴보겠습니다.

void foo(std::stop_token token){
      ...

         //register stop callback
          std::stop_callback cb{token, [&syncout]{
                     syncout << "stop callback executed from   
                             << std::this_thread::get_id() 
                             << "\n"<<std::flush_emit
          };

                                    for(int secs=0; secs<6; ++secs){
                                              ...

                                    }
                                    }
                                void stop_foo(){
                                     std::osyncstream syncout{std::cout};

                                     //get the thread id of stop_foo function
                                      syncout << "stop foo in thread: " 
                                              << std::this_thread::get_id() 
                                              << "\n" << std::flush_emit;
                                     ...
                                        for(int secs=0; secs<6; ++secs){...}

                                            //launch another thead and request stop from there
                                             std::jthread stop_req_thread{
                                                         [&t1, &syncout]{
                                                                     syncout << "stop_req_thread thread id: " 
                                                                             << std::this_thread::get_id() 
                                                                             << "\n" << std::flush_emit;
                                                                    t1.request_stop();
                                                         };
                                               int main(){ stop_foo();}
                                               ```

                                               위의 코드 조각에서 콜백은 foo 함수 내에 등록되며, 이는 이전과 같이 t1 스레드에서 시작된다. 흥미로운 것은 다른 스레드 stop_req_thread가 실행되어 람다 캡처에서 t1을 캡처하고 이 스레드 안에서 t1의 request_stop이 실행된다는 것이다.

                                               이로 인해 stop_callback은 thread stop_req_thread에서 실행되며, 출력에서 관찰할 수 있는 것과 같이 thread t1에 생성되었음에도 불구하고 실행된다.

                                               ```js
                                               stop foo in thread: 139771773687616
                                               thread id of foo: 139853637019392
                                               from foo: 1 seconds elapsed
                                               from foo: 2 seconds elapsed
                                               from stop_foo: 0 seconds elapsed
                                               from foo: 3 seconds elapsed
                                               from stop_foo: 1 seconds elapsed
                                               stop_req_thread id: 139771765290752
                                               foo requested to stop
                                               stop callback executed from 139771765290752
                                               ```

                                               <div class="content-ad"></div>

                                               전체 예는 컴파일러 탐색기에서 이 링크에 액세스할 수 있습니다.

                                               # 3. 요약

                                               공유 중지 상태를 사용하는 방법에는 두 가지가 있다: (1) jthread의 공유 중지 상태를 사용하는 방법 또는 (2) 공유 중지 상태를 명시적으로 생성하고 jthread의 중지 토큰에 묶는 방법. 이 글에서 우리는 첫 번째 방법을 살펴보았다.

                                               이 방법에서, 우리는 jthread::request_stop에 대한 명시적인 호출에 의해 또는 jthread의 소멸자가 실행될 때 jthread의 공유 정지 상태와 관련된 stop_token이 정지 신호 viz를 수신하는 메커니즘을 살펴보았다.

                                               각각의 경우에 실행 중인 태스크는 jthread의 생성자가 태스크와 태스크의 다른 인수를 수신할 때 jthread의 내부 공유 정지 상태에 묶이는 첫 번째 선택적 파라미터로서 stop_token을 가져야 한다.아라미터).

                                               <div class="content-ad"></div>

                                               마지막으로 stop_callback은 레이스 조건을 방지하는 RAII 측면에서 매우 다재다능한 jthread 의 공유 정지 상태로 등록될 수 있다. stop_callback이 포함된 이 기사에서 간단한 예만 살펴보았지만, C++20의 공동성 기능 4번째 호에서는 stop_callback의 사용법과 jthread를 공동으로 중지하는 두 번째 방법에 대해 자세히 살펴보겠습니다.

                                               # 4. 참고 문헌

                                               [1] 중단에 대한 설명: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0660r10.pdf의 4페이지

                                               [2] 스레드를 죽이면 실행 중인 스레드가 작업 중간에 중단되어 리소스가 누출될 수 있습니다. 또한 스레드를 죽이면 일반적으로 OS 커널을 처리해야 합니다.

                                               [3] std::stop_callback의 cpp 참조 페이지 : https://en.cppreference.com/w/cpp/thread/stop_callback

                                               <div class="content-ad"></div>

댓글