【问题标题】:QThread: safe way of modifying a variable from different thread?QThread:从不同线程修改变量的安全方法?
【发布时间】:2013-09-28 00:32:24
【问题描述】:

我是 QThread 和多线程的新手,所以我不确定我是否做得正确。该程序到目前为止还没有崩溃,但我想检查我是否正确地执行它。我有一些代码如下(MyThreadClass 继承自 QThread):

std::vector<MyThreadClass* > workThreads;
for(int i=0;i<Solutions.size();i++)
{
    workThreads.push_back(new MyThreadClass(Solutions[i]));
}
for(int i=0;i<workThreads.size();i++)
{
    connect(workThreads[i], SIGNAL(finished()), this, SLOT(onFinished()));
    workThreads[i]->start();
}

bool finished = false;
while(!finished)
{
    if(m_finishedThread==workThreads.size())
        finished=true;

    this->msleep(10);
}

onFinished函数给出如下:

void MyClass::onFinished()
{
    ++m_finishedThread;
}

因此您可以看到 while 循环正在等待所有线程完成并更新 m_finishedThread 变量。这是一种安全的方法吗?如果所有线程都完成工作并尝试“连接”到 onFinished() 函数,会不会导致问题?

【问题讨论】:

    标签: c++ multithreading qt qthread


    【解决方案1】:

    这很安全。诀窍是您将在 QThread 子类和 this 之间建立的连接将是一个 queued 连接。那是因为发出信号finished (*) 的线程与this(接收者)所在的线程不同。

    排队连接是通过事件发布实现的。一个特殊事件被发布到线程this所在的事件队列中;该事件的处理正在调用您的插槽。因此,您的插槽只会被访问

    1. 依次
    2. 仅来自一个线程:this 生活在其中

    这显然是线程安全的。

    顺便问一下,那是你的真实代码吗?你可以做同样的事情

    for (int i = 0; i < numThreads; ++i) 
        threads[i]->wait();
    

    (*) 我说的是发出信号的线程,而不是发出信号的对象的亲和性。特别是,您的 QThread 子类对象很可能与this 生活在同一个线程中!

    问题在哪里?发出finished 的线程不是那些对象(和this)所在的线程——是那些对象管理 的线程。


    附录(2):this是实现信号发射的代码。如您所见,检查是针对当前运行的线程与接收者所在的线程进行的。不考虑发送者所在的线程,因此您不需要需要执行thread-&gt;moveToThread(thread) 之类的操作来建立排队连接。

    当前运行的线程怎么可能不是发送者所在的线程?因为这正是 QThread 所做的:QThread 对象存在于一个线程中,它管理的线程(并发出finished()!)是另一个线程:

    // runs in the MANAGED thread; lives in any thread
    void QThread::run_in_another_thread() {
        run(); // user-supplied implementation
        emit finished();
    }
    

    附录的附录:但是,您是否依赖于 finished 从另一个线程发出的未记录知识?那不是靠实现吗?

    答案是否。只有一个其他选择:finished() 作为收割/清理处理的一部分发出,来自 QThread 对象正在离开的事件循环。也就是说,同一个线程this 生活在其中。

    它不能来自其他任何地方——如果我们正在运行用户代码,我们就不会运行其他任何东西(请记住,一个线程不能运行两个 不同的东西)。

    这意味着:

    1. 在this 所在的线程中有一个正在运行的事件循环。无论如何你都需要这个。
    2. 调用将是“直接”的(即直接调用),因为发出信号的线程与this 所在的线程相同。所以,我们只是在同一线程中调用一个函数 /em>;根据定义,这是线程安全的。

    因此,这完全不会改变我们处理此问题的方式。它需要this 中的活动事件循环,仅此而已。


    附录:我应该更好地阅读代码。这不会改变我上面所说的,但是:

    bool finished = false;
    while(!finished)
    {
        if(m_finishedThread==workThreads.size())
            finished=true;
    
        this->msleep(10);
    }
    

    这里的循环永远不会返回到事件循环。这意味着您的插槽将永远不会被调用,因为元调用事件将永远不会被处理。请使用QTimer 或偷偷调用QCoreApplication::processEvents 而不是这种循环!

    【讨论】:

    • 但 OP 没有创建排队连接。
    • 是的,他是。我解释了原因。
    • 在这种情况下,需要显式地创建一个Qt::QueuedConnection,但是代码没有指定连接类型。这意味着连接将是 Qt::AutoConnection 类型,而后者又默认为 Qt::DirectConnection,因为 MyThreadClass 的线程亲和性是主线程,因为没有调用 moveToThread()。因此代码将不是线程安全的,因为插槽将在主线程中执行。
    • 感谢连接部分的解释。我不确定threads[i]->wait() 是否会给出我想做的事情。我试图让计算在单独的线程上运行以加快处理时间。我忘记输入的是每个线程将采用不同的“Solution[i]”值来给出不同的结果。所有结果都写入一个向量(用互斥锁保护它)。
    • @VíctorFernández:再次,不。您假设选择着眼于 QThread 对象与this 的亲和性。事实并非如此,我解释了原因。
    【解决方案2】:

    我可能不会运行 while(!finished) 循环,因为这会挂起您的主线程(或 while 循环的执行线程)。而是在 onFinished() 中检查正在运行的线程,并在一切完成时发出信号。

    如果您需要等待所有线程完成,仍然存在 wait()-Condition。

    要修改变量,如果变量不可直接访问,您可以使用带有信号的 Qt:QueuedConnection 来修改变量。

    直接从多个线程访问变量时要小心。从不同线程访问同一个变量时,使用 QMutex 通常是个好主意。

    【讨论】:

    • 感谢您的建议。我忘记提到的一件事是所有代码实际上都在另一个单独的线程上运行。这就是为什么我不介意使用 while 循环阻塞执行(即使这可能不是一个好习惯)。
    【解决方案3】:

    首先,你不应该直接继承QThread。看: https://www.qt.io/blog/2010/06/17/youre-doing-it-wrong

    改为创建一个普通类(QObject 或类似的子类)并在那里实现您的代码。然后使用moveToThread() 将子类移动到该线程。

    使用Qt::QueuedConnection 将信号连接到对象中的槽是安全的,只要传递的参数不是对象并且您没有在没有任何同步机制的情况下调用这些对象的方法。

    无论如何,使用QtConcurrent 可能更容易实现您想要做的事情。

    【讨论】:

    • 我完全同意 QThread 不应该被继承,除非你想改变底层的线程实现。但是,对于 QtConcurrent 的未来似乎存在疑问。见这里:comments.gmane.org/gmane.comp.lib.qt.devel/7942
    • 根据情况子类QThread是完全合法的(单调用长时间运行操作是子类QThread的一个例子,即图像识别和处理)。至于短时间的独立动作 qt::concurrent 更合适,而永久运行的线程应该实现为驻留在 QThread 中的对象。
    • 是的,它是合法的,有时是最好的选择,但 95% 的时间它被滥用。这就是为什么我认为一般来说它不应该被子类化,除非你 100% 确定你做对了。
    • @SebastianLange,我不同意你关于何时继承 QThread 的观点。 QThread 根本不是一个线程,而是一个线程控制器,只有在你想改变线程被处理(控制)的方式时才应该被子类化。它不是为了添加要在线程上运行的任务而设计的子类。请注意,即使是 Qt 架构师也声称这一点并声明 Qt 示例是错误的!
    • 这是个人观点。一般来说,给定的帖子“你做错了”开始了某种反线程子类化运动。但是在某些情况下仍然更喜欢继承 QThread,因为它会产生更少的开销和更容易的阻塞操作。只有在不需要事件队列时才应该这样做。大多数情况下,您不必将 QThread 子类化,但仍有一些情况您希望 QThread 被子类化。还有一个anti-anti-thread-subclass的帖子:woboq.com/blog/qthread-you-were-not-doing-so-wrong.html
    【解决方案4】:

    乍一看,我会说不。实际上,您已经连接了线程 finished() 信号,而没有指定 connect 的最后一个参数(即 ConnectionType )

    从文档中可以看出,如果不指定这个参数,Qt::AutoConnection是默认的,并指定如果信号来自同一个线程中的对象,或者在一个不同的线程。

    在这种情况下,Qt::QueuedConnection 将用于这些调用,这意味着当信号将触发对插槽的调用时,此调用将在接收线程的事件循环中排队。如果多个线程同时完成,对onFinished()的调用将被排队,并被一一调用。

    编辑:此外,我建议您考虑一种基于线程的不同设计方法,并阅读this,其中说您应该创建一个工作对象,然后将其移至线程。

    【讨论】:

    • 吹毛求疵:没有“默认为”——类型是根据每次排放选择的。
    • @peppe 我的意思是,在他的情况下,Qt 会为这些调用选择 QueuedConnection。
    【解决方案5】:

    不,看起来没问题,Qt 的信号/槽系统是线程安全的,实际上,当信号发出时,它被放在队列的末尾,并由主事件循环按此顺序处理。

    【讨论】:

    • 小心。如果您不注意,您完全可以使用 Qt 信号/插槽系统获得线程不安全的代码......
    • 感谢您的回答! @JBL 你能给我一个 QT 信号/槽系统的线程不安全情况吗?
    • 那是错误的。取决于连接类型。
    • @JohnYang 简单:将来自某个线程的信号连接到某个其他线程的插槽,并使用Qt::DirectConnection 作为连接类型:您现在可能有线程不安全的代码。
    猜你喜欢
    • 2011-07-07
    • 1970-01-01
    • 2017-10-05
    • 2021-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多