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

时间:2010-06-11 10:02:04

标签: 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

代码如下:

#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;
}

2 个答案:

答案 0 :(得分:12)

  

这是信号和信号的组合吗?   一个明智的想法?我是另一个论坛   有人建议其他人不要   “走这条路”。

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

  

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

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

  

我的期望是否真实,使用我的GUI类提供Web界面或QT,VTK或任何窗口都会“轻松”?

这并不容易。要解决此问题,您必须使用通知切换线程上下文。这就是我要做的事情:

让您的GuiClass成为一个抽象基类,实现它自己的消息队列。当您的线程调用GuiClass::slot_data_changed时,您将锁定互斥锁并在内部(private:)消息队列中发布已接收通知的副本。在GuiClass的线程中,您创建了一个锁定互斥锁并在队列中查找通知的函数。此函数应该在客户端代码的线程中运行(在您从abstract 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();

你将拥有:

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

processors.join_all();

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

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

例如,假设您有aclass A [在线程tA中运行]执行某些操作的实例bclass B的实例[正在运行]在线程tB的上下文中,b想要告诉a某事。

“想要告诉a某事”部分意味着b想要调用a.something(),而a.something()将在tB的上下文中调用(在{线程B)。

要更改此项(要在tA的上下文中运行a.something()),您必须切换线程上下文。这意味着代替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实现,因此如果您使用该链接,则无需自行实施。

答案 1 :(得分:7)

有一个非常重要的陷阱:

据我了解signals2's thread safety,广告位在信号主题中运行。大多数GUI库(尤其是Qt和OpenGL)必须从单个线程完成所有绘图。一般来说这不是问题,但需要一点小心。您有两种选择:

首先,你要注意不要在GuiClass::slot_data_changed内进行任何绘图(因为你使用Qt看看QCoreApplication :: postEvent(抱歉不允许发布链接到Qt文档))。

第二个是您自己构建一个消息队列,它保存了插槽调用并在GUI线程中执行它们。这有点繁重,但也更安全,因为您的GUI类可以在不关心线程安全的情况下编写。