从Qt 4开始,可以安全地跨线程复制隐式共享类,就像任何其他值类一样。
因此,以下示例中的getText()
将是线程安全的。正确的吗?
class MyClass {
private:
QString text;
public:
QString getText() const { return text; } // Is this thread-safe?
};
答案 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;
}
人们希望这个例子是安全的,c1
和c2
都等于'0'
或'x'
,这取决于该副本是否之前拍摄过。或修改后。但是,如果您是并发专家,那么“之前”和“之后”这两个字应该会向您尖叫“种族条件!”。确实,考虑到足够的CPU内核,禁用的优化和几次尝试,即使'0' 'x'
甚至被声明为copy
,该代码有时也会打印const
!幕后发生的事情是这样的:
SourceThread
(ST)检查参考计数。它等于1,所以它继续进行而不会分离。DestinationThread
(DT)创建一个副本,将引用计数提高到2。不过,ST不再在乎。'0'
)。'x'
。'x'
。当我第一次想到这个例子时,我什至认为隐式共享类并不是真正可重入的。这不是真的。上面的代码依赖于线程安全,而不依赖于重入。并发访问单个实例,尽管只有一个访问用于写入,而另一个访问用于读取,但这仍然不安全。
因此,即使文档说“可以在线程间安全地复制隐式共享类”,但只有在确实确实地安全地复制的情况下,才可以在线程间安全地复制它们。在某个线程创建了副本之后,可以自由地对其进行修改,而其他任何线程都不会看到这些修改。但是从不同线程并发进行读/写是不行的。