İş parçacığı arasında özel durumları nasıl yayabilirim?


105

Tek bir iş parçacığının çağırdığı bir işleve sahibiz (bunu ana iş parçacığı olarak adlandırıyoruz). İşlevin gövdesi içinde, CPU yoğun iş yapmak için birden çok çalışan iş parçacığı oluştururuz, tüm iş parçacıklarının bitmesini bekleriz, ardından sonucu ana iş parçacığında döndürürüz.

Sonuç, arayanın işlevi saf bir şekilde kullanabilmesi ve dahili olarak birden çok çekirdeği kullanmasıdır.

Şimdiye kadar hepsi iyi ..

Sahip olduğumuz sorun istisnalarla uğraşmaktır. Çalışan iş parçacıklarında istisnaların uygulamayı çökertmesini istemiyoruz. Arayanın onları ana ileti dizisinde yakalayabilmesini istiyoruz. İşçi iş parçacıklarındaki istisnaları yakalamalı ve oradan ayrılmaya devam etmelerini sağlamak için bunları ana iş parçacığına yaymalıyız.

Bunu nasıl yapabiliriz?

Aklıma gelen en iyi şey:

  1. Çalışan iş parçacıklarımızda çok çeşitli istisnaları yakalayın (std :: exception ve kendimize ait olanlardan birkaçı).
  2. İstisnanın türünü ve mesajını kaydedin.
  3. Ana iş parçacığı üzerinde, çalışan iş parçacığına kaydedilen türden istisnaları yeniden toplayan karşılık gelen bir switch deyimine sahip olun.

Bu, yalnızca sınırlı bir istisna türü kümesini desteklemenin bariz dezavantajına sahiptir ve yeni istisna türleri eklendiğinde değişiklik yapılması gerekir.

Yanıtlar:


89

C ++ 11, exception_ptriş parçacıkları arasında istisnaların taşınmasına izin veren türü tanıttı :

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

Sizin durumunuzda birden fazla çalışan iş parçacığına sahip olduğunuz exception_ptriçin, her biri için bir tane tutmanız gerekecek .

Bunun exception_ptrpaylaşılan bir ptr benzeri işaretçi exception_ptrolduğuna dikkat edin, bu nedenle her bir istisnayı işaret eden en az bir tane tutmanız gerekecek, aksi takdirde serbest bırakılacaktır.

Microsoft'a özgü: SEH İstisnaları ( /EHa) kullanırsanız, örnek kod, istediğiniz gibi olmayan erişim ihlalleri gibi SEH istisnalarını da taşıyacaktır.


Ana bilgisayardan ortaya çıkan birden çok iş parçacığı ne olacak? İlk iş parçacığı bir istisnaya ulaşır ve çıkarsa, main () sonsuza kadar çalışabilecek ikinci iş parçacığı birleşiminde () bekleyecektir. main (), iki birleşmeden () sonra teptr'i asla test edemez. Görünüşe göre tüm iş parçacıklarının düzenli olarak global teptr'i kontrol etmesi ve uygunsa çıkması gerekiyor. Bu durumu halletmenin temiz bir yolu var mı?
Cosmo

75

Şu anda, taşınabilir tek yol, iş parçacıkları arasında aktarmak isteyebileceğiniz tüm istisna türleri için catch cümleleri yazmak, bilgiyi bu catch cümlesinden bir yerde saklamak ve daha sonra bir istisnayı yeniden atmak için kullanmaktır. Bu, Boost.Exception tarafından benimsenen yaklaşımdır .

C ++ 0 x, sen bir istisna yakalamak mümkün olacak catch(...)ve daha sonra bir örneği depolamak std::exception_ptrkullanarak std::current_exception(). Daha sonra ile aynı veya farklı bir iş parçacığından tekrar atabilirsiniz std::rethrow_exception().

Microsoft Visual Studio 2005 veya sonraki bir sürümünü kullanıyorsanız, o zaman sadece :: iş parçacığı C ++ 0x iş parçacığı kitaplığı destekler std::exception_ptr. (Sorumluluk reddi: bu benim ürünüm).


7
Bu artık C ++ 11'in bir parçasıdır ve MSVS 2010 tarafından desteklenmektedir; bkz. msdn.microsoft.com/en-us/library/dd293602.aspx .
Johan Råde

7
Linux üzerinde gcc 4.4+ tarafından da desteklenmektedir.
Anthony Williams

Harika, kullanım örneği için bir bağlantı var: en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke

11

Eğer C ++ 11 kullanıyorsanız, o zaman std::futureo automagicallylar tuzak istisnalar işçi iş parçacığı üst bunu yapabilir ve noktada ebeveyn Konuya yoluyla geçmesi: Aradığınız tam olarak ne yapabilir std::future::getolduğunu aranan. (Perde arkasında, bu aynen @AnthonyWilliams'ın cevabında olduğu gibi gerçekleşir; zaten sizin için uygulandı.)

Kötü tarafı, a'yı "önemsemeyi bırakmanın" standart bir yolu olmamasıdır std::future; yıkıcısı bile görev tamamlanana kadar basitçe engelleyecektir. [DÜZENLEME, 2017: Engelleme-yıkıcı davranışı, yalnızca geri dönen sahte vadeli işlemlerin bir yanlışlığıdır ve std::asyncyine de asla kullanmamanız gerekir. Normal vadeli işlemler yıkıcılarını engellemez. Ancak kullanıyorsanız, görevleri yine de "iptal edemezsiniz" std::future: vaatleri yerine getiren görevler, artık cevabı kimse dinlemese bile perde arkasında koşmaya devam edecektir.] İşte ne yaptığımı açıklığa kavuşturabilecek oyuncak bir örnek anlamına gelmek:

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

Ben sadece std::threadve kullanarak benzer bir örnek yazmaya çalıştım std::exception_ptr, ama bir şeyler ters gidiyor std::exception_ptr(libc ++ kullanarak) bu yüzden onu gerçekten işe yarayacak hale getirmedim. :(

[DÜZENLEME, 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'te neyi yanlış yaptığım hakkında hiçbir fikrim yok, ama eminim ki bu benim hatamdı.]


Neden yaratılan geleceği bir isimlendirip fsonra emplace_backona atıyorsunuz? Yapamaz mısın yoksa bir waitables.push_back(std::async(…));şeyi gözden mi kaçırıyorum (Derliyor, soru sızabilir mi, ama nasıl olduğunu anlamıyorum)?
Konrad Rudolph

1
Ayrıca, vadeli işlemleri iptal ederek yığını çözmenin bir yolu var waitmı? "İşlerden biri başarısız olur olmaz, diğerleri artık önemli değil" çizgisinde bir şey.
Konrad Rudolph

4 yıl sonra cevabım pek yaşlanmadı. :) Re "Neden": Bence sadece açıklık için (bunun asyncbaşka bir şeyden çok bir gelecek döndürdüğünü göstermek için ). Yeniden "Ayrıca, orada": Yok std::future, ancak yeni başlayanlar için tüm STL'yi yeniden yazmanın sakıncası yoksa bunu uygulamanın farklı yolları için Sean Parent'in "Better Code: Concurrency" veya "Futures from Scratch" konuşmasına bakın . :) Anahtar arama terimi "iptal" tir.
Quuxplusone

Cevabın için teşekkürler. Bir dakika bulduğumda muhakkak görüşmelere bakacağım.
Konrad Rudolph

1
İyi 2017 düzenlemesi. Kabul edilenle aynı, ancak kapsamlı bir istisna göstericisiyle. En üste koyardım ve belki geri kalanından bile kurtulurdum.
Nathan Cooper

6

Sorununuz, birden çok iş parçacığından birden çok istisna alabilmenizdir, çünkü her biri başarısız olabilir, belki farklı nedenlerle.

Ana iş parçacığının bir şekilde sonuçları almak için iş parçacıklarının bitmesini beklediğini veya diğer iş parçacıklarının ilerlemesini düzenli olarak kontrol ettiğini ve paylaşılan verilere erişimin senkronize edildiğini varsayıyorum.

Basit çözüm

Basit çözüm, her iş parçacığındaki tüm istisnaları yakalamak, bunları paylaşılan bir değişkene (ana iş parçacığına) kaydetmek olacaktır.

Tüm iş parçacıkları bittiğinde, istisnalarla ne yapılacağına karar verin. Bu, diğer tüm iş parçacıklarının işlemeye devam ettiği anlamına gelir, bu belki de istediğiniz şey değildir.

Karmaşık çözüm

Daha karmaşık çözüm, başka bir iş parçacığından bir istisna atılmışsa, iş parçacıklarınızın her birinin uygulamalarının stratejik noktalarında kontrol edilmesini sağlamaktır.

Bir iş parçacığı bir istisna atarsa, iş parçacığından çıkmadan önce yakalanır, istisna nesnesi ana iş parçacığındaki bazı kapsayıcılara kopyalanır (basit çözümde olduğu gibi) ve bazı paylaşılan boole değişkenleri true olarak ayarlanır.

Ve başka bir iş parçacığı bu boole'yi test ettiğinde, yürütmenin iptal edileceğini görür ve zarif bir şekilde iptal eder.

Tüm iş parçacığı iptal edildiğinde, ana iş parçacığı istisnayı gerektiği gibi işleyebilir.


4

Bir iş parçacığından atılan bir istisna, ana iş parçacığında yakalanamaz. İpliklerin farklı bağlamları ve yığınları vardır ve genel olarak ana iş parçacığının orada kalması ve istisnalarını yakalayabilmesi için çocukların bitirmesini beklemesi gerekmez. Kodda bu yakalama için yer yoktur:

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

İhtiyaç duyabileceğiniz istisnaları yeniden atmak için her iş parçacığı içindeki istisnaları yakalamanız ve ana iş parçacığındaki iş parçacıklarından çıkış durumunu yorumlamanız gerekecektir.

BTW, bir iş parçacığındaki bir yakalama olmaması durumunda, yığın çözme işlemi yapılacaksa uygulamaya özgüdür, yani otomatik değişkenlerinizin yıkıcıları, sonlandırma çağrılmadan önce çağrılmayabilir. Bazı derleyiciler bunu yapar, ancak gerekli değildir.


3

Çalışan iş parçacığındaki istisnayı seri hale getirebilir, bunu ana iş parçacığına geri gönderebilir, seriyi kaldırabilir ve tekrar atabilir misiniz? Bunun işe yaraması için istisnaların hepsinin aynı sınıftan (veya en azından switch deyimine sahip küçük bir sınıf kümesinden) türetilmesi gerektiğini bekliyorum. Ayrıca, seri hale getirilebilir mi emin değilim, sadece yüksek sesle düşünüyorum.


Her iki iş parçacığı da aynı süreç içindeyse neden onu serileştirmek gerekiyor?
Nawaz

1
@Nawaz çünkü istisna büyük olasılıkla diğer iş parçacıkları tarafından otomatik olarak kullanılamayan yerel evre değişkenlerine referanslar içeriyor.
tvanfosson

2

Gerçekten de istisnaları bir iş parçacığından diğerine aktarmanın iyi ve genel bir yolu yoktur.

Eğer olması gerektiği gibi, tüm istisnalarınız std :: exception'dan türetilmişse, o zaman istisnayı bir şekilde tekrar atılacağı ana iş parçacığına gönderecek bir üst düzey genel istisna yakalamasına sahip olabilirsiniz. Sorun, istisnanın fırlatma noktasını kaybetmenizdir. Bu bilgiyi almak ve iletmek için muhtemelen derleyiciye bağlı kod yazabilirsiniz.

Tüm istisnalarınız std :: istisnayı miras almıyorsa, o zaman başınız belada ve iş parçacığınıza çok sayıda üst düzey yakalama yazmanız gerekiyor ... ancak çözüm hala geçerli.


1

Çalışan içindeki tüm istisnalar için (erişim ihlalleri gibi standart olmayan istisnalar dahil) genel bir yakalama yapmanız ve çalışan iş parçacığından (sanırım yerinde bir tür mesajınız olduğunu varsayalım) denetleyiciye bir mesaj göndermeniz gerekecektir. iş parçacığı, istisnaya canlı bir işaretçi içerir ve istisnanın bir kopyasını oluşturarak oraya yeniden atılır. Ardından işçi orijinal nesneyi serbest bırakabilir ve çıkabilir.


Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.