views:

189

answers:

3

I have to write a program that performs highly computationally intensive calculations. The program might run for several days. The calculation can be separated easily in different threads without the need of shared data. I want a GUI or a web service that informs me of the current status.

My current design uses BOOST::signals2 and BOOST::thread. It compiles and so far works as expected. If a thread finished one iteration and new data is available it calls a signal which is connected to a slot in the GUI class.

My question(s):

  • Is this combination of signals and threads a wise idea? I another forum somebody advised someone else not to "go down this road".
  • Are there potential deadly pitfalls nearby that I failed to see?
  • Is my expectation realistic that it will be "easy" to use my GUI class to provide a web interface or a QT, a VTK or a whatever window?
  • Is there a more clever alternative (like other boost libs) that I overlooked?

following code compiles with

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

code follows:

#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;
}
+4  A: 

Is this combination of signals and threads a wise idea? I another forum somebody advised someone else not to "go down this road".

It seems to be sound. Can you provide a link to the other thread? Were they explaining their reasoning?

Are there potential deadly pitfalls nearby that I failed to see?

If they are I fail to see them also. What you need to take care of is that the notifications are thread-safe (the triggering of the signal doesn't switch thread contexts, to your GuiClass::slot_data_changed should be called from all the other threads.

Is my expectation realistic that it will be "easy" to use my GUI class to provide a web interface or a QT, a VTK or a whatever window?

It will not be easy. To fix this, you'd have to make your notification switch threading contexts. Here's what I would do:

Have your GuiClass be an abstract base class, implementing it's own message queue. When GuiClass::slot_data_changed is called by your threads, you lock a mutex and post a copy of the received notification on an internal (private:) message queue. In the thread of the GuiClass you create a function that locks the mutex and looks for notifications in the queue. This function should run in the client code's thread (in the thread of the concrete classes you specialize from the abstract GuiClass).

Advantages:

  • your base class encapsulates and isolates the thread context switching, transparently to it's specializations.

Disadvantages:

  • your client code has to either run the polling method or allow it to run (as a thread-processing function).

  • it's a bit complicated :)

Is my expectation realistic that it will be "easy" to use my GUI class to provide a web interface or a QT, a VTK or a whatever window?

Doesn't see so, but it's not so easy. Besides the thread context-switching there may be other issues I'm missing at the moment.

Is there a more clever alternative (like other boost libs) that I overlooked?

Not other boost libs, but the way you wrote your threads is not good: the joins are made sequentially in your code. To have only one join for all threads, use a boost::thread_group.

Instead of:

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

you will have:

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

processors.join_all();

Edit: A thread context is everything that is specific to a particular running thread (thread-specific storage, the stack of that thread, any exceptions thrown in that thread's context and so on).

When you have various thread contexts in the same application (multiple threads) you need to synchronize access to resources created within a thread context and accessed from different threads (using locking primitives).

For example, let's say you have a, an instance of class A [running in thread tA] doing some stuff and b, an instance of class B [running in the context of thread tB] and b wants to tell a something.

The "wants to tell a something" part means that b wants to call a.something() and a.something() will be called in the context of tB (on the stack of thread B).

To change this (to have a.something() run in the context of tA), you have to switch the thread context. This means that instead of b telling a "run A::something()", b tells a "run A::something()` in your own thread context".

Classical implementation steps:

  • b sends a message to a from within tB

  • a polls for messages from within tA

  • When a finds the message from b, it runs a.something() itself, within tA.

This is the switching of threading contexts (the execution of A::something will be executed in tA instead of tB, as it would have been if called directly from b).

From the link you provided, it seems this is already implemented by boost::asio::io_service, so if you use that, you don't have to implement it yourself.

utnapistim
+1 : good points
neuro
The warning about threads and signals can be found here: http://www.gamedev.net/community/forums/topic.asp?topic_id=553476I don't quite understand what you are meaning with the term 'thread context switching'. I fear this term is just missing in my vocabulary.
Jens
@Jens, see my edit above.
utnapistim
A: 

You can google the term 'thread context switching' or you can read here

+1  A: 

There is one very important pitfall:

As far as I understand signals2's thread safety the slots are run in the signalling thread. Most GUI libraries (especially Qt and OpenGL) must do all drawing from a single thread. This is no problem in general, but it takes a bit of care. You have two options:

The first is that you are careful that you do not do any drawing inside of GuiClass::slot_data_changed (since you use Qt have a look at QCoreApplication::postEvent ( sorry not allowed to post link to Qt docu)).

The second is that you build a message queue yourself, which saves the slot invocations and executes them in the GUI thread. This is somewhat more onerous, but also safer, because your GUI Classes can be written without caring about thread safety.

Fabio Fracassi
+1 for two well described alternatives
Jens