我是否需要QMutex来通过单个语句访问变量?

如何解决我是否需要QMutex来通过单个语句访问变量?

this document中,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--”,而这些教程说您应该使用Mutex保护变量“ count”。我无法理解使用互斥锁的意义。这是否意味着可以在中间中断单个语句“ count ++”或“ count--”的执行并产生意外结果?会得到什么意外的结果?

解决方法

这是否意味着执行单个语句“ 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;

如果在两个不同线程中的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

在一个定义明确的程序中,对foo()的N次调用应导致a == N。 但是,从多个线程在foo()的同一实例上调用A会产生未定义的行为。在N次调用a之后,无法知道foo()的值。 这将取决于您如何编译程序,使用了哪些优化标志,使用了哪个编译器,CPU的负载是多少,CPU的内核数量,...

NB

A类{ 上市: bool check()const {返回a == b; } int get_a()const {return a; } int get_b()const {return b; } 无效foo(){ ++ a; ++ b; } 私人的: 整数a = 0; int b = 0; };

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

A类{ 上市: bool check()const {返回true; } int get_a()const {return a; } int get_b()const {return b; } 无效foo(){ ++ a; ++ b; } 私人的: int a = 0; int b = 0; }; 因为它不会改变程序的可观察行为。

但是,如果您通过从多个线程在A的同一实例上调用foo()来调用未定义的行为,则如果a = 3b = 2check()仍返回{{ 1}}。您的代码已失去其含义,该程序未按照预期执行任何操作。

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

如果您想定义未定义的行为,可以在这里查看:https://en.cppreference.com/w/cpp/language/ub 并在C ++标准中 为了更好地了解UB,您可以查找有关该主题的CppCon演讲。

,

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

会发生什么类型的错误? 就像评论中的某人说的那样,如果情况不同, 您共享的基本数据类型,例如整数,布尔,浮点数 或对象引用。我添加了一些qt代码 例如,在不使用互斥锁的过程中强调了2个可能的问题。问题#3是一个基本的问题,本杰明T及其漂亮的答案对此进行了详细描述。

Blockquote

main.cpp

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

countingthread.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

countingthread.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个测试。

如果您取消注释 TEST_ATOMIC_VAR_SHARE 并在countingthread.h中注释TEST_OBJECT_VAR_SHARE 你会看到的

问题#1 :如果您在线程中多次使用变量,则可能是其他线程在后台进行了更改,除了我期望的构建环境中没有应用崩溃或异常异常在执行期间使用int计数器。

如果您取消注释 TEST_OBJECT_VAR_SHARE 并评论 TEST_OBJECT_SHARE_FIX 并在countingthread.h中评论TEST_ATOMIC_VAR_SHARE 你会看到的

问题2 ,您会遇到细分错误,无法通过try catch进行处理。由于多个线程正在使用字符串函数在同一个对象上进行编辑,因此出现这种情况。

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

问题3 ,请参见answer from Benjamin T

什么是互斥体:

我真的很喜欢vallabh建议的chicken explanation

我也找到了一个很好的解释here

,

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

实际上,对于bool来说,未定义的行为可能不会以崩溃的形式出现,而是更有可能以线程B的形式出现,有时不会“看到”由bool所做的更改线程A,由于缓存和/或优化问题(例如,优化器“知道”布尔在函数调用期间无法更改,因此它不会多次检查它)

如果您不想使用互斥来保护访问,则另一个选择是将flagbool更改为std::atomic<bool>std::atomic<bool>类型具有您要查找的语义,即可以从任何线程读取和/或写入它而无需调用未定义的行为。

,

请查看此处的说明:Do I have to use atomic<bool> for "exit" bool variable?

要同步对flag的访问,可以将其设置为std::atomic<bool>

或者您可以将QReadWriteLockQReadLockerQWriteLocker一起使用。与使用QMutex相比,这具有以下优点:如果使用异常或早期返回语句,则无需关心对QMutex::unlock()的调用。

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

QReadWriteLock lock;
...
//In thread1
{
  QReadLocker readLocker(&lock);
  if(flag)
    dowork1;
  else
    dowork2;
}
...
//In thread2
void setflag(bool f)
{
    QWriteLocker writeLocker(&lock);
    flag=f;
}
,

保持程序表达其意图(即在锁定状态下访问共享var)是程序维护和清晰性的一大胜利。您需要有一些很好的理由来放弃原子性和设计一致的竞争条件等晦涩方法的清晰度。

充分的理由包括您已测量程序花费太多时间来切换互斥量。在任何体面的实现中,无竞争的互斥锁和原子之间的区别都是微小的-互斥锁的锁定和解锁通常采用乐观的“比较与交换”,并迅速返回。如果您的供应商没有提供良好的实施,则可以将其提出来。

在您的示例中,在锁定互斥锁的情况下调用了 dowork1 dowork2 ;因此互斥不仅可以保护 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(忽略说您的房屋可能烧毁的语言间)。 / p>

障碍: 如果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,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。存储屏障将强制执行该操作。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?
Java在半透明框架/面板/组件上重新绘画。
Java“ Class.forName()”和“ Class.forName()。newInstance()”之间有什么区别?
在此环境中不提供编译器。也许是在JRE而不是JDK上运行?
Java用相同的方法在一个类中实现两个接口。哪种接口方法被覆盖?
Java 什么是Runtime.getRuntime()。totalMemory()和freeMemory()?
java.library.path中的java.lang.UnsatisfiedLinkError否*****。dll
JavaFX“位置是必需的。” 即使在同一包装中
Java 导入两个具有相同名称的类。怎么处理?
Java 是否应该在HttpServletResponse.getOutputStream()/。getWriter()上调用.close()?
Java RegEx元字符(。)和普通点?