ฉันจะเผยแพร่ข้อยกเว้นระหว่างเธรดได้อย่างไร


105

เรามีฟังก์ชันที่เธรดเดียวเรียกเข้ามา (เราตั้งชื่อนี้ว่าเธรดหลัก) ภายในเนื้อความของฟังก์ชั่นเราวางเธรดผู้ปฏิบัติงานหลายเธรดเพื่อทำงานที่เข้มข้นของ CPU รอให้เธรดทั้งหมดเสร็จสิ้นจากนั้นส่งคืนผลลัพธ์ในเธรดหลัก

ผลลัพธ์คือผู้โทรสามารถใช้ฟังก์ชันได้อย่างไร้เดียงสาและภายในจะใช้ประโยชน์จากหลายคอร์

ดีมาก ...

ปัญหาที่เรามีคือการจัดการกับข้อยกเว้น เราไม่ต้องการให้มีข้อยกเว้นในเธรดของผู้ปฏิบัติงานทำให้แอปพลิเคชันหยุดทำงาน เราต้องการให้ผู้เรียกใช้ฟังก์ชันจับพวกเขาบนเธรดหลักได้ เราต้องตรวจจับข้อยกเว้นในเธรดของผู้ปฏิบัติงานและเผยแพร่ข้ามไปยังเธรดหลักเพื่อให้พวกเขาคลี่คลายต่อไปจากที่นั่น

เราจะทำเช่นนี้ได้อย่างไร?

สิ่งที่ดีที่สุดที่ฉันคิดได้คือ:

  1. ตรวจสอบข้อยกเว้นที่หลากหลายในเธรดผู้ปฏิบัติงานของเรา (มาตรฐาน :: ข้อยกเว้นและข้อยกเว้นบางส่วนของเราเอง)
  2. บันทึกประเภทและข้อความของข้อยกเว้น
  3. มีคำสั่งสวิตช์ที่สอดคล้องกันบนเธรดหลักซึ่งเรียกคืนข้อยกเว้นประเภทใดก็ตามที่บันทึกไว้ในเธรดผู้ปฏิบัติงาน

สิ่งนี้มีข้อเสียที่ชัดเจนในการรองรับชุดข้อยกเว้นที่ จำกัด เท่านั้นและจะต้องมีการแก้ไขเมื่อใดก็ตามที่มีการเพิ่มประเภทข้อยกเว้นใหม่

คำตอบ:


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 /EHaexceptions ( ) โค้ดตัวอย่างจะส่งข้อยกเว้น SEH เช่นการละเมิดการเข้าถึงซึ่งอาจไม่ใช่สิ่งที่คุณต้องการ


แล้วหลายเธรดที่เกิดจากหลัก? หากเธรดแรกพบข้อยกเว้นและออก main () จะรอที่เธรดที่สอง join () ซึ่งอาจทำงานตลอดไป main () จะไม่ได้รับการทดสอบ teptr หลังจากการรวมทั้งสอง () ดูเหมือนว่าเธรดทั้งหมดจำเป็นต้องตรวจสอบ global teptr เป็นระยะและออกหากเหมาะสม มีวิธีจัดการกับสถานการณ์นี้ที่สะอาดหรือไม่?
Cosmo

75

ปัจจุบันวิธีเดียวที่พกพาได้คือการเขียนคำสั่ง catch สำหรับข้อยกเว้นทุกประเภทที่คุณอาจต้องการถ่ายโอนระหว่างเธรดเก็บข้อมูลไว้ที่ใดที่หนึ่งจากประโยค catch นั้นแล้วใช้ในภายหลังเพื่อลบข้อยกเว้น นี่คือวิธีการที่ถ่ายโดยBoost.Exception

ใน C ++ 0x คุณจะสามารถที่จะจับข้อยกเว้นด้วยcatch(...)แล้วเก็บไว้ในตัวอย่างของการใช้std::exception_ptr std::current_exception()จากนั้นคุณสามารถ rethrow std::rethrow_exception()ภายหลังจากที่เดียวกันหรือหัวข้อที่แตกต่างกับ

ถ้าคุณกำลังใช้ Microsoft Visual Studio 2005 หรือหลังจากนั้นเพียง :: ด้าย C ++ 0x ด้ายห้องสมุดstd::exception_ptrสนับสนุน (คำเตือน: นี่คือผลิตภัณฑ์ของฉัน)


7
ตอนนี้เป็นส่วนหนึ่งของ C ++ 11 และรองรับ MSVS 2010 ดูmsdn.microsoft.com/en-us/library/dd293602.aspx
Johan Råde

7
นอกจากนี้ยังรองรับโดย gcc 4.4+ บน linux
Anthony Williams

เจ๋งมีลิงค์ตัวอย่างการใช้งาน: en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke

11

หากคุณใช้ C ++ 11 std::futureอาจทำในสิ่งที่คุณกำลังมองหานั่นคือสามารถดักจับข้อยกเว้นโดยอัตโนมัติที่ทำให้มันอยู่ด้านบนสุดของเธรดผู้ปฏิบัติงานและส่งผ่านไปยังเธรดแม่ในจุดที่std::future::getเป็น เรียกว่า. (เบื้องหลังสิ่งนี้เกิดขึ้นเหมือนกับในคำตอบของ @AnthonyWilliams มันเพิ่งถูกนำไปใช้สำหรับคุณแล้ว)

ข้อเสียคือไม่มีวิธีมาตรฐานในการ "เลิกใส่ใจ" a 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::threadและstd::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 แต่ฉันแน่ใจว่ามันเป็นความผิดของฉัน]


ทำไมคุณกำหนดจะสร้างอนาคตที่จะมีชื่อfและจากนั้นemplace_backมันได้หรือไม่ คุณไม่สามารถทำได้waitables.push_back(std::async(…));หรือฉันกำลังมองข้ามบางสิ่งบางอย่าง (มันรวบรวมคำถามคือมันรั่วได้หรือไม่ แต่ฉันไม่เห็นวิธีการ)
Konrad Rudolph

1
นอกจากนี้ยังมีวิธีคลายสแต็กโดยการยกเลิกฟิวเจอร์สแทนการwaitฝังตัวหรือไม่? บางสิ่งบางอย่างตามบรรทัด "ทันทีที่งานหนึ่งล้มเหลวงานอื่น ๆ ก็ไม่สำคัญอีกต่อไป"
Konrad Rudolph

4 ปีต่อมาคำตอบของฉันยังไม่ดีเท่าที่ควร :) Re "Why": ฉันคิดว่ามันเป็นเพียงเพื่อความชัดเจน (เพื่อแสดงให้เห็นว่าasyncผลตอบแทนในอนาคต Re "นอกจากนี้ยังมี": ไม่ได้อยู่ในstd::futureคำพูดของ Sean Parent "Better Code: Concurrency"หรือ"Futures from Scratch"ของฉันสำหรับวิธีต่างๆในการนำไปใช้หากคุณไม่สนใจที่จะเขียน STL ทั้งหมดใหม่สำหรับผู้เริ่มต้น :) คำค้นหาหลักคือ "การยกเลิก"
Quuxplusone

ขอบคุณสำหรับการตอบกลับของคุณ. ฉันจะดูการพูดคุยเมื่อฉันพบสักครู่
Konrad Rudolph

1
แก้ไข 2017 ได้ดี เหมือนกับตัวชี้ที่ยอมรับ แต่มีตัวชี้ข้อยกเว้นที่กำหนดขอบเขตไว้ ฉันจะวางไว้ที่ด้านบนและอาจจะกำจัดส่วนที่เหลือ
Nathan Cooper

6

ปัญหาของคุณคือคุณอาจได้รับข้อยกเว้นหลายรายการจากหลายเธรดเนื่องจากแต่ละเธรดอาจล้มเหลวอาจมาจากสาเหตุที่แตกต่างกัน

ฉันสมมติว่าเธรดหลักกำลังรอให้เธรดสิ้นสุดเพื่อดึงผลลัพธ์หรือตรวจสอบความคืบหน้าของเธรดอื่นเป็นประจำและการเข้าถึงข้อมูลที่แชร์จะซิงโครไนซ์

วิธีแก้ปัญหาง่ายๆ

วิธีแก้ไขง่ายๆคือจับข้อยกเว้นทั้งหมดในแต่ละเธรดบันทึกไว้ในตัวแปรที่ใช้ร่วมกัน (ในเธรดหลัก)

เมื่อเธรดทั้งหมดเสร็จสิ้นให้ตัดสินใจว่าจะทำอย่างไรกับข้อยกเว้น ซึ่งหมายความว่าเธรดอื่น ๆ ทั้งหมดยังคงประมวลผลซึ่งอาจไม่ใช่สิ่งที่คุณต้องการ

โซลูชันที่ซับซ้อน

วิธีแก้ปัญหาที่ซับซ้อนมากขึ้นคือให้แต่ละเธรดของคุณตรวจสอบที่จุดยุทธศาสตร์ของการดำเนินการหากมีข้อยกเว้นเกิดขึ้นจากเธรดอื่น

หากเธรดส่งข้อยกเว้นเธรดจะถูกจับก่อนที่จะออกจากเธรดอ็อบเจ็กต์ข้อยกเว้นจะถูกคัดลอกไปยังคอนเทนเนอร์บางส่วนในเธรดหลัก (เช่นเดียวกับวิธีแก้ปัญหาแบบง่าย) และตัวแปรบูลีนที่ใช้ร่วมกันบางตัวถูกตั้งค่าเป็นจริง

และเมื่อเธรดอื่นทดสอบบูลีนนี้เธรดจะเห็นว่าการดำเนินการจะถูกยกเลิกและจะยกเลิกด้วยวิธีที่สง่างาม

เมื่อเธรดทั้งหมดถูกยกเลิกเธรดหลักสามารถจัดการกับข้อยกเว้นได้ตามต้องการ


4

ข้อยกเว้นที่เกิดจากเธรดจะไม่สามารถจับได้ในเธรดหลัก เธรดมีบริบทและสแต็กที่แตกต่างกันและโดยทั่วไปเธรดพาเรนต์ไม่จำเป็นต้องอยู่ที่นั่นและรอให้ชายด์เล่นจนจบเพื่อให้สามารถจับข้อยกเว้นได้ ไม่มีสถานที่ในรหัสสำหรับการจับนั้น:

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

คุณจะต้องตรวจจับข้อยกเว้นภายในแต่ละเธรดและตีความสถานะการออกจากเธรดในเธรดหลักเพื่อโยนข้อยกเว้นที่คุณอาจต้องการอีกครั้ง

BTW ในกรณีที่ไม่มีการดักจับในเธรดจะมีการใช้งานที่เฉพาะเจาะจงหากการคลายสแต็กจะทำเลยนั่นคือตัวทำลายตัวแปรอัตโนมัติของคุณอาจไม่ถูกเรียกก่อนที่จะเรียกการยุติ คอมไพเลอร์บางตัวทำเช่นนั้น แต่ไม่จำเป็นต้องใช้


3

คุณช่วยจัดลำดับข้อยกเว้นในเธรดของผู้ปฏิบัติงานส่งสิ่งนั้นกลับไปที่เธรดหลักคลายเส้นและโยนใหม่ได้หรือไม่ ฉันคาดหวังว่าสิ่งนี้จะใช้งานได้ข้อยกเว้นทั้งหมดจะต้องได้มาจากคลาสเดียวกัน (หรืออย่างน้อยก็เป็นคลาสเล็ก ๆ ที่มีคำสั่งสวิตช์อีกครั้ง) นอกจากนี้ฉันไม่แน่ใจว่าพวกเขาจะต่อเนื่องกันได้ฉันแค่คิดออกมาดัง ๆ


เหตุใดจึงต้องทำให้เป็นอนุกรมหากเธรดทั้งสองอยู่ในกระบวนการเดียวกัน
Nawaz

1
@Nawaz เนื่องจากข้อยกเว้นอาจมีการอ้างอิงถึงตัวแปรเธรดโลคัลที่ไม่สามารถใช้ได้กับเธรดอื่นโดยอัตโนมัติ
tvanfosson

2

ไม่มีวิธีที่ดีและทั่วไปในการส่งข้อยกเว้นจากเธรดหนึ่งไปยังเธรดถัดไป

หากตามที่ควรจะเป็นข้อยกเว้นทั้งหมดของคุณมาจาก std :: ข้อยกเว้นคุณสามารถมีข้อยกเว้นทั่วไประดับบนสุดที่จะส่งข้อยกเว้นไปยังเธรดหลักซึ่งจะถูกโยนอีกครั้ง ปัญหาคือคุณหลุดจุดขว้างของข้อยกเว้น คุณสามารถเขียนโค้ดที่ขึ้นกับคอมไพเลอร์เพื่อรับข้อมูลนี้และส่งไปได้

หากข้อยกเว้นของคุณไม่ได้รับการยกเว้น std :: ข้อยกเว้นทั้งหมดแสดงว่าคุณมีปัญหาและต้องเขียนการจับระดับบนสุดจำนวนมากในเธรดของคุณ ... แต่วิธีแก้ปัญหายังคงค้างอยู่


1

คุณจะต้องทำการตรวจจับทั่วไปสำหรับข้อยกเว้นทั้งหมดในพนักงาน (รวมถึงข้อยกเว้นที่ไม่ใช่มาตรฐานเช่นการละเมิดการเข้าถึง) และส่งข้อความจากชุดข้อความของผู้ปฏิบัติงาน (ฉันคิดว่าคุณมีข้อความบางอย่างอยู่ในสถานที่?) ไปยังฝ่ายควบคุม เธรดที่มีตัวชี้สดไปยังข้อยกเว้นและสร้างใหม่อีกครั้งโดยการสร้างสำเนาของข้อยกเว้น จากนั้นผู้ปฏิบัติงานสามารถปลดปล่อยวัตถุดั้งเดิมและออก


1

ดูhttp://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html นอกจากนี้ยังเป็นไปได้ที่จะเขียนฟังก์ชัน wrapper ของฟังก์ชันใด ๆ ที่คุณเรียกเพื่อเข้าร่วมเธรดลูกซึ่งจะพ่นใหม่โดยอัตโนมัติ (โดยใช้ boost :: rethrow_exception) ข้อยกเว้นใด ๆ ที่เกิดจากเธรดลูก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.