信号和线程 - 好还是坏的设计决策?
我必须编写一个执行高度计算密集型计算的程序。该程序可能会运行几天。 计算可以很容易地分离在不同的线程中,而不需要共享数据。 我想要一个 GUI 或 Web 服务来通知我当前状态。
我当前的设计使用 BOOST::signals2 和 BOOST::thread。 它编译并到目前为止按预期工作。 如果线程完成一次迭代并且有新数据可用,它会调用连接到 GUI 类中的槽的信号。
我的问题:
- 信号和线程的这种组合是一个明智的想法吗?在另一个论坛上,有人建议其他人不要“走这条路”。
- 附近是否存在我没有发现的潜在致命陷阱?
- 我的期望是否现实,即使用我的 GUI 类来提供 Web 界面或 QT、VTK 或任何窗口会“容易”?
- 是否有我忽略的更聪明的替代方案(如其他升压库)?
以下代码编译后
g++ -Wall -o main -lboost_thread-mt <filename>.cpp
代码如下:
#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;
}
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;
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
看来是有道理的。您能提供另一个线程的链接吗?他们有解释他们的理由吗?
如果是的话,我也看不到他们。您需要注意的是通知是线程安全的(信号的触发不会切换线程上下文,应该从所有其他线程调用您的
GuiClass::slot_data_changed
。这并不容易。要解决此问题,您必须使通知切换线程上下文。这就是我要做的:
让您的
GuiClass
成为一个抽象基类,实现它自己的消息队列。当您的线程调用GuiClass::slot_data_changed
时,您会锁定互斥体并将收到的通知的副本发布到内部(private:
)消息队列上。在GuiClass
的线程中,您创建一个函数来锁定互斥体并在队列中查找通知。该函数应该在客户端代码的线程中运行(在您从抽象GuiClass
专门化的具体类的线程中)。优点:
缺点:
您的客户端代码必须运行轮询方法或允许它运行(作为线程处理函数)。
有点复杂:)
不这么看,但这并不那么容易。除了线程上下文切换之外,我目前可能还缺少其他问题。
不是其他 boost 库,而是您编写线程的方式不好:连接是在代码中按顺序进行的。要让所有线程只有一个
join
,请使用 boost::thread_group。而不是:
您将拥有:
编辑:线程上下文是特定于特定运行线程的所有内容(特定于线程的存储、该线程的堆栈、该线程上下文中抛出的任何异常等)在)。
当同一应用程序中有不同的线程上下文(多个线程)时,您需要同步对在线程上下文中创建的资源的访问以及从不同线程访问的资源(使用锁定原语)。
例如,假设您有
a
,一个A 类
的实例 [在线程 tA 中运行] 执行一些操作,以及b
,它是一个B 类
[在线程 tB 的上下文中运行] 和b
想要告诉a
一些事情。“想要告诉
a
某事”部分意味着b
想要调用a.something()
和a.something()
将在 tB 的上下文中(在线程 B 的堆栈上)调用。要更改此设置(让
a.something()
在 tA 的上下文中运行),您必须切换线程上下文。这意味着b
不是告诉a
“运行A::something()
”,而是b
告诉a
“在您自己的线程上下文中运行 A::something()`”。经典实现步骤:
b
从 tB 内部向a
发送消息a
轮询 tA 内的消息当
a 时
找到来自b
的消息,它会在 tA 内运行 a.something() 本身。这是线程上下文的切换(
A::something
的执行将在 tA 而不是 tB 中执行,就像直接从b
调用一样)。从您提供的链接来看,这似乎已经由
boost::asio::io_service
实现了,所以如果您使用它,则不必自己实现它。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_changed
should 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
GuiClass
be an abstract base class, implementing it's own message queue. WhenGuiClass::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 theGuiClass
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 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
join
for 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] andb
wants to tella
something.The "wants to tell
a
something" part means thatb
wants 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 ofb
tellinga
"runA::something()
",b
tellsa
"run A::something()` in your own thread context".Classical implementation steps:
b
sends a message toa
from within tBa
polls for messages from within tAWhen
a
finds the message fromb
, 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 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.有一个非常重要的陷阱:
据我了解 signals2 的线程安全性 槽在信令线程中运行。大多数 GUI 库(尤其是 Qt 和 OpenGL)必须从单个线程完成所有绘图。这一般来说是没有问题的,但是需要注意一下。你有两个选择:
第一个是你要小心,不要在
GuiClass::slot_data_changed
内部进行任何绘图(因为你使用 Qt 看看 QCoreApplication::postEvent (抱歉不允许发布 Qt 文档的链接))。第二种是您自己构建一个消息队列,该队列保存槽调用并在 GUI 线程中执行它们。这有点麻烦,但也更安全,因为你的 GUI 类可以在不关心线程安全的情况下编写。
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.