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;
}
It seems to be sound. Can you provide a link to the other thread? Were they explaining their reasoning?
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_changedshould be called from all the other threads.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
GuiClassbe an abstract base class, implementing it’s own message queue. WhenGuiClass::slot_data_changedis 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 theGuiClassyou 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 abstractGuiClass).Advantages:
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 🙂
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.
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
joinfor all threads, use a boost::thread_group.Instead of:
you will have:
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 ofclass A[running in thread tA] doing some stuff andb, an instance ofclass B[running in the context of thread tB] andbwants to tellasomething.The “wants to tell
asomething” part means thatbwants to calla.something()anda.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 ofbtellinga“runA::something()“,btellsa“run A::something()` in your own thread context”.Classical implementation steps:
bsends a message toafrom within tBapolls for messages from within tAWhen
afinds the message fromb, it runs a.something() itself, within tA.This is the switching of threading contexts (the execution of
A::somethingwill be executed in tA instead of tB, as it would have been if called directly fromb).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.