问题:复制隐式共享类的实例是否是线程安全的?

时间:2018-03-02 15:18:10

标签: c++ multithreading qt thread-safety

Qt documentation州:

  

从Qt 4开始,可以安全地跨线程复制隐式共享类,就像任何其他值类一样。

因此,以下示例中的getText()将是线程安全的。正确的吗?

class MyClass {

private:
    QString text;

public:
    QString getText() const { return text; }      // Is this thread-safe?
};

2 个答案:

答案 0 :(得分:2)

在编写类时,它是线程安全的。没有人可以写信给text,因此您无法解决任何同步问题。

如果你有了

class MyClass {

private:
    QString text;

public:
    QString getText() const { return text; }
    void setText(const QString& str) { text = str; }
};

然后getText函数仍然是安全的,因为它不会更改类中的任何状态,但现在类本身不是线程安全的。由于您可以在一个线程中设置状态(写入),因此需要进行同步。

答案 1 :(得分:1)

这个话题比看起来要复杂。

首先,让我们从定义开始。隐式共享的类是 reentrant not 不一定是线程安全的。可重入意味着同时访问一个类的不同实例是安全的,即使访问是只读的,访问单个实例也不一定安全。

如果每个线程都有自己的相同字符串副本,则无论该字符串是否实际共享,它们都可以自由地做任何事情(读或写)。这是安全的,因为QString是可重入的。但是在您的情况下,实际上您有多个线程访问单个实例以对其进行复制。那不是真正的可重入性,那是线程安全。

复制是只读访问。在这里,“隐式共享类可以安全地跨线程复制”这一短语变得相当混乱。

如果对单个实例的所有访问严格都是只读的,那么将应用常规并发规则。也就是说,该对象应该由某个线程正确发布,然后才能被其他线程安全地读取。我不是C ++并发方面的专家,但是从我的Java经验来看,鉴于各种重新排序,缓存和优化问题,正确的发布比看起来要复杂得多。为安全起见,请确保text字段在任何读取线程开始之前已初始化(当然不算初始化线程),或者在第一次读取之前的某个时候使用显式同步。

如果两个或多个线程试图同时修改实例,那么这当然是不安全的。

最有趣的情况是只有一个线程修改其自己的副本。乍一看,似乎应该很安全。好吧,在大多数情况下,只有在创建副本后 发生修改的情况下,才可以。考虑以下示例:

#include <QtDebug>
#include <QSemaphore>
#include <QString>
#include <QThread>

struct SourceThread : public QThread {
    QSemaphore &c;
    QString s = "0123456789ABCDEF0";
    SourceThread(QSemaphore &c) : c(c) {}
    void run() override {
        c.acquire();
        s[0] = 'x'; // concurrent access here
    }
};

struct DestinationThread : public QThread {
    QSemaphore &c;
    SourceThread &s;
    DestinationThread(QSemaphore &c, SourceThread &s) : c(c), s(s) {}
    void run() override {
        c.acquire();
        const QString copy = s.s; // and here
        QChar c1 = copy.at(0);
        QChar c2 = copy.at(0);
        if (c1 != c2)
            qDebug() << c1 << c2;
    }
};

int main() {
    for (int i = 0; i < 10000; ++i) {
        QSemaphore c(2);
        SourceThread s(c);
        DestinationThread d(c, s);
        c.acquire(2);
        s.start();
        d.start();
        c.release(2);
        s.wait();
        d.wait();
    }
    return 0;
} 

人们希望这个例子是安全的,c1c2都等于'0''x',这取决于该副本是否之前拍摄过。或修改后。但是,如果您是并发专家,那么“之前”和“之后”这两个字应该会向您尖叫“种族条件!”。确实,考虑到足够的CPU内核,禁用的优化和几次尝试,即使'0' 'x'甚至被声明为copy,该代码有时也会打印const!幕后发生的事情是这样的:

  1. SourceThread(ST)检查参考计数。它等于1,所以它继续进行而不会分离。
  2. DestinationThread(DT)创建一个副本,将引用计数提高到2。不过,ST不再在乎。
  3. DT接受第一个字符('0')。
  4. ST将第一个字符更改为'x'
  5. DT再次获取第一个字符,这次是'x'
  6. 糟糕!

当我第一次想到这个例子时,我什至认为隐式共享类并不是真正可重入的。这不是真的。上面的代码依赖于线程安全,而不依赖于重入。并发访问单个实例,尽管只有一个访问用于写入,而另一个访问用于读取,但这仍然不安全。

因此,即使文档说“可以在线程间安全地复制隐式共享类”,但只有在确实确实地安全地复制的情况下,才可以在线程间安全地复制它们。在某个线程创建了副本之后,可以自由地对其进行修改,而其他任何线程都不会看到这些修改。但是从不同线程并发进行读/写是不行的。