信号和线程 - 设计决策的好坏?

Jen*_*ens 6 c++ boost-thread boost-signals2

我必须编写一个执行高度计算密集型计算的程序.该程序可能会运行几天.可以在不同的线程中轻松分离计算,而无需共享数据.我想要一个GUI或一个Web服务,告诉我当前的状态.

我目前的设计使用BOOST :: signals2和BOOST :: thread.它编译并到目前为止按预期工作.如果一个线程完成一次迭代并且有新数据可用,则它会调用一个连接到GUI类中的插槽的信号.

我的问题:

  • 这种信号和线程的组合是一个明智的想法吗?我是另一个论坛,有人建议别人不要"走这条路".
  • 附近是否有潜在的致命陷阱,我没有看到?
  • 我期望的现实,这将是"容易"用我的GUI类来提供Web界面或QT,一个VTK或任何窗口?
  • 有没有更聪明的选择(像其他升压库),我忽略了?

以下代码编译

g++ -Wall -o main -lboost_thread-mt <filename>.cpp
Run Code Online (Sandbox Code Playgroud)

代码如下:

#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;
}
Run Code Online (Sandbox Code Playgroud)

utn*_*tim 13

这种信号和线程的组合是一个明智的想法吗?我是另一个论坛,有人建议别人不要"走这条路".

这似乎是合理的.你能提供其他线程的链接吗?他们在解释他们的推理吗?

附近是否有潜在的致命陷阱,我没有看到?

如果他们是我也看不到他们.您需要注意的是通知是线程安全的(触发信号不会切换线程上下文,GuiClass::slot_data_changed应该从所有其他线程调用.

我期望的现实,这将是"容易"用我的GUI类来提供Web界面或QT,一个VTK或任何窗口?

没那么简单.要解决此问题,您必须使用通知切换线程上下文.这就是我要做的事情:

让你GuiClass成为一个抽象的基类,实现它自己的消息队列.当GuiClass::slot_data_changed您的线程调用时,您将锁定互斥锁并在内部(private:)消息队列上发布已接收通知的副本.在您的线程中GuiClass创建一个锁定互斥锁并在队列中查找通知的函数.此函数应该在客户端代码的线程中运行(在您从抽象中专门化的具体类的线程中GuiClass).

好处:

  • 您的基类封装并隔离线程上下文切换,透明地对其特化.

缺点:

  • 您的客户端代码必须运行轮询方法或允许它运行(作为线程处理函数).

  • 这有点复杂:)

我期望的现实,这将是"容易"用我的GUI类来提供Web界面或QT,一个VTK或任何窗口?

看不到,但这并不容易.除了线程上下文切换之外,我可能还有其他问题.

有没有更聪明的选择(像其他升压库),我忽略了?

不是其他的升级库,但是编写线程的方式并不好:连接是在代码中按顺序进行的.要只join为所有线程使用一个,请使用boost :: thread_group.

代替:

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();
Run Code Online (Sandbox Code Playgroud)

你将会有:

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

processors.join_all();
Run Code Online (Sandbox Code Playgroud)

编辑:线程上下文是特定于特定运行线程的所有内容(特定于线程的存储,该线程的堆栈,该线程的上下文中抛出的任何异常等).

当您在同一个应用程序(多个线程)中有各种线程上下文时,您需要同步访问在线程上下文中创建并从不同线程访问的资源(使用锁定原语).

例如,假设你有a一个class A[在线程tA中运行]做一些事情b的实例,以及class B[在线程tB的上下文中运行]的一个实例,并且b想要说出a一些东西.

"想要告诉a某事"部分意味着b想要调用a.something()并将a.something()在tB的上下文中调用(在线程B的堆栈上).

要更改此项(要a.something()在tA的上下文中运行),您必须切换线程上下文.这意味着,而不是b告诉a"运行A::something()",b讲述a"运行A ::东西()在`你自己的线程上下文".

经典实施步骤:

  • ba从tB内发送消息

  • a 来自tA内的消息的民意调查

  • a从中发现消息时b,它在tA内运行a.something()本身.

这是线程上下文的切换(执行A::something将以tA而不是tB执行,就像直接从中调用一样b).

从您提供的链接,似乎已经实现了boost::asio::io_service,因此如果您使用它,您不必自己实现它.


Fab*_*ssi 7

有一个非常重要的陷阱:

据我所知,signal2的线程安全性,插槽在信令线程中运行.大多数GUI库(尤其是Qt和OpenGL)必须从单个线程完成所有绘图.一般来说这不是问题,但需要一点小心.您有两种选择:

首先,你要小心不要在里面做任何绘图GuiClass::slot_data_changed(因为你使用Qt看看QCoreApplication :: postEvent(抱歉不允许发布链接到Qt文档)).

第二个是你自己构建一个消息队列,它保存了插槽调用并在GUI线程中执行它们.这有点繁琐,但也更安全,因为您可以编写GUI类而无需关心线程安全性.