Làm cách nào tôi có thể truyền các ngoại lệ giữa các chuỗi?


105

Chúng ta có một chức năng mà một luồng đơn gọi vào (chúng tôi đặt tên là luồng chính). Trong phần thân của hàm, chúng tôi tạo ra nhiều luồng công nhân để thực hiện công việc chuyên sâu của CPU, đợi tất cả các luồng kết thúc, sau đó trả về kết quả trên luồng chính.

Kết quả là người gọi có thể sử dụng hàm một cách thuần túy và bên trong nó sẽ sử dụng nhiều lõi.

Tất cả tốt cho đến nay ..

Vấn đề chúng tôi gặp phải là xử lý các trường hợp ngoại lệ. Chúng tôi không muốn các trường hợp ngoại lệ trên các luồng công nhân làm hỏng ứng dụng. Chúng tôi muốn người gọi hàm có thể bắt chúng trên luồng chính. Chúng ta phải bắt các ngoại lệ trên các luồng công nhân và truyền chúng qua luồng chính để chúng tiếp tục giải phóng từ đó.

Làm thế nào chúng ta có thể làm điều này?

Điều tốt nhất tôi có thể nghĩ đến là:

  1. Nắm bắt nhiều loại ngoại lệ trên các chuỗi công nhân của chúng tôi (std :: ngoại lệ và một số ngoại lệ của riêng chúng tôi).
  2. Ghi lại kiểu và thông báo của ngoại lệ.
  3. Có một câu lệnh chuyển đổi tương ứng trên luồng chính sẽ hiển thị lại các ngoại lệ của bất kỳ loại nào đã được ghi lại trên luồng công nhân.

Điều này có nhược điểm rõ ràng là chỉ hỗ trợ một số loại ngoại lệ hạn chế và sẽ cần sửa đổi bất cứ khi nào các loại ngoại lệ mới được thêm vào.

Câu trả lời:


89

C ++ 11 đã giới thiệu exception_ptrkiểu cho phép vận chuyển ngoại lệ giữa các luồng:

#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;
}

Vì trong trường hợp của bạn, bạn có nhiều luồng công nhân, bạn sẽ cần giữ một luồng exception_ptrcho mỗi luồng .

Lưu ý rằng đó exception_ptrlà một con trỏ giống ptr được chia sẻ, vì vậy bạn sẽ cần giữ ít nhất một con exception_ptrtrỏ tới mỗi ngoại lệ nếu không chúng sẽ được giải phóng.

Cụ thể của Microsoft: nếu bạn sử dụng SEH Exceptions ( /EHa), mã ví dụ cũng sẽ vận chuyển các ngoại lệ SEH như vi phạm quyền truy cập, có thể không phải là những gì bạn muốn.


Điều gì về nhiều chủ đề sinh ra từ chính? Nếu luồng đầu tiên gặp một ngoại lệ và thoát ra, main () sẽ đợi ở luồng thứ hai tham gia () có thể chạy mãi mãi. main () sẽ không bao giờ kiểm tra teptr sau khi cả hai tham gia (). Có vẻ như tất cả các luồng cần phải kiểm tra định kỳ teptr toàn cầu và thoát ra nếu thích hợp. Có cách nào sạch sẽ để xử lý tình huống này không?
Cosmo

75

Hiện tại, cách di động duy nhất là viết mệnh đề bắt cho tất cả các loại ngoại lệ mà bạn có thể muốn chuyển giữa các luồng, lưu trữ thông tin ở đâu đó từ mệnh đề bắt đó và sau đó sử dụng nó để ném lại một ngoại lệ. Đây là cách tiếp cận được thực hiện bởi Boost.Exception .

Trong C ++ 0x, bạn sẽ có thể bắt một ngoại lệ catch(...)và sau đó lưu trữ nó trong một phiên bản std::exception_ptrsử dụng std::current_exception(). Sau đó, bạn có thể ném lại nó sau từ cùng một chủ đề hoặc một chuỗi khác với std::rethrow_exception().

Nếu bạn đang sử dụng Microsoft Visual Studio 2005 trở lên, thì thư viện luồng just :: thread C ++ 0x sẽ hỗ trợ std::exception_ptr. (Tuyên bố từ chối trách nhiệm: đây là sản phẩm của tôi).


7
Đây là một phần của C ++ 11 và được hỗ trợ bởi MSVS 2010; xem msdn.microsoft.com/en-us/library/dd293602.aspx .
Johan Råde

7
Nó cũng được hỗ trợ bởi gcc 4.4+ trên linux.
Anthony Williams

Tuyệt vời, có liên kết cho một ví dụ sử dụng: en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke.

11

Nếu bạn đang sử dụng C ++ 11, sau đó std::futurecó thể thực hiện chính xác những gì bạn đang tìm kiếm: nó có thể Automagically bẫy ngoại lệ mà làm cho nó lên trên cùng của các sợi nhân, và vượt qua chúng thông qua các chủ đề cha mẹ tại thời điểm đó std::future::getlà gọi là. (Phía sau, điều này xảy ra chính xác như trong câu trả lời của @AnthonyWilliams; nó vừa được triển khai cho bạn.)

Mặt trái của nó là không có cách tiêu chuẩn nào để "ngừng quan tâm đến" a std::future; thậm chí trình hủy của nó sẽ đơn giản chặn cho đến khi tác vụ được hoàn thành. [EDIT, 2017: Các hành vi chặn-destructor là một misfeature chỉ của pseudo-tương lai trở về từ std::async, mà bạn không bao giờ nên sử dụng anyway. Hợp đồng tương lai bình thường không chặn trong trình hủy của chúng. Nhưng bạn vẫn không thể "hủy" nhiệm vụ nếu đang sử dụng std::future: (các) nhiệm vụ thực hiện lời hứa sẽ tiếp tục chạy ở hậu trường ngay cả khi không ai lắng nghe câu trả lời nữa.] Đây là một ví dụ đồ chơi có thể làm rõ điều tôi nghĩa là:

#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());
    }
}

Tôi vừa cố gắng viết một ví dụ giống công việc bằng cách sử dụng std::threadstd::exception_ptr, nhưng có gì đó không ổn với std::exception_ptr(sử dụng libc ++) nên tôi vẫn chưa làm cho nó thực sự hoạt động. :(

[EDIT, 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!");
    }
}

Tôi không biết mình đã làm gì sai trong năm 2013, nhưng tôi chắc chắn đó là lỗi của tôi.]


Tại sao bạn chỉ định tương lai tạo cho một tương lai được đặt tên fvà sau đó emplace_backnó? Bạn không thể chỉ làm waitables.push_back(std::async(…));hay tôi đang bỏ qua một cái gì đó (Nó biên dịch, câu hỏi là liệu nó có thể bị rò rỉ, nhưng tôi không thấy làm thế nào)?
Konrad Rudolph

1
Ngoài ra, có cách nào để giải nén ngăn xếp bằng cách hủy bỏ hợp đồng tương lai thay vì nhập waitkhông? Một cái gì đó dọc theo dòng "ngay khi một trong những công việc thất bại, những công việc khác không còn quan trọng nữa".
Konrad Rudolph

4 năm sau, câu trả lời của tôi vẫn chưa già đi. :) Re "Tại sao": Tôi nghĩ nó chỉ là để rõ ràng (để cho thấy rằng asynctrả về một tương lai hơn là một cái gì đó-khác). Re "Ngoài ra, có ở đó": Không tham gia std::future, nhưng hãy xem bài nói chuyện của Sean Parent "Better Code: Concurrency" hoặc "Futures from Scratch" của tôi để biết các cách khác nhau để thực hiện điều đó nếu bạn không ngại viết lại toàn bộ STL cho người mới bắt đầu. :) Cụm từ tìm kiếm chính là "hủy bỏ".
Quuxplusone

Cảm ơn vì đã trả lời. Tôi chắc chắn sẽ xem qua các cuộc nói chuyện khi tôi tìm thấy một phút.
Konrad Rudolph,

1
Tốt 2017 chỉnh sửa. Giống như được chấp nhận, nhưng với một con trỏ ngoại lệ trong phạm vi. Tôi sẽ đặt nó ở đầu và thậm chí có thể loại bỏ phần còn lại.
Nathan Cooper

6

Vấn đề của bạn là bạn có thể nhận được nhiều ngoại lệ, từ nhiều luồng, vì mỗi luồng có thể bị lỗi, có lẽ do các lý do khác nhau.

Tôi giả sử luồng chính bằng cách nào đó đang đợi các luồng kết thúc để truy xuất kết quả hoặc thường xuyên kiểm tra tiến trình của các luồng khác và quyền truy cập vào dữ liệu được chia sẻ được đồng bộ hóa.

Giải pháp đơn giản

Giải pháp đơn giản sẽ là bắt tất cả các ngoại lệ trong mỗi luồng, ghi lại chúng trong một biến chia sẻ (trong luồng chính).

Sau khi tất cả các chuỗi hoàn thành, hãy quyết định xem phải làm gì với các ngoại lệ. Điều này có nghĩa là tất cả các luồng khác vẫn tiếp tục xử lý, điều này có lẽ không như bạn muốn.

Giải pháp phức tạp

Giải pháp phức tạp hơn là yêu cầu mỗi luồng của bạn kiểm tra tại các điểm chiến lược trong quá trình thực thi của chúng, nếu một ngoại lệ được đưa ra từ một luồng khác.

Nếu một luồng ném một ngoại lệ, nó sẽ bị bắt trước khi thoát khỏi luồng, đối tượng ngoại lệ được sao chép vào một vùng chứa nào đó trong luồng chính (như trong giải pháp đơn giản) và một số biến boolean dùng chung được đặt thành true.

Và khi một luồng khác kiểm tra boolean này, nó thấy việc thực thi sẽ bị hủy bỏ và hủy bỏ một cách duyên dáng.

Khi tất cả luồng đã hủy bỏ, luồng chính có thể xử lý ngoại lệ khi cần.


4

Một ngoại lệ được đưa ra từ một chuỗi sẽ không thể bắt được trong chuỗi chính. Luồng có các ngữ cảnh và ngăn xếp khác nhau, và nói chung luồng cha không bắt buộc phải ở đó và đợi phần con hoàn thành, để nó có thể bắt các ngoại lệ của chúng. Đơn giản là không có chỗ nào trong mã cho việc bắt đó:

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

Bạn sẽ cần bắt các ngoại lệ bên trong mỗi luồng và diễn giải trạng thái thoát khỏi các luồng trong luồng chính để ném lại bất kỳ ngoại lệ nào bạn có thể cần.

BTW, trong trường hợp vắng mặt của một bắt trong một luồng, nó được triển khai cụ thể nếu việc hủy cuộn ngăn xếp sẽ được thực hiện hoàn toàn, tức là các trình hủy biến tự động của bạn thậm chí có thể không được gọi trước khi kết thúc được gọi. Một số trình biên dịch làm điều đó, nhưng nó không bắt buộc.


3

Bạn có thể tuần tự hóa ngoại lệ trong luồng công nhân, truyền dữ liệu đó trở lại luồng chính, deserialize và ném lại nó không? Tôi hy vọng rằng để điều này hoạt động, tất cả các ngoại lệ sẽ phải bắt nguồn từ cùng một lớp (hoặc ít nhất là một tập hợp nhỏ các lớp có lại câu lệnh switch). Ngoài ra, tôi không chắc rằng chúng sẽ có thể được nối tiếp hóa, tôi chỉ đang suy nghĩ lung tung.


Tại sao người ta cần tuần tự hóa nó nếu cả hai chủ đề đều trong cùng một quy trình?
Nawaz

1
@Nawaz vì ngoại lệ có thể có tham chiếu đến các biến cục bộ của luồng không tự động có sẵn cho các luồng khác.
tvanfosson

2

Thực sự là không có cách nào tốt và chung chung để truyền các ngoại lệ từ luồng này sang luồng tiếp theo.

Nếu đúng như vậy, tất cả các ngoại lệ của bạn bắt nguồn từ std :: exception, thì bạn có thể có một bắt ngoại lệ chung cấp cao nhất bằng cách nào đó sẽ gửi ngoại lệ đến luồng chính nơi nó sẽ được ném lại. Vấn đề là bạn mất điểm ném của ngoại lệ. Bạn có thể viết mã phụ thuộc vào trình biên dịch để lấy thông tin này và truyền tải nó.

Nếu không phải tất cả ngoại lệ của bạn đều thừa kế std :: exception, thì bạn đang gặp rắc rối và phải viết rất nhiều lệnh bắt cấp cao nhất trong luồng của mình ... nhưng giải pháp vẫn giữ nguyên.


1

Bạn sẽ cần thực hiện quy trình bắt chung cho tất cả các ngoại lệ trong worker (bao gồm các ngoại lệ không phải std, như vi phạm quyền truy cập) và gửi một tin nhắn từ chuỗi worker (tôi cho rằng bạn có một số loại thông báo tại chỗ?) Đến kiểm soát luồng, chứa một con trỏ trực tiếp đến ngoại lệ và quay lại đó bằng cách tạo một bản sao của ngoại lệ. Sau đó, worker có thể giải phóng đối tượng ban đầu và thoát ra.


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.