Сигналы и потоки — хорошее или плохое дизайнерское решение?

Мне нужно написать программу, которая выполняет вычисления, требующие больших вычислительных ресурсов. Программа может работать несколько дней. Расчет можно легко разделить на разные потоки без необходимости использования общих данных. Мне нужен графический интерфейс или веб-служба, которая информирует меня о текущем состоянии.

Мой текущий дизайн использует BOOST::signals2 и BOOST::thread. Он компилируется и пока работает так, как ожидалось. Если поток завершил одну итерацию и доступны новые данные, он вызывает сигнал, который подключается к слоту в классе GUI.

Мои вопросы):

  • Является ли эта комбинация сигналов и потоков разумной идеей? На другом форуме кто-то посоветовал кому-то не "идти по этой дороге".
  • Есть ли поблизости потенциальные смертельные ловушки, которые я не заметил?
  • Реалистично ли мое ожидание, что будет «легко» использовать мой класс GUI для предоставления веб-интерфейса или 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;
}

person Jens    schedule 11.06.2010    source источник
comment
Хотя этот конкретный пример в порядке, вы должны быть осторожны, поскольку вы не защищаете свой вектор с помощью мьютекса в функции out   -  person nos    schedule 11.06.2010


Ответы (2)


Является ли эта комбинация сигналов и потоков разумной идеей? На другом форуме кто-то посоветовал кому-то не "идти по этой дороге".

Вроде бы звук. Можете дать ссылку на другую ветку? Объяснили ли они свои рассуждения?

Есть ли поблизости потенциальные смертельные ловушки, которые я не заметил?

Если они есть, я их тоже не вижу. Вам нужно позаботиться о том, чтобы уведомления были потокобезопасными (срабатывание сигнала не переключает контексты потоков, ваш GuiClass::slot_data_changed должен вызываться из всех других потоков.

Реалистично ли мое ожидание, что будет «легко» использовать мой класс GUI для предоставления веб-интерфейса или QT, VTK или любого другого окна?

Это будет непросто. Чтобы исправить это, вам нужно сделать так, чтобы ваши уведомления переключали контексты потоков. Вот что я бы сделал:

Пусть ваш GuiClass будет абстрактным базовым классом, реализующим собственную очередь сообщений. Когда ваши потоки вызывают GuiClass::slot_data_changed, вы блокируете мьютекс и отправляете копию полученного уведомления во внутреннюю (private:) очередь сообщений. В потоке GuiClass вы создаете функцию, которая блокирует мьютекс и ищет уведомления в очереди. Эта функция должна выполняться в потоке клиентского кода (в потоке конкретных классов, которые вы специализируете на абстрактном GuiClass).

Преимущества:

  • ваш базовый класс инкапсулирует и изолирует переключение контекста потока, прозрачно для его специализации.

Недостатки:

  • ваш клиентский код должен либо запустить метод опроса, либо разрешить его запуск (как функцию обработки потока).

  • это немного сложно :)

Реалистично ли мое ожидание, что будет «легко» использовать мой класс GUI для предоставления веб-интерфейса или 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();

Редактировать. Контекст потока — это все, что относится к конкретному запущенному потоку (хранилище для конкретного потока, стек этого потока, любые исключения, возникающие в контексте этого потока, и т. д.).

Когда у вас есть разные контексты потока в одном приложении (несколько потоков), вам необходимо синхронизировать доступ к ресурсам, созданным в контексте потока, и доступ к ним из разных потоков (используя примитивы блокировки).

Например, допустим, у вас есть 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::something()` в контексте вашего собственного потока".

Классические этапы реализации:

  • b отправляет сообщение a из tB

  • a опросы для сообщений из tA

  • Когда a находит сообщение от b, он сам запускает a.something() внутри tA.

Это переключение контекстов потоков (выполнение A::something будет выполняться в tA вместо tB, как это было бы, если бы вызывалось напрямую из b).

По предоставленной вами ссылке кажется, что это уже реализовано boost::asio::io_service, поэтому, если вы используете это, вам не нужно реализовывать это самостоятельно.

person utnapistim    schedule 11.06.2010
comment
Предупреждение о тредах и сигналах можно найти здесь: gamedev.net/community /forums/topic.asp?topic_id=553476 Я не совсем понимаю, что вы имеете в виду под термином «переключение контекста потока». Боюсь, этот термин просто отсутствует в моем лексиконе. - person Jens; 11.06.2010

Есть один очень важный подводный камень:

Насколько я понимаю безопасность потоков signal2 слоты запускаются в потоке signalling. Большинство библиотек графического интерфейса (особенно Qt и OpenGL) должны выполнять все отрисовки в одном потоке. В целом это не проблема, но требует осторожности. У вас есть два варианта:

Во-первых, вы должны быть осторожны, чтобы не делать никаких рисунков внутри GuiClass::slot_data_changed (поскольку вы используете Qt, взгляните на QCoreApplication::postEvent (извините, не разрешено публиковать ссылку на документ Qt)).

Во-вторых, вы сами создаете очередь сообщений, которая сохраняет вызовы слотов и выполняет их в потоке графического интерфейса. Это несколько более обременительно, но и безопаснее, потому что ваши GUI-классы можно писать, не заботясь о безопасности потоков.

person Fabio Fracassi    schedule 11.06.2010