스레드간에 예외를 전파하려면 어떻게해야합니까?


105

단일 스레드가 호출하는 함수가 있습니다 (이것을 메인 스레드라고합니다). 함수 본문 내에서 CPU 집약적 인 작업을 수행하기 위해 여러 작업자 스레드를 생성하고 모든 스레드가 완료 될 때까지 기다린 다음 결과를 메인 스레드에 반환합니다.

그 결과 호출자가 함수를 순진하게 사용할 수 있고 내부적으로 여러 코어를 사용할 수 있습니다.

지금까지 모두 좋아 ..

우리가 가진 문제는 예외를 다루는 것입니다. 작업자 스레드의 예외로 인해 응용 프로그램이 중단되는 것을 원하지 않습니다. 우리는 함수 호출자가 메인 스레드에서 그들을 잡을 수 있기를 원합니다. 작업자 스레드에서 예외를 포착하고이를 주 스레드로 전파하여 계속 풀리도록해야합니다.

어떻게 할 수 있습니까?

내가 생각할 수있는 최선은 :

  1. 워커 스레드 (std :: exception 및 몇 가지 자체 예외)에서 다양한 예외를 포착합니다.
  2. 예외 유형 및 메시지를 기록하십시오.
  3. 작업자 스레드에 기록 된 모든 유형의 예외를 다시 발생시키는 주 스레드에 해당하는 switch 문이 있습니다.

이것은 제한된 예외 유형 세트 만 지원한다는 명백한 단점이 있으며 새 예외 유형이 추가 될 때마다 수정이 필요합니다.

답변:


89

C ++ 11 exception_ptr은 스레드간에 예외를 전송할 수 있는 유형을 도입했습니다 .

#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept>

static std::exception_ptr teptr = nullptr;

void f()
{
    try
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        throw std::runtime_error("To be passed between threads");
    }
    catch(...)
    {
        teptr = std::current_exception();
    }
}

int main(int argc, char **argv)
{
    std::thread mythread(f);
    mythread.join();

    if (teptr) {
        try{
            std::rethrow_exception(teptr);
        }
        catch(const std::exception &ex)
        {
            std::cerr << "Thread exited with exception: " << ex.what() << "\n";
        }
    }

    return 0;
}

귀하의 경우에는 여러 작업자 스레드가 있기 때문에 exception_ptr각각에 대해 하나씩 유지해야 합니다.

exception_ptr당신이 하나 이상의 유지해야합니다 있도록 공유 PTR 같은 포인터 exception_ptr각 예외로 가리키는하거나 해제됩니다.

Microsoft 특정 : SEH 예외 ( /EHa) 를 사용하는 경우 예제 코드는 액세스 위반과 같은 SEH 예외도 전송합니다.


메인에서 생성 된 여러 스레드는 어떻습니까? 첫 번째 스레드가 예외에 도달하고 종료되면 main ()은 영원히 실행될 수있는 두 번째 스레드 join ()에서 대기합니다. main ()은 두 개의 joins () 후에 teptr을 테스트하지 않습니다. 모든 스레드가 주기적으로 전역 teptr을 확인하고 적절한 경우 종료해야하는 것 같습니다. 이 상황을 처리 할 수있는 깨끗한 방법이 있습니까?
Cosmo

75

현재 유일하게 이식 가능한 방법은 스레드간에 전송하려는 모든 유형의 예외에 대한 catch 절을 작성하고 해당 catch 절에서 정보를 저장 한 다음 나중에 예외를 다시 발생시키는 데 사용하는 것입니다. 이것은 Boost.Exception이 취하는 접근 방식 입니다.

C ++ 0x에서는를 사용하여 예외를 포착 한 catch(...)다음 std::exception_ptrusing 인스턴스에 저장할 수 std::current_exception()있습니다. 그런 다음 나중에를 사용하여 동일하거나 다른 스레드에서 다시 던질 수 있습니다 std::rethrow_exception().

Microsoft Visual Studio 2005 이상을 사용하는 경우 just :: thread C ++ 0x 스레드 라이브러리std::exception_ptr. (면책 조항 : 이것은 내 제품입니다).


7
이것은 이제 C ++ 11의 일부이며 MSVS 2010에서 지원됩니다. msdn.microsoft.com/en-us/library/dd293602.aspx를 참조 하십시오 .
Johan Råde

7
Linux의 gcc 4.4+에서도 지원됩니다.
Anthony Williams


11

당신이 C ++ (11)를 사용하는 경우, 다음 std::future이 자동적으로 트랩 예외 작업자 스레드의 상단에 그것을 만들 수 및 지점에서 부모 스레드를 통해 전달할 : 당신이 찾고있는 정확하게 할 수 std::future::get있다 전화. (뒤에서 이것은 @AnthonyWilliams의 대답에서와 똑같이 발생합니다. 이미 구현되었습니다.)

단점은 "관심을 중지"하는 표준 방법이 없다는 것입니다 std::future. 소멸자조차도 작업이 완료 될 때까지 단순히 차단됩니다. [편집 2017 : 블로킹-소멸자의 행동이 misfeature입니다 단지 에서 반환 의사 미래의 std::async당신이 어쨌든 사용해서는 안됩니다. 정상적인 선물은 소멸자에서 차단되지 않습니다. 그러나 사용중인 경우 여전히 작업을 "취소"할 수 없습니다 std::future. 약속 이행 작업은 아무도 더 이상 대답을 듣지 않더라도 뒤에서 계속 실행됩니다.] 다음은 내가 무엇을 명확히 할 수있는 장난감 예제입니다. 평균:

#include <atomic>
#include <chrono>
#include <exception>
#include <future>
#include <thread>
#include <vector>
#include <stdio.h>

bool is_prime(int n)
{
    if (n == 1010) {
        puts("is_prime(1010) throws an exception");
        throw std::logic_error("1010");
    }
    /* We actually want this loop to run slowly, for demonstration purposes. */
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    for (int i=2; i < n; ++i) { if (n % i == 0) return false; }
    return (n >= 2);
}

int worker()
{
    static std::atomic<int> hundreds(0);
    const int start = 100 * hundreds++;
    const int end = start + 100;
    int sum = 0;
    for (int i=start; i < end; ++i) {
        if (is_prime(i)) { printf("%d is prime\n", i); sum += i; }
    }
    return sum;
}

int spawn_workers(int N)
{
    std::vector<std::future<int>> waitables;
    for (int i=0; i < N; ++i) {
        std::future<int> f = std::async(std::launch::async, worker);
        waitables.emplace_back(std::move(f));
    }

    int sum = 0;
    for (std::future<int> &f : waitables) {
        sum += f.get();  /* may throw an exception */
    }
    return sum;
    /* But watch out! When f.get() throws an exception, we still need
     * to unwind the stack, which means destructing "waitables" and each
     * of its elements. The destructor of each std::future will block
     * as if calling this->wait(). So in fact this may not do what you
     * really want. */
}

int main()
{
    try {
        int sum = spawn_workers(100);
        printf("sum is %d\n", sum);
    } catch (std::exception &e) {
        /* This line will be printed after all the prime-number output. */
        printf("Caught %s\n", e.what());
    }
}

방금 std::threadand 를 사용하여 작업과 유사한 예제를 작성하려고 시도했지만 (libc ++ 사용) std::exception_ptr문제가 발생 std::exception_ptr하여 아직 실제로 작동하지 않았습니다. :(

[2017 년 편집 :

int main() {
    std::exception_ptr e;
    std::thread t1([&e](){
        try {
            ::operator new(-1);
        } catch (...) {
            e = std::current_exception();
        }
    });
    t1.join();
    try {
        std::rethrow_exception(e);
    } catch (const std::bad_alloc&) {
        puts("Success!");
    }
}

2013 년에 내가 뭘 잘못하고 있었는지 모르겠지만 그게 내 잘못이라고 확신한다.]


왜 창조 미래를 명명 된 f다음 할당 emplace_back합니까? 그냥 할 waitables.push_back(std::async(…));수 없습니까? 아니면 간과하고 있습니까 (컴파일되고 질문은 누출 될 수 있는지 여부이지만 어떻게 볼 수는 없습니다)?
Konrad Rudolph

1
또한 waiting 대신 선물을 중단하여 스택을 해제하는 방법 이 있습니까? "일자리 중 하나가 실패하자마자 나머지는 더 이상 중요하지 않습니다."
Konrad Rudolph

4 년이 지난 지금 제 대답은 늙지 않았습니다. :) Re "Why": 명확성을위한 것이라 생각합니다 ( async다른 것이 아닌 미래 를 반환하는 것을 보여주기 위해 ). Re "Also, is there": 아니에요 std::future.하지만 시작을 위해 전체 STL을 다시 작성하는 데 신경 쓰지 않는 경우이를 구현하는 다양한 방법에 대해서는 Sean Parent의 "Better Code : Concurrency" 또는 "Futures from Scratch" 를 참조하십시오. :) 주요 검색어는 "취소"입니다.
Quuxplusone

답장을 보내 주셔서 감사합니다. 잠시 시간을 내면 강연을 확실히 볼 것입니다.
콘라드 루돌프

1
좋은 2017 편집. 허용되는 것과 동일하지만 범위가 지정된 예외 포인터가 있습니다. 나는 그것을 맨 위에 놓고 나머지는 제거 할 것입니다.
Nathan Cooper

6

문제는 여러 스레드에서 여러 예외를받을 수 있다는 것입니다. 각 스레드는 아마도 다른 이유로 실패 할 수 있습니다.

주 스레드가 결과를 검색하기 위해 스레드가 끝나기를 기다리거나 다른 스레드의 진행 상황을 정기적으로 확인하고 공유 데이터에 대한 액세스가 동기화되기를 기다리는 중이라고 가정합니다.

간단한 솔루션

간단한 해결책은 각 스레드에서 모든 예외를 포착하여 공유 변수 (메인 스레드)에 기록하는 것입니다.

모든 스레드가 완료되면 예외로 수행 할 작업을 결정하십시오. 이것은 다른 모든 스레드가 처리를 계속했음을 의미하며, 이는 아마도 원하는 것이 아닙니다.

복잡한 솔루션

더 복잡한 솔루션은 다른 스레드에서 예외가 발생한 경우 각 스레드가 실행의 전략적 지점에서 확인하도록하는 것입니다.

스레드에서 예외가 발생하면 스레드를 종료하기 전에 예외 개체가 기본 스레드의 일부 컨테이너에 복사되고 (간단한 솔루션에서와 같이) 일부 공유 부울 변수가 true로 설정됩니다.

그리고 다른 스레드가이 부울을 테스트 할 때 실행이 중단 될 것임을 확인하고 정상적으로 중단됩니다.

모든 스레드가 중단되면 주 스레드는 필요에 따라 예외를 처리 할 수 ​​있습니다.


4

스레드에서 발생한 예외는 상위 스레드에서 catch 할 수 없습니다. 스레드는 다른 컨텍스트와 스택을 가지고 있으며 일반적으로 부모 스레드는 거기에 머물러서 자식이 끝날 때까지 기다릴 필요가 없으므로 예외를 포착 할 수 있습니다. 그 캐치에 대한 코드에는 단순히 자리가 없습니다.

try
{
  start thread();
  wait_finish( thread );
}
catch(...)
{
  // will catch exceptions generated within start and wait, 
  // but not from the thread itself
}

각 스레드 내에서 예외를 포착하고 기본 스레드의 스레드에서 종료 상태를 해석하여 필요한 예외를 다시 발생시켜야합니다.

BTW, 스레드에 catch가 없으면 스택 해제가 전혀 수행되는지 여부는 구현에 따라 다릅니다. 즉, 종료가 호출되기 전에 자동 변수의 소멸자가 호출되지 않을 수도 있습니다. 일부 컴파일러는이를 수행하지만 필수는 아닙니다.


3

작업자 스레드에서 예외를 직렬화하고,이를 다시 주 스레드로 전송하고, 역 직렬화하고, 다시 throw 할 수 있습니까? 이 작업을 수행하려면 예외가 모두 동일한 클래스 (또는 적어도 switch 문이있는 작은 클래스 집합)에서 파생되어야합니다. 또한, 그것들이 직렬화 될 수 있을지 확신이 서지 않습니다. 나는 단지 큰 소리로 생각하고 있습니다.


두 스레드가 동일한 프로세스에있는 경우 직렬화해야하는 이유는 무엇입니까?
Nawaz

1
@Nawaz는 예외가 다른 스레드에서 자동으로 사용할 수없는 스레드 로컬 변수에 대한 참조를 가지고 있기 때문입니다.
tvanfosson

2

실제로 한 스레드에서 다음 스레드로 예외를 전송하는 훌륭하고 일반적인 방법은 없습니다.

그래야만 모든 예외가 std :: exception에서 파생되는 경우, 어떻게 든 예외를 다시 throw 될 주 스레드로 보내는 최상위 일반 예외 catch를 가질 수 있습니다. 문제는 예외의 던지는 지점을 잃어버린 것입니다. 이 정보를 얻고이를 전송하기 위해 컴파일러 종속 코드를 작성할 수 있습니다.

모든 예외가 std :: exception을 상속하는 것이 아니라면 문제가 발생하고 스레드에 많은 최상위 캐치를 작성해야하지만 솔루션은 여전히 ​​유지됩니다.


1

작업자의 모든 예외 (액세스 위반과 같은 비표준 예외 포함)에 대해 일반적인 catch를 수행하고 작업자 스레드에서 메시지를 전송해야합니다 (어떤 종류의 메시징이 제자리에 있다고 가정합니까?). 예외에 대한 라이브 포인터를 포함하는 스레드를 포함하고 예외 사본을 작성하여 다시 발생시킵니다. 그런 다음 작업자는 원래 개체를 해제하고 종료 할 수 있습니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.