簡體   English   中英

如何在線程之間傳播異常?

[英]How can I propagate exceptions between threads?

我們有一個單個線程調用的函數(我們將其命名為主線程)。 在函數體內,我們生成多個工作線程來進行CPU密集型工作,等待所有線程完成,然后在主線程上返回結果。

結果是調用者可以天真地使用該函數,並且在內部它將使用多個核心。

到目前為止都很好..

我們遇到的問題是處理異常。 我們不希望工作線程上的異常使應用程序崩潰。 我們希望函數的調用者能夠在主線程上捕獲它們。 我們必須捕獲工作線程上的異常,並將它們傳播到主線程,讓它們繼續從那里展開。

我們應該怎么做?

我能想到的最好的是:

  1. 在我們的工作線程上捕獲各種異常(std :: exception和我們自己的一些)。
  2. 記錄異常的類型和消息。
  3. 在主線程上有一個相應的switch語句,它重新拋出工作線程上記錄的任何類型的異常。

這有一個明顯的缺點,即只支持一組有限的異常類型,並且每當添加新的異常類型時都需要修改。

目前,唯一可移植的方法是為您可能希望在線程之間傳輸的所有類型的異常編寫catch子句,將信息存儲在該catch子句的某處,然后在以后使用它來重新拋出異常。 這是Boost.Exception采用的方法。

在C ++ 0x中,您將能夠使用catch(...)捕獲異常,然后使用std::current_exception()將其存儲在std::exception_ptr的實例中。 然后,您可以使用std::rethrow_exception()從相同或不同的線程重新拋出它。

如果您使用的是Microsoft Visual Studio 2005或更高版本,那么just :: thread C ++ 0x線程庫支持std::exception_ptr (免責聲明:這是我的產品)。

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例外,例如訪問沖突,這可能不是您想要的。

如果您正在使用C ++ 11,那么std::future可能正是您正在尋找的:它可以自動捕獲使其成為工作線程頂部的異常,並將它們傳遞給父線程調用std::future::get的觀點。 (在幕后,這與@AnthonyWilliams的回答完全一樣;它已經為你實現了。)

缺點是沒有標准的方法來“停止關注” std::future ; 甚至它的析構函數都會阻塞,直到任務完成。 [編輯,2017:阻塞析構函數行為只是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::threadstd::exception_ptr編寫一個類似於工作的示例,但是std::exception_ptr (使用libc ++)出了問題,所以我還沒有實際工作。 :(

[編輯,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年我做錯了什么,但我確定這是我的錯。

您的問題是,您可能會從多個線程收到多個異常,因為每個線程可能會因為不同的原因而失敗。

我假設主線程以某種方式等待線程結束以檢索結果,或定期檢查其他線程的進度,並且同步對共享數據的訪問。

簡單解決方案

簡單的解決方案是捕獲每個線程中的所有異常,將它們記錄在共享變量中(在主線程中)。

完成所有線程后,決定如何處理異常。 這意味着所有其他線程繼續進行處理,這可能不是您想要的。

復雜解決方案

如果從另一個線程拋出異常,則更復雜的解決方案是讓每個線程檢查其執行的關鍵點。

如果一個線程拋出異常,它會在退出線程之前被捕獲,異常對象被復制到主線程中的某個容器中(如在簡單解決方案中),並且一些共享布爾變量被設置為true。

當另一個線程測試這個布爾值時,它會看到執行將被中止,並以優雅的方式中止。

當所有線程都中止時,主線程可以根據需要處理異常。

從線程拋出的異常將無法在父線程中捕獲。 線程具有不同的上下文和堆棧,並且通常父線程不需要留在那里等待子項完成,以便它可以捕獲它們的異常。 代碼中沒有任何地方可以捕獲:

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

您將需要捕獲每個線程內的異常並解釋主線程中的線程的退出狀態,以重新拋出您可能需要的任何異常。

順便說一下,如果一個線程中有一個catch,那么它是特定於實現的,如果完全沒有堆棧展開,即在調用terminate之前甚至不能調用自動變量的析構函數。 有些編譯器會這樣做,但並不是必需的。

你能否在工作線程中序列化異常,將其傳回主線程,反序列化並再次拋出? 我希望為了使這個工作,所有異常都必須從同一個類派生(或者至少有一小部分類再次使用switch語句)。 另外,我不確定它們是否可序列化,我只是在大聲思考。

實際上,沒有好的和通用的方法將異常從一個線程傳輸到下一個線程。

如果,因為它應該,所有的異常派生自std :: exception,那么你可以有一個頂級的一般異常catch,它會以某種方式將異常發送到主線程再次拋出它。 問題是你失去了異常的拋出點。 您可以編寫與編譯器相關的代碼來獲取此信息並進行傳輸。

如果不是所有的異常都繼承了std :: exception,那么你就麻煩了並且必須在你的線程中編寫很多頂級catch ...但是解決方案仍然存在。

請參閱http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html 也可以編寫一個函數來包含你調用的函數來加入子線程,它會自動重新拋出(使用boost :: rethrow_exception)子線程發出的任何異常。

您需要對worker中的所有異常執行泛型捕獲(包括非std異常,例如訪問沖突),並從工作線程發送消息(我假設您有某種消息傳遞?)到控制thread,包含指向異常的實時指針,並通過創建異常副本重新拋出該異常。 然后工人可以釋放原始對象並退出。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM