Bagaimana cara menyebarkan pengecualian antar utas?

105

Kami memiliki fungsi yang dipanggil oleh satu utas (kami beri nama ini utas utama). Di dalam tubuh fungsi kita menelurkan beberapa utas pekerja untuk melakukan pekerjaan intensif CPU, tunggu sampai semua utas selesai, lalu kembalikan hasilnya ke utas utama.

Hasilnya adalah pemanggil dapat menggunakan fungsi tersebut secara naif, dan secara internal akan menggunakan beberapa inti.

Sejauh ini semuanya baik-baik saja ..

Masalah yang kami hadapi adalah tentang pengecualian. Kami tidak ingin pengecualian pada utas pekerja membuat aplikasi mogok. Kami ingin pemanggil ke fungsi tersebut dapat menangkap mereka di utas utama. Kita harus menangkap pengecualian pada utas pekerja dan menyebarkannya ke utas utama agar mereka terus melepaskannya dari sana.

Bagaimana kita bisa melakukan ini?

Yang terbaik yang bisa saya pikirkan adalah:

  1. Tangkap berbagai macam pengecualian pada utas pekerja kita (std :: exception dan beberapa milik kita sendiri).
  2. Catat jenis dan pesan pengecualiannya.
  3. Memiliki pernyataan sakelar yang sesuai di utas utama yang memunculkan kembali pengecualian dari jenis apa pun yang direkam di utas pekerja.

Ini memiliki kerugian yang jelas karena hanya mendukung serangkaian jenis pengecualian terbatas, dan akan memerlukan modifikasi setiap kali jenis pengecualian baru ditambahkan.

pauldoo
sumber

Jawaban:

89

C ++ 11 memperkenalkan exception_ptrtipe yang memungkinkan untuk mengangkut pengecualian antar utas:

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

Karena dalam kasus Anda, Anda memiliki beberapa utas pekerja, Anda perlu menyimpannya exception_ptruntuk masing-masing.

Perhatikan bahwa itu exception_ptradalah penunjuk seperti ptr bersama, jadi Anda harus menyimpan setidaknya satuexception_ptr penunjuk ke setiap pengecualian atau mereka akan dirilis.

Khusus Microsoft: jika Anda menggunakan Pengecualian SEH ( /EHa), kode contoh juga akan mengirimkan pengecualian SEH seperti pelanggaran akses, yang mungkin bukan yang Anda inginkan.

Gerardo Hernandez
sumber
Bagaimana dengan beberapa utas yang muncul dari main? Jika utas pertama mencapai pengecualian dan keluar, main () akan menunggu di utas kedua join () yang mungkin berjalan selamanya. main () tidak akan pernah bisa menguji teptr setelah dua joins (). Tampaknya semua utas perlu memeriksa teptr global secara berkala dan keluar jika sesuai. Adakah cara yang bersih untuk menangani situasi ini?
Cosmo
75

Saat ini, satu-satunya cara portabel adalah menulis klausa catch untuk semua jenis pengecualian yang mungkin ingin Anda transfer antar utas, menyimpan informasi di suatu tempat dari klausa catch tersebut dan kemudian menggunakannya nanti untuk melempar ulang pengecualian. Ini adalah pendekatan yang diambil oleh Boost.Exception .

Di C ++ 0x, Anda akan dapat menangkap pengecualian dengan catch(...) dan kemudian menyimpannya dalam contoh std::exception_ptrpenggunaan std::current_exception(). Anda kemudian dapat mengembalikannya nanti dari utas yang sama atau berbeda dengan std::rethrow_exception().

Jika Anda menggunakan Microsoft Visual Studio 2005 atau yang lebih baru, maka perpustakaan thread just :: thread C ++ 0x mendukung std::exception_ptr. (Penafian: ini adalah produk saya).

Anthony Williams
sumber
7
Ini sekarang menjadi bagian dari C ++ 11 dan didukung oleh MSVS 2010; lihat msdn.microsoft.com/en-us/library/dd293602.aspx .
Johan Råde
7
Ini juga didukung oleh gcc 4.4+ di linux.
Anthony Williams
Keren, ada tautan untuk contoh penggunaan: en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke
11

Jika Anda menggunakan C ++ 11, maka std::futuremungkin melakukan apa yang Anda cari: secara otomatis dapat menjebak pengecualian yang membuatnya berada di atas utas pekerja, dan meneruskannya ke utas induk pada titik itu.std::future::get adalah dipanggil. (Di balik layar, ini terjadi persis seperti jawaban @AnthonyWilliams; ini baru saja diterapkan untuk Anda.)

Sisi negatifnya adalah tidak ada cara standar untuk "berhenti memedulikan" a std::future; bahkan penghancurnya hanya akan memblokir sampai tugas selesai.[EDIT, 2017: Perilaku penghancur-pemblokiran adalah kesalahan fungsi hanya dari masa depan semu yang dikembalikan std::async, yang seharusnya tidak pernah Anda gunakan. Masa depan normal tidak menghalangi penghancurnya. Tapi Anda masih tidak bisa "membatalkan" tugas jika Anda menggunakan std::future: tugas yang memenuhi janji akan terus berjalan di belakang layar meskipun tidak ada lagi yang mendengarkan jawabannya.] Berikut adalah contoh mainan yang mungkin menjelaskan apa yang saya berarti:

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

Saya baru saja mencoba menulis contoh karya yang mirip menggunakan std::threaddan std::exception_ptr, tetapi ada yang salah denganstd::exception_ptr (menggunakan libc ++) jadi saya belum membuatnya benar-benar berfungsi. :(

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

Saya tidak tahu apa yang saya lakukan salah di tahun 2013, tapi saya yakin itu salah saya.]

Quuxplusone
sumber
Mengapa Anda menetapkan masa depan ciptaan ke bernama fdan kemudian emplace_backitu? Tidak bisakah Anda melakukannya waitables.push_back(std::async(…));atau saya mengabaikan sesuatu (Ini terkompilasi, pertanyaannya adalah apakah itu bisa bocor, tetapi saya tidak melihat bagaimana)?
Konrad Rudolph
1
Juga, apakah ada cara untuk melepas tumpukan dengan membatalkan kontrak berjangka daripada menggunakan wait? Sesuatu di sepanjang garis "segera setelah salah satu pekerjaan gagal, yang lain tidak lagi penting".
Konrad Rudolph
4 tahun kemudian, jawaban saya belum menua. :) Re "Why": Saya pikir itu hanya untuk kejelasan (untuk menunjukkan bahwa asyncmengembalikan masa depan daripada sesuatu-lain). Re "Juga, apakah ada": Tidak di std::future, tapi lihat pembicaraan Sean Parent "Kode Lebih Baik: Konkurensi" atau "Masa Depan dari Awal" saya untuk berbagai cara menerapkannya jika Anda tidak keberatan menulis ulang seluruh STL sebagai permulaan. :) Kata kunci pencariannya adalah "pembatalan".
Quuxplusone
Terima kasih untuk balasan Anda. Saya pasti akan melihat pembicaraan ketika saya menemukan satu menit.
Konrad Rudolph
1
Hasil edit tahun 2017 yang bagus. Sama seperti yang diterima, tetapi dengan pointer pengecualian terbatas. Saya akan meletakkannya di atas dan mungkin bahkan menyingkirkan sisanya.
Nathan Cooper
6

Masalah Anda adalah bahwa Anda dapat menerima beberapa pengecualian, dari beberapa utas, karena masing-masing dapat gagal, mungkin karena alasan yang berbeda.

Saya berasumsi utas utama entah bagaimana menunggu utas berakhir untuk mengambil hasil, atau memeriksa kemajuan utas lain secara teratur, dan akses ke data bersama disinkronkan.

Solusi sederhana

Solusi sederhananya adalah menangkap semua pengecualian di setiap utas, merekamnya dalam variabel bersama (di utas utama).

Setelah semua utas selesai, putuskan apa yang harus dilakukan dengan pengecualian. Ini berarti bahwa semua utas lainnya melanjutkan pemrosesannya, yang mungkin, bukan yang Anda inginkan.

Solusi yang kompleks

Solusi yang lebih kompleks adalah meminta setiap utas Anda memeriksa titik-titik strategis pelaksanaannya, jika pengecualian dilemparkan dari utas lain.

Jika utas menampilkan pengecualian, itu ditangkap sebelum keluar dari utas, objek pengecualian disalin ke beberapa wadah di utas utama (seperti dalam solusi sederhana), dan beberapa variabel boolean bersama disetel ke true.

Dan ketika thread lain menguji boolean ini, ia melihat bahwa eksekusi harus dibatalkan, dan dibatalkan dengan cara yang anggun.

Jika semua utas dibatalkan, utas utama dapat menangani pengecualian sesuai kebutuhan.

paercebal.dll
sumber
4

Pengecualian yang dilemparkan dari utas tidak akan bisa ditangkap di utas induk. Utas memiliki konteks dan tumpukan yang berbeda, dan umumnya utas induk tidak diharuskan untuk tetap di sana dan menunggu anak-anak selesai, sehingga dapat menangkap pengecualian mereka. Tidak ada tempat dalam kode untuk tangkapan itu:

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

Anda perlu menangkap pengecualian di dalam setiap utas dan menafsirkan status keluar dari utas di utas utama untuk mengembalikan pengecualian apa pun yang mungkin Anda perlukan.

BTW, jika tidak ada tangkapan dalam utas itu adalah implementasi khusus jika pelepasan tumpukan akan dilakukan sama sekali, yaitu penghancur variabel otomatis Anda bahkan mungkin tidak dipanggil sebelum penghentian dipanggil. Beberapa kompiler melakukannya, tetapi itu tidak wajib.

n-alexander
sumber
3

Bisakah Anda membuat serial pengecualian di thread pekerja, mengirimkannya kembali ke thread utama, melakukan deserialisasi, dan membuangnya lagi? Saya berharap agar ini berfungsi, semua pengecualian harus berasal dari kelas yang sama (atau setidaknya satu set kelas kecil dengan pernyataan switch lagi). Juga, saya tidak yakin bahwa mereka akan dapat diserialkan, saya hanya berpikir keras.

tvanfosson.dll
sumber
Mengapa perlu membuat serial jika kedua utas berada dalam proses yang sama?
Nawaz
1
@Nawaz karena pengecualian mungkin memiliki referensi ke variabel lokal-thread yang tidak tersedia secara otomatis untuk thread lain.
tvanfosson
2

Tidak ada cara yang baik dan umum untuk mengirimkan pengecualian dari satu utas ke utas berikutnya.

Jika, sebagaimana mestinya, semua pengecualian Anda berasal dari std :: exception, maka Anda dapat memiliki tangkapan pengecualian umum tingkat atas yang entah bagaimana akan mengirim pengecualian ke utas utama di mana ia akan dilemparkan lagi. Masalahnya adalah Anda kehilangan titik lempar pengecualian. Anda mungkin dapat menulis kode yang bergantung pada kompilator untuk mendapatkan informasi ini dan mengirimkannya.

Jika tidak semua pengecualian Anda mewarisi std :: exception, maka Anda dalam masalah dan harus menulis banyak tangkapan tingkat atas di utas Anda ... tetapi solusinya masih berlaku.

PierreBdR
sumber
1

Anda perlu melakukan penangkapan umum untuk semua pengecualian di pekerja (termasuk pengecualian non-std, seperti pelanggaran akses), dan mengirim pesan dari utas pekerja (saya kira Anda memiliki semacam pesan di tempat?) Ke pengontrol utas, berisi penunjuk langsung ke pengecualian, dan luncurkan kembali ke sana dengan membuat salinan pengecualian. Kemudian pekerja dapat membebaskan objek asli dan keluar.

anon6439
sumber