2010-06-11 10 views
5

Je dois écrire un programme qui effectue des calculs très intensifs en calcul. Le programme peut fonctionner pendant plusieurs jours. Le calcul peut être facilement séparé dans différents threads sans avoir besoin de données partagées. Je souhaite une interface graphique ou un service Web qui m'informe de l'état actuel.Signaux et threads - décision de conception bonne ou mauvaise?

Ma conception actuelle utilise BOOST :: signals2 et BOOST :: thread. Il compile et jusqu'ici fonctionne comme prévu. Si un thread a terminé une itération et que de nouvelles données sont disponibles, il appelle un signal qui est connecté à un slot dans la classe GUI.

Ma question (s):

  • est cette combinaison de signaux et les fils une bonne idée? Dans un autre forum, quelqu'un a conseillé à quelqu'un de ne pas "suivre cette route".
  • Existe-t-il d'éventuels pièges mortels à proximité que je n'ai pas pu voir?
  • Est-ce que mes attentes sont réalistes qu'il sera "facile" d'utiliser ma classe GUI pour fournir une interface web ou un QT, un VTK ou une fenêtre quelconque?
  • Existe-t-il une alternative plus intelligente (comme d'autres bibliothèques de boost) que j'ai oublié?

code suivant compile avec le code

g++ -Wall -o main -lboost_thread-mt <filename>.cpp 

suit:

#include <boost/signals2.hpp> 
#include <boost/thread.hpp> 
#include <boost/bind.hpp> 

#include <iostream> 
#include <iterator> 
#include <string> 

using std::cout; 
using std::cerr; 
using std::string; 

/** 
* Called when a CalcThread finished a new bunch of data. 
*/ 
boost::signals2::signal<void(string)> signal_new_data; 

/** 
* The whole data will be stored here. 
*/ 
class DataCollector 
{ 
    typedef boost::mutex::scoped_lock scoped_lock; 
    boost::mutex mutex; 

public: 
    /** 
    * Called by CalcThreads call the to store their data. 
    */ 
    void push(const string &s, const string &caller_name) 
    { 
     scoped_lock lock(mutex); 
     _data.push_back(s); 
     signal_new_data(caller_name); 
    } 

    /** 
    * Output everything collected so far to std::out. 
    */ 
    void out() 
    { 
     typedef std::vector<string>::const_iterator iter; 
     for (iter i = _data.begin(); i != _data.end(); ++i) 
      cout << " " << *i << "\n"; 
    } 

private: 
    std::vector<string> _data; 
}; 

/** 
* Several of those can calculate stuff. 
* No data sharing needed. 
*/ 
struct CalcThread 
{ 
    CalcThread(string name, DataCollector &datcol) : 
     _name(name), _datcol(datcol) 
    { 

    } 

    /** 
    * Expensive algorithms will be implemented here. 
    * @param num_results how many data sets are to be calculated by this thread. 
    */ 
    void operator()(int num_results) 
    { 
     for (int i = 1; i <= num_results; ++i) 
     { 
      std::stringstream s; 
      s << "["; 
      if (i == num_results) 
       s << "LAST "; 
      s << "DATA " << i << " from thread " << _name << "]"; 
      _datcol.push(s.str(), _name); 
     } 
    } 

private: 
    string _name; 
    DataCollector &_datcol; 
}; 

/** 
* Maybe some VTK or QT or both will be used someday. 
*/ 
class GuiClass 
{ 
public: 
    GuiClass(DataCollector &datcol) : 
     _datcol(datcol) 
    { 

    } 

    /** 
    * If the GUI wants to present or at least count the data collected so far. 
    * @param caller_name is the name of the thread whose data is new. 
    */ 
    void slot_data_changed(string caller_name) const 
    { 
     cout << "GuiClass knows: new data from " << caller_name << std::endl; 
    } 

private: 
    DataCollector & _datcol; 

}; 

int main() 
{ 
    DataCollector datcol; 

    GuiClass mc(datcol); 
    signal_new_data.connect(boost::bind(&GuiClass::slot_data_changed, &mc, _1)); 

    CalcThread r1("A", datcol), r2("B", datcol), r3("C", datcol), r4("D", 
      datcol), r5("E", datcol); 

    boost::thread t1(r1, 3); 
    boost::thread t2(r2, 1); 
    boost::thread t3(r3, 2); 
    boost::thread t4(r4, 2); 
    boost::thread t5(r5, 3); 

    t1.join(); 
    t2.join(); 
    t3.join(); 
    t4.join(); 
    t5.join(); 

    datcol.out(); 

    cout << "\nDone" << std::endl; 
    return 0; 
} 
+0

Alors que cet exemple particulier est bien, vous devriez faire attention - comme vous ne protégez pas votre vecteur avec le mutex dans la fonction 'out' – nos

Répondre

12

est cette combinaison de signaux et fils une idée sage? J'ai un autre forum quelqu'un a conseillé à quelqu'un d'autre de ne pas "descendre cette route".

Il semble être sain. Pouvez-vous fournir un lien vers l'autre thread? Expliquaient-ils leur raisonnement?

Y a-t-il des pièges potentiellement mortels à proximité que je n'ai pas réussi à voir?

Si elles sont, je ne parviens pas à les voir aussi. Ce que vous devez prendre en charge que les notifications sont thread-safe (le déclenchement du signal ne passe pas des contextes de fil, à votre GuiClass::slot_data_changed devrait être appelé de tous les autres threads.

est mon attente réaliste qu'il sera « facile » à utiliser ma classe de GUI pour fournir une interface web ou un QT, un VTK ou quelle que soit la fenêtre?

il ne sera pas facile. pour résoudre ce problème, vous auriez à faire vos contextes de threads de changement de notification Voici ce que je ferais:

Demandez à votre GuiClass d'être un n classe de base abstraite, implémentant sa propre file d'attente de messages. Lorsque GuiClass::slot_data_changed est appelée par vos threads, vous verrouillez un mutex et publiez une copie de la notification reçue dans une file d'attente de messages interne (private:). Dans le thread du GuiClass, vous créez une fonction qui verrouille le mutex et recherche les notifications dans la file d'attente.Cette fonction doit être exécutée dans le thread du code client (dans le fil des classes concrètes que vous avez sélectionnées dans le résumé GuiClass).

Avantages:

  • votre classe de base encapsule et isole le changement de contexte de fil, de manière transparente à sa spécialisation.

Inconvénients:

  • votre code client doit soit exécuter le mode de scrutin ou permettre d'exécuter (en fonction thread-traitement).

  • il est un peu compliqué :)

est mon attente réaliste qu'il sera « facile » à utiliser ma classe GUI pour fournir une interface web ou un QT, un VTK ou une fenêtre quelconque?

Ne le voit pas, mais ce n'est pas si facile. Outre le changement de contexte de thread, il peut y avoir d'autres problèmes qui me manquent pour le moment.

Yat-il un plus intelligent alternatives (comme d'autres libs boost) que je négligé?

Pas d'autres bibliothèques de boost, mais la façon dont vous avez écrit vos threads n'est pas bonne: les jointures sont faites séquentiellement dans votre code. Pour avoir un seul join pour tous les threads, utilisez boost :: thread_group.

Au lieu de:

boost::thread t1(r1, 3); 
boost::thread t2(r2, 1); 
boost::thread t3(r3, 2); 
boost::thread t4(r4, 2); 
boost::thread t5(r5, 3); 

t1.join(); 
t2.join(); 
t3.join(); 
t4.join(); 
t5.join(); 

vous avez:

boost::thread_group processors; 
processors.create_thread(r1, 3); 
// the other threads here 

processors.join_all(); 

Modifier: Un contexte de fil est tout ce qui est spécifique à un fil conducteur particulier (stockage spécifique-fil, la pile de ce thread, toutes les exceptions lancées dans le contexte de ce thread et ainsi de suite). Lorsque vous avez différents contextes de threads dans la même application (plusieurs threads), vous devez synchroniser l'accès aux ressources créées dans un contexte de thread et accédées à partir de threads différents (en utilisant des primitives de verrouillage).

Par exemple, disons que vous avez a, une instance de class A [en cours d'exécution en fil tA] faire des trucs et b, une instance de class B [en cours d'exécution dans le contexte de fil tB] et b veut dire a quelque chose.

Le « veut dire quelque chose a » partie signifie que b veut appeler a.something() et a.something() seront appelés dans le cadre de tB (sur la pile de fil B).

Pour changer cela (pour avoir a.something() exécuté dans le contexte de tA), vous devez basculer le contexte de fil. Cela signifie qu'à la place de b racontant a "exécuter A::something()", b dit a "exécuter A :: quelque chose()` dans votre propre contexte de thread ".

étapes de mise en œuvre classiques:

  • b envoie un message à a à l'intérieur tB

  • a sondages pour les messages à partir de tA

  • Lorsque a trouve le message de b, il exécute a.something() lui-même, à l'intérieur de tA.

C'est la commutation des contextes de thread (l'exécution de A::something sera exécuté en tA au lieu de tB, comme cela aurait été si elle est appelée directement à partir b). À partir du lien que vous avez fourni, il semble que cela soit déjà implémenté par boost::asio::io_service, donc si vous l'utilisez, vous n'avez pas besoin de l'implémenter vous-même.

+0

+1: les bons points – neuro

+0

L'avertissement concernant les threads et les signaux peut être trouvé ici: http://www.gamedev.net/community/forums/topic.asp?topic_id=553476 Je ne comprends pas très bien ce que vous voulez dire par «changement de contexte de thread». Je crains que ce terme ne manque pas dans mon vocabulaire. – Jens

+0

@Jens, voir ma vérification ci-dessus. – utnapistim

7

Il y a un écueil très important:

Pour autant que je comprends signals2's thread safety les machines à sous sont exploités dans le fil de signalisation . La plupart des bibliothèques GUI (en particulier Qt et OpenGL) doivent dessiner toutes à partir d'un seul thread. Ce n'est pas un problème en général, mais cela demande un peu de soin. Vous avez deux options:

La première est que vous faites attention à ne pas faire de dessin à l'intérieur de GuiClass::slot_data_changed (puisque vous utilisez Qt jetez un coup d'œil à QCoreApplication :: postEvent (désolé de ne pas publier de lien vers le document Qt)). La seconde est que vous construisez une file d'attente de messages vous-même, ce qui enregistre les appels d'emplacement et les exécute dans le thread graphique. C'est un peu plus onéreux, mais aussi plus sûr, parce que vos classes GUI peuvent être écrites sans se préoccuper de la sécurité des threads.

+0

+1 pour deux alternatives bien décrites – Jens