QT多线程数据从主线程传递到工作线程

时间:2016-09-25 20:24:16

标签: multithreading qt

我在QT计划中使用多线程。我需要将数据传递给来自主gui线程的工作线程中的worker对象。我在QObject子类中创建了一个setData函数,以从主gui线程传递所有必要的数据。但是我通过查看setData函数中的QThread :: currentThreadId()验证了从主线程调用的函数。即使从主线程调用工作对象函数,这确保工作线程仍然拥有自己的数据副本,这是重入类所需的吗?请记住,这是在工作线程启动之前发生的。

此外,如果在没有动态内存的类中使用基本数据类型,并且只要该类的所有其他成员数据都是可重入的,那么该类是否可以重入? (它有可重入的数据成员,如qstrings,qlists等,另外还有基本的内容bools等)

感谢您的帮助

编辑新内容:

我的主要问题是,从主gui线程调用生成另一个线程的QObject子类方法是否合适,以便将我的数据传递给要处理的工作线程(在我的情况下,包含备份作业信息的自定义类)对于长期挂起的文件扫描和数据备份的副本)。数据传递都是在线程启动之前发生的,因此两个线程都没有立即修改数据的危险(我想但我不是多线程专家......)听起来这样做的方法就是使用你的帖子来使用从主线程到工作线程中的插槽的信号,用于传递数据。我已经确认我的数据备份作业是可重入的,所以我需要做的就是确保工作线程在它自己的这些类的实例上工作。当前通过调用QObject子类方法完成的数据传输也是在工作线程启动之前完成的 - 这是否会阻止竞争条件并且是否安全?

在“从其他线程访问QObject子类”一节下的here下,在QObject子类中使用插槽看起来有点危险......

好的,这是我最近忙的代码...... 编辑代码:

void Replicator::advancedAllBackup()
{
    updateStatus("<font color = \"green\">Starting All Advanced Backups</font>");
    startBackup();

    worker = new Worker;
    worker->moveToThread(workerThread);
    setupWorker(normal);

    QList<BackupJob> jobList;
    for (int backupCount = 0; backupCount < advancedJobs.size(); backupCount++)
        jobList << advancedJobs[backupCount];

    worker->setData(jobList);
    workerThread->start();
}

startBackup函数设置一些布尔值并更新gui。 setupWorker函数连接工作线程和工作对象的所有信号和插槽。 setData函数将worker作业列表数据设置为后端的数据,并在线程启动之前调用,因此没有并发。 然后我们启动线程并完成它的工作。

这是工人代码:

void setData(QList<BackupJob> jobs) { this->jobs = jobs; }

所以我的问题是:这样安全吗?

2 个答案:

答案 0 :(得分:3)

你的问题有一些误解。

可重入和多线程是正交概念。单线程代码可以很容易地强制应对重入 - 并且只要你重新进入事件循环(因此你不应该)。

你要问的问题是纠正,因此:如果数据成员支持多线程访问,那么类的方法是否是线程安全的?答案是肯定的。但这是一个无用的答案,因为您错误地认为您使用的数据类型支持此类访问。他们很可能不

事实上,除非您明确地寻找它们,否则您不太可能使用多线程安全数据类型。 POD类型不是,大多数C ++标准类型都不是,大多数Qt类型都不是。只是为了没有误解:QString不是多线程安全的数据类型!以下代码具有未定义的行为(它会崩溃,刻录并向您的配偶发送一封似乎来自非法爱人的电子邮件):

QString str{"Foo"};
for (int i = 0; i < 1000; ++i)
  QtConcurrent::run([&]{ str.append("bar"); });

后续问题可能是:

  • 我的数据成员是否支持多线程访问?我以为他们做到了。

    不,除非您显示其他证明的代码,否则它们不会。

  • 我是否需要支持多线程访问?

    也许。但是完全避免它的需要要容易得多。

与Qt类型相关的混淆的可能原因是它们的隐式共享语义。值得庆幸的是,他们与多线程的关系很简单:

  1. 可以在给定时间从任何一个线程访问任何Qt隐式共享类的实例。推论:每个线程需要一个实例。复制您的对象,并在其自己的线程中使用每个副本 - 这是非常安全的。这些实例最初可能共享数据,Qt将确保任何写入时复制都是线程安全的。

  2. 补充工具栏:如果在非const实例上使用迭代器或内部指针指向数据,则必须在构造迭代器/指针之前强制detach()对象。问题迭代器是当分离对象的数据时它们变得无效,并且在实例是非const的任何线程中都可能发生分离 - 所以至少有一个线程最终会出现无效的迭代器。我不再谈论这个了,重点是隐式共享数据类型很难实现和安全使用。使用C ++ 11,不再需要隐式共享:它们是C ++ 98中缺少移动语义的一种解决方法。

  3. 那是什么意思呢?这意味着:

    // Unsafe: str1 potentially accessed from two threads at once
    QString str1{"foo"};
    QtConcurrent::run([&]{ str1.apppend("bar"); });
    str1.append("baz");
    
    // Safe: each instance is accessed from one thread only
    QString str1{"foo"};
    QString str2{str1};
    QtConcurrent::run([&]{ str1.apppend("bar"); });
    str2.append("baz");
    

    原始代码可以修复:

    QString str{"Foo"};
    for (int i = 0; i < 1000; ++i)
      QtConcurrent::run([=]() mutable { str.append("bar"); });
    

    这并不是说这段代码非常有用:当在工作线程中破坏仿函数时,修改后的数据会丢失。但它有助于说明如何处理Qt值类型和多线程。这就是它的工作原理:构造仿函数的每个实例时都会获得str的副本。然后将该仿函数传递给要执行的工作线程,其中附加了字符串的副本。该副本最初与原始线程中的str实例共享数据,但QString将线程安全地复制数据。您可以明确地写出仿函数,以明确发生的事情:

    QString str{"Foo"};
    struct Functor {
      QString str;
      Functor(const QString & str) : str{str} {}
      void operator()() {
        str.append("bar");
      }
    };
    for (int i = 0; i < 1000; ++i)
      QtConcurrent::run(Functor(str));
    

    我们如何处理使用Qt类型进出工作对象的数据?当对象在工作线程中时,与对象的所有通信必须通过信号/插槽完成。 Qt将以线程安全的方式自动为我们复制数据,这样每个值的实例只能在一个线程中访问。 E.g:

    class ImageSource : public QObject {
      QImage render() {
        QImage image{...};
        QPainter p{image};
        ...
        return image;
      }
    public:
      Q_SIGNAL newImage(const QImage & image);
      void makeImage() {
        QtConcurrent::run([this]{
          emit newImage(render());
        });
      }
    };
    
    int main(int argc, char ** argv) {
      QApplication app...;
      ImageSource source;
      QLabel label;
      label.show();
      connect(source, &ImageSource::newImage, &label, [&](const QImage & img){
        label.setPixmap(QPixmap::fromImage(img));
      });
      source.makeImage();
      return app.exec();
    }
    

    源的信号和标签的线程上下文之间的连接是自动的。信号恰好在默认线程池中的工作线程中发出。在信号发射时,比较源线程和目标线程,如果不同,则将函数包裹在一个事件中,事件发布标签,标签的QObject::event将运行设置像素图的仿函数。这是所有线程安全的,并利用Qt使其几乎毫不费力。目标线程上下文&label非常重要:没有它,仿函数将在工作线程中运行,而不是在UI线程中运行。

    请注意,我们甚至不必将对象移动到工作线程:实际上,应该避免将QObject移动到工作线程,除非对象确实需要对事件做出反应并且只是生成一个数据。你通常想要移动,例如处理通信的对象,或从UI中抽象出来的复杂应用程序控制器。仅通过使用QtConcurrent::run使用{{1}}来完成数据生成,就可以抽象出将数据从工作线程提取到另一个线程的线程安全魔法。

答案 1 :(得分:1)

为了使用Qt的机制在具有队列的线程之间传递数据,您无法直接调用对象的函数。您需要使用signal/slot mechanism,或者您可以使用QMetaObject::invokeMethod来电:

 QMetaObject::invokeMethod(myObject, "mySlotFunction",
                           Qt::QueuedConnection,
                           Q_ARG(int, 42));

这只有在发送和接收对象都有事件队列运行时才会起作用 - 即主线程或基于QThread的线程。

有关问题的其他部分,请参阅有关重入的Qt文档部分: http://doc.qt.io/qt-4.8/threads-reentrancy.html#reentrant

  

许多Qt类都是可重入的,但它们不是线程安全的,   因为使它们成为线程安全的会产生额外的开销   反复锁定和解锁QMutex。例如,QString是   可重入但不是线程安全的。您可以安全地访问不同的   同时来自多个线程的QString实例,但是你   无法从多个线程安全地访问同一个QString实例   同时(除非你自己用一个保护访问   QMutex)。