IT/C++

[C++] C++에서 std::thread를 어떻게 종료시킬 수 있을까?

wookiist 2020. 1. 18. 16:09
728x90

시작하기에 앞서, 본 포스트는
"How to terminate a C++ std::thread?" 를 번역한 글임을 밝힙니다.

 

C++11부터, C++은 스레드를 자체적으로 지원하고자 std::thread를 도입하였다. 그 이후로, C++에서 새로운 스레드를 사용하는 것은 새로운 객체를 생성하는 것만큼이나 간단해졌다. 하지만, 동작하고 있는 C++ 스레드를 동적으로 종료하는 작업은 여전히 어렵다. 특히, joined 또는 detached 상태의 스레드라면 더더욱 그러하다. 이 주제에 관해 상당히 많은 논의가 오고 갔는데, 결론은 다음의 사이트에서 찾을 수 있다.

 

“terminate 1 thread + forcefully (target thread doesn’t cooperate) + pure C++11 = No way”.

 

이 포스트를 내장 C++ API를 이용한 C++ 스레드를 종료하는 마법을 제공하고자 쓴 것은 아니다. 이 글에서 우리는 C++ 스레드를 native (OS/컴파일러) 함수를 이용할 것이다.

 

완전한 코드는 Bo Yang의 Github에서 찾을 수 있으며, CentOS 7에서 GCC 4.8.5로 빌드하였다.

 

1. std::thread 소멸자가 동작하지 않는다.

std::thread::~thread() 소멸자는 오로지 joinable 상태인 스레드만을 종료시킬 수 있다. C++에서는 스레드가 생성된 후, 그리고 join이나 detach가 호출되기 전의 스레드를 joinable하다고 한다. 따라서 joined/detached 상태인 스레드는 std::thread 소멸자를 이용해 종료시키는 것이 애초에 불가능하다.

 

다음 코드는 스레드를 동적으로 시작하고 멈추게 하는 클래스이다. 스레드의 이름과 스레드를 기록하기 위해 std::unordered_map<std::string, std::thread>를 이용하였다. 그리고 stop_thread() 메서드는 std::thread 소멸자가 직접 호출한다.

class Foo {
public:
    void sleep_for(const std::string &tname, int num)
    {
        prctl(PR_SET_NAME,tname.c_str(),0,0,0);        
        sleep(num);
    }

    void start_thread(const std::string &tname)
    {
        std::thread thrd = std::thread(&Foo::sleep_for, this, tname, 3600);
        thrd.detach();
        tm_[tname] = std::move(thrd);
        std::cout << "Thread " << tname << " created:" << std::endl;
    }

    void stop_thread(const std::string &tname)
    {
        ThreadMap::const_iterator it = tm_.find(tname);
        if (it != tm_.end()) {
            it->second.std::thread::~thread(); // thread not killed
            tm_.erase(tname);
            std::cout << "Thread " << tname << " killed:" << std::endl;
        }
    }

private:
    typedef std::unordered_map<std::string, std::thread> ThreadMap;
    ThreadMap tm_;
};

현재 동작하고 있는 스레드를 보이기 위해, 별도로 show_thread() 함수를 구성하였으며, 스레드가 시작하거나 멈출 때마다 호출된다.

void show_thread(const std::string &keyword)
{
    std::string cmd("ps -T | grep ");
    cmd += keyword;
    system(cmd.c_str());
}
 
int main()
{
    Foo foo;
    std::string keyword("test_thread");
    std::string tname1 = keyword + "1";
    std::string tname2 = keyword + "2";

    // create and kill thread 1
    foo.start_thread(tname1);
    show_thread(keyword);
    foo.stop_thread(tname1);
    show_thread(keyword);

    // create and kill thread 2
    foo.start_thread(tname2);
    show_thread(keyword);
    foo.stop_thread(tname2);
    show_thread(keyword);

    return 0;
}

상기 테스트 코드의 동작 결과는 다음과 같다.

$ g++ -Wall -std=c++11 kill_cpp_thread.cc -o kill_cpp_thread -pthread -lpthread
$ ./kill_cpp_thread
Thread test_thread1 created:
29469 29470 pts/5    00:00:00 test_thread1
Thread test_thread1 killed:
29469 29470 pts/5    00:00:00 test_thread1
Thread test_thread2 created:
29469 29470 pts/5    00:00:00 test_thread1
29469 29477 pts/5    00:00:00 test_thread2
Thread test_thread2 killed:
29469 29470 pts/5    00:00:00 test_thread1
29469 29477 pts/5    00:00:00 test_thread2

확실히, 테스트 스레드는 Foo::stop_thread() 메서드로 종료할 수 없음을 확인할 수 있다.

2. std::thread::id vs. pthread_t

OS/컴파일러 의존 함수를 이용해서 스레드를 종료시키려면, 우선 C++ std::thread로부터 native 스레드 데이터 형을 어떻게 가져올 것인지 알아야 한다. 다행히도, std::thread는 join()이나 detach()를 호출하기 전에, 해당 스레드의 native handle type을 가져올 수 있도록 native_handle() API를 제공하고 있다. 그리고 이 native handle은 pthread_cancle() 같은 native OS의 스레드 종료 함수로 넘겨진다.

 

다음은 std::thread::native_handle() 과 std::thread::get_id(), 그리고 pthread_self() 가 Linux/GCC 환경에서 C++의 스레드를 처리하기 위해 모두 같은 pthread_t를 반환한다는 것을 확인하기 위한 데모 코드이다.

#include <mutex>
#include <iostream>
#include <chrono>
#include <cstring>
#include <pthread.h>
 
std::mutex iomutex;
void f(int num)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::lock_guard<std::mutex> lk(iomutex);
    std::cout << "Thread " << num << " pthread_t " << pthread_self() << std::endl;
}
 
int main()
{
    std::thread t1(f, 1), t2(f, 2);
    
    //t1.join(); t2.join();
    //t1.detach(); t2.detach();
    
    std::cout << "Thread 1 thread id " << t1.get_id() << std::endl;
    std::cout << "Thread 2 thread id " << t2.get_id() << std::endl;
    
    std::cout << "Thread 1 native handle " << t1.native_handle() << std::endl;
    std::cout << "Thread 2 native handle " << t2.native_handle() << std::endl;
    
    t1.join(); t2.join();
    //t1.detach(); t2.detach();
}

코드를 실행시키면 다음과 같은 결과를 얻을 수 있다.

$ g++ -Wall -std=c++11 cpp_thread_pthread.cc -o cpp_thread_pthread -pthread -lpthread
$ ./cpp_thread_pthread 
Thread 1 thread id 140109390030592
Thread 2 thread id 140109381637888
Thread 1 native handle 140109390030592
Thread 2 native handle 140109381637888
Thread 1 pthread_t 140109390030592
Thread 2 pthread_t 140109381637888

그러나, join() 이나 detach()를 호출한 이후에는 C++ 스레드가 native handle type의 정보를 잃어버린다.

int main()
{
    std::thread t1(f, 1), t2(f, 2);
    
    t1.join(); t2.join();
    //t1.detach(); t2.detach();
    
    std::cout << "Thread 1 thread id " << t1.get_id() << std::endl;
    std::cout << "Thread 2 thread id " << t2.get_id() << std::endl;
    
    std::cout << "Thread 1 native handle " << t1.native_handle() << std::endl;
    std::cout << "Thread 2 native handle " << t2.native_handle() << std::endl;
}
$ ./cpp_thread_pthread
Thread 1 pthread_t 139811504355072
Thread 2 pthread_t 139811495962368
Thread 1 thread id thread::id of a non-executing thread
Thread 2 thread id thread::id of a non-executing thread
Thread 1 native handle 0
Thread 2 native handle 0

따라서, 만일 스레드 t1이 joined / detached 상태가 된 이후에 pthread_cancle(t1.native_handle()) 을 호출하게 되면, 프로그램은 분명 coredump 하고 말 것이다.

3. The Terminator (종결자)

요약하자면, 효율적으로 native 스레드 종료 함수 (예, pthread_cancel) 를 호출하기 위해서는 std::thread::join() 이나 std::thread::detach() 를 호출하기 전에 native handle 을 저장하는 작업이 필요하다는 것을 알 수 있다. 이렇게 하면 native terminator가 사용할 native handle을 언제나 올바르게 가지고 있도록 할 수 있다.

 

다음은 C++ 스레드를 종료할 수 있도록 수정한 Foo 클래스이다.

class Foo {
public:
    void sleep_for(const std::string &tname, int num)
    {
        prctl(PR_SET_NAME,tname.c_str(),0,0,0);        
        sleep(num);
    }

    void start_thread(const std::string &tname)
    {
        std::thread thrd = std::thread(&Foo::sleep_for, this, tname, 3600);
        tm_[tname] = thrd.native_handle();
        thrd.detach();
        std::cout << "Thread " << tname << " created:" << std::endl;
    }

    void stop_thread(const std::string &tname)
    {
        ThreadMap::const_iterator it = tm_.find(tname);
        if (it != tm_.end()) {
            pthread_cancel(it->second);
            tm_.erase(tname);
            std::cout << "Thread " << tname << " killed:" << std::endl;
        }
    }

private:
    typedef std::unordered_map<std::string, pthread_t> ThreadMap;
    ThreadMap tm_;
};

그리고 결과는 다음과 같다 :

$ g++ -Wall -std=c++11 kill_cpp_thread.cc -o kill_cpp_thread -pthread -lpthread
$ ./kill_cpp_thread 
Thread test_thread1 created:
30332 30333 pts/5    00:00:00 test_thread1
Thread test_thread1 killed:
Thread test_thread2 created:
30332 30340 pts/5    00:00:00 test_thread2
Thread test_thread2 killed:

근래 스레드를 사용해야하는 작업이 있어서 정리해보았다. detach() 를 하게 되면 스레드가 작업이 끝나지 않는 한 언제까지고 살아 있을 텐데, 만약 무한 루프에라도 빠지게 된다면.. 문제가 커질 것이라고 생각했다. 본 포스트를 활용해서 코드를 추가로 수정해볼 계획이다.

728x90
반응형
1 2 3