0

本文档中,QMutex 用于保护“数字”不被多个线程同时修改。我有一个代码,其中一个线程被指示根据另一个线程设置的标志做不同的工作。

//In thread1
if(flag)
   dowork1;
else
   dowork2;

//In thread2
void setflag(bool f)
{
    flag=f;
}

我想知道是否需要 QMutex 来保护标志,即

//In thread1
mutex.lock();
if(flag)
{
   mutex.unlock();
   dowork1;
}
else
{
   mutex.unlock();
   dowork2;
}

//In thread2
void setflag(bool f)
{
    mutex.lock();
    flag=f;
    mutex.unlock();
}

该代码与文档的不同之处在于两个线程中通过单个语句访问(读取/写入)标志,并且只有一个线程修改标志的值。

PS: 我总是在多线程编程教程中看到一个线程执行“count++”,另一个线程执行“count--”的示例,并且教程说您应该使用互斥锁来保护变量“count”。我无法理解使用互斥锁的意义。是否意味着单条语句“count++”或“count--”的执行可以在中间被打断并产生意想不到的结果?能得到什么意想不到的结果?

4

5 回答 5

2

是否意味着单条语句“count++”或“count--”的执行可以在中间被打断并产生意想不到的结果?能得到什么意想不到的结果?

只是回答这部分:是的,执行可以在语句中间中断。

让我们想象一个简单的案例:

class A {
    void foo(){
        ++a;
    }
    int a = 0;
};

单个语句++a在汇编中被翻译为

    mov     eax, DWORD PTR [rdi]
    add     eax, 1
    mov     DWORD PTR [rdi], eax

这可以看作

eax = a;
eax += 1;
a = eax;

如果在 2 个不同线程的同一实例foo()调用(无论是在单个内核上还是在多个内核上),您无法预测程序的结果。A

它可以表现得很好:

thread 1 > eax = a  // eax in thread 1 is equal to 0
thread 1 > eax += 1 // eax in thread 1 is equal to 1
thread 1 > a = eax  // a is set to 1
thread 2 > eax = a  // eax in thread 2 is equal to 1
thread 2 > eax += 1 // eax in thread 2 is equal to 2
thread 2 > a = eax  // a is set to 2

或不:

thread 1 > eax = a  // eax in thread 1 is equal to 0
thread 2 > eax = a  // eax in thread 2 is equal to 0
thread 2 > eax += 1 // eax in thread 2 is equal to 1
thread 2 > a = eax  // a is set to 1
thread 1 > eax += 1 // eax in thread 1 is equal to 1
thread 1 > a = eax  // a is set to 1

在一个定义良好的程序中,N 次调用foo()应该导致a == N. 但是从多个线程调用foo()同一个实例会产生未定义的行为。A没有办法知道aN 次调用后的值foo()。这取决于你如何编译你的程序,使用了哪些优化标志,使用了哪个编译器,你的 CPU 的负载是多少,你的 CPU 的核心数,......

注意

class A {
public:
     bool check() const { return a == b; }
     int get_a() const { return a; }
     int get_b() const { return b; }
     void foo(){
        ++a;
         ++b;
     }
 private:
     int a = 0;
     int b = 0;
 };

现在我们有一个类,对于外部观察者来说,它始终保持a相等b。优化器可以将此类优化为:

class A {
public:
     bool check() const { return true; }
     int get_a() const { return a; }
     int get_b() const { return b; }
     void foo(){
         ++a;
         ++b;
     }
 private:
     int a = 0;
     int b = 0;
 };

因为它不会改变程序的可观察行为。

但是,如果您通过从多个线程调用 A 的同一实例上的 foo() 来调用未定义的行为,您最终可能会出现 if a = 3b = 2并且check()仍然返回true。你的代码失去了意义,程序没有做它应该做的事情,并且可以做任何事情。

从这里您可以想象更复杂的情况,例如如果 A 管理网络连接,您最终可以将客户端 #10 的数据发送到客户端 #6。如果您的程序在工厂中运行,您最终可能会激活错误的工具。

如果你想定义未定义的行为,你可以看这里:https : //en.cppreference.com/w/cpp/language/ub 和 C++ 标准为了更好地理解 UB,你可以寻找关于该主题的 CppCon 会谈.

于 2020-09-18T18:07:29.697 回答
1

对于从多个线程访问的任何标准对象(包括bool),其中至少一个线程可以修改对象的状态,您需要使用互斥锁来保护对该对象的访问,否则您将调用未定义的行为。

作为一个实际问题,对于一个bool未定义的行为可能不会以崩溃的形式出现,但更有可能以线程 B 的形式出现,由于缓存和 /或优化问题(例如优化器“知道” bool 在函数调用期间不能更改,因此它不会多次检查它)

如果您不想使用互斥锁来保护您的访问,另一种选择是flag从 a更改boolstd::atomic<bool>;该std::atomic<bool>类型具有您正在寻找的语义,即它可以从任何线程读取和/或写入,而不会调用未定义的行为。

于 2020-09-15T12:58:51.763 回答
1

对我来说,在这里使用互斥锁似乎更方便。一般来说,共享引用时不使用互斥锁可能会导致问题。在这里使用互斥锁的唯一缺点似乎是,您会稍微降低性能,因为您的线程必须相互等待。

会发生什么样的错误? 就像评论中的某个人所说,如果您共享基本数据类型,例如 int、bool、float 或对象引用,情况会有所不同。我添加了一些 qt 代码示例,其中强调了在不使用互斥锁期间可能出现的 2 个问题。问题 #3 是一个基本问题,Benjamin T 和他的好答案详细描述了这个问题。

块引用

主文件

#include <QCoreApplication>
#include <QThread>
#include <QtDebug>
#include <QTimer>
#include "countingthread.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    int amountThread = 3;
    int counter = 0;
    QString *s = new QString("foo");
    QMutex *mutex = new QMutex();

    //we construct a lot of thread
    QList<CountingThread*> threadList;

    //we create all threads
   for(int i=0;i<amountThread;i++)
   {
    CountingThread *t = new CountingThread();

#ifdef TEST_ATOMIC_VAR_SHARE
        t->addCounterdRef(&counter);
#endif
#ifdef TEST_OBJECT_VAR_SHARE
        t->addStringRef(s);
        //we add a mutex, which is shared to read read write
        //just used with TEST_OBJECT_SHARE_FIX define uncommented
        t->addMutexRef(mutex);
#endif
    //t->moveToThread(t);
    threadList.append(t);
   }

   //we start all with low prio, otherwise we produce something like a fork bomb
   for(int i=0;i<amountThread;i++)
        threadList.at(i)->start(QThread::Priority::LowPriority);

    return a.exec();
}

计数线程.h

#ifndef COUNTINGTHREAD_H
#define COUNTINGTHREAD_H

#include <QThread>
#include <QtDebug>
#include <QTimer>
#include <QMutex>

//atomic var is shared
//#define TEST_ATOMIC_VAR_SHARE

//more complex object var is shared
#define TEST_OBJECT_VAR_SHARE

// we add the fix
#define TEST_OBJECT_SHARE_FIX

class CountingThread : public QThread
{
    Q_OBJECT
    int *m_counter;
    QString *m_string;
    QMutex *m_locker;
public :
    void addCounterdRef(int *r);
    void addStringRef(QString  *s);
    void addMutexRef(QMutex  *m);
    void run() override;
};

#endif // COUNTINGTHREAD_H

计数线程.cpp

#include "countingthread.h"

void CountingThread::run()
{
    //forever
    while(1)
    {
#ifdef TEST_ATOMIC_VAR_SHARE
        //first use of counter
        int counterUse1Copy=  (*m_counter);
        //some other operations, here sleep 10 ms
        this->msleep(10);
        //we will retry to use a second time
        int counterUse2Copy=  (*m_counter);
        if(counterUse1Copy != counterUse2Copy)
                  qDebug()<<this->thread()->currentThreadId()<<" problem #1 found, counter not like we expect";
        //we increment afterwards our counter
        (*m_counter) +=1; //this works for fundamental types, like float, int, ...
#endif

#ifdef TEST_OBJECT_VAR_SHARE

#ifdef TEST_OBJECT_SHARE_FIX
            m_locker->lock();
#endif
            m_string->replace("#","-");
            //this will crash here !!, with problem #2,
            //segmentation fault, is not handle by try catch
            m_string->append("foomaster");
            m_string->append("#");

            if(m_string->length()>10000)
                 qDebug()<<this->thread()->currentThreadId()<<" string is: " <<   m_string;

#ifdef TEST_OBJECT_SHARE_FIX
            m_locker->unlock();
#endif
#endif
    }//end forever
}

void CountingThread::addCounterdRef(int *r)
{
    m_counter = r;
    qDebug()<<this->thread()->currentThreadId()<<" add counter with value:  " << *m_counter << " and address : "<< m_counter ;
}
void CountingThread::addStringRef(QString *s)
{
    m_string = s;
    qDebug()<<this->thread()->currentThreadId()<<" add string with value:  " << *m_string << " and address : "<< m_string ;
}
void CountingThread::addMutexRef(QMutex *m)
{
    m_locker = m;
}

如果您跟进代码,您可以执行 2 次测试。

如果您在countingthread.h 中取消注释TEST_ATOMIC_VAR_SHARE 并注释TEST_OBJECT_VAR_SHARE,您将看到

问题 #1如果您在线程中多次使用变量,则可能是来自另一个线程的后台更改,除了我的预期之外,在使用 int 计数器执行期间,我的构建环境中没有应用程序崩溃或奇怪的异常。

如果您在countingthread.h 中取消注释TEST_OBJECT_VAR_SHARE并注释TEST_OBJECT_SHARE_FIX 和注释TEST_ATOMIC_VAR_SHARE,您将看到

问题 #2你得到一个分段错误,这是无法通过 try catch 处理的。这是因为多个线程正在使用字符串函数对同一对象进行编辑。

如果您也取消注释TEST_OBJECT_SHARE_FIX,您会看到通过互斥锁进行的正确处理。

问题 #3Benjamin T 的回答

什么是互斥锁:

我真的很喜欢vallabh 建议的鸡肉解释。

我也在这里找到了一个很好的解释

于 2020-09-15T09:43:29.230 回答
1

在这里寻找解释:我必须将 atomic<bool> 用于“exit” bool 变量吗?

要同步访问,flag您可以将其设为std::atomic<bool>.

或者您可以将 aQReadWriteLock与 aQReadLocker和 a一起使用QWriteLocker。与使用this 相比,如果使用异常或提前返回语句QMutex,您无需关心调用。QMutex::unlock()

或者,QMutexLocker如果QReadWriteLock与您的用例不匹配,您可以使用 a。

QReadWriteLock lock;
...
//In thread1
{
  QReadLocker readLocker(&lock);
  if(flag)
    dowork1;
  else
    dowork2;
}
...
//In thread2
void setflag(bool f)
{
    QWriteLocker writeLocker(&lock);
    flag=f;
}
于 2020-09-15T13:21:29.383 回答
1

让您的程序表达其意图(即访问锁定下的共享变量)对于程序维护和清晰度来说是一个巨大的胜利。你需要有一些很好的理由来放弃那种清晰的方法,比如原子和设计一致的竞争条件。

很好的理由包括您测量了您的程序花费了太多时间来切换互斥锁。在任何体面的实现中,无竞争互斥锁和原子互斥锁之间的区别是微小的——互斥锁锁和解锁通常采用乐观的比较和交换,快速返回。如果您的供应商没有提供体面的实现,您可能会向他们提出。

在您的示例中,dowork1dowork2是在互斥锁锁定的情况下调用的;所以 mutex 不仅保护flag,而且还序列化这些函数。如果这只是您提出问题的方式,那么竞争条件(原子讽刺的变体)就不那么可怕了。

在您的 PS 中(上面的评论重复):是的,count++ 最好被认为是:

   mov $_count, %r1
   ld (%r1), %r0
   add $1, %r0, %r2
   st %r2,(%r1)

即使具有自然原子 inc (x86,68k,370,dinosaurs) 指令的机器也可能不会被编译器一致地使用。因此,如果两个线程几乎同时执行count--;count++;结果可能是-1、0、1。(忽略那些说你的房子可能会被烧毁的语言)。

障碍:如果 CPU0 执行:

store $1 to b
store $2 to c

CPU1 执行:

load barrier -- discard speculatively read values.
load  b to r0
load  c to r1

然后 CPU1 可以将 r0,r1 读取为:(0,0), (1,0), (1,2), (0,2)。这是因为内存写入的可观察顺序很弱;处理器可以以任意方式使它们可见。所以,我们改变 CPU0 来执行:

 store $1 to b
 store barrier  -- stop storing until all previous stores are visible
 store $2 to c

然后,如果 CPU1 看到 r1 (c) 为 2,则 r0 (b) 必须为 1。存储屏障强制执行此操作。

于 2020-09-16T14:52:30.093 回答