qt多线程下,QString赋值导致崩溃

问题

在多线程情况下,给全局的QString变量赋值(拷贝操作),导致程序崩溃。

例如有一个全局变量 QString strGlobal,在多线程代码段中进行赋值,

strGlobal = QString("real value"),线程执行频率到一定程度后,软件崩溃。

原因及解决方法

解决的办法如果是qt5,多线程情况下,尽量使用基础数据类型。qt6不存在此问题。

这是由于qt5的隐式共享机制在多线程情况下不安全导致的。隐式共享机制,就是指针共享,写时复制。在qt5中QString的赋值操作源码如下:

QString &QString::operator=(const QString &other) noexcept
{
    other.d->ref.ref();
    if (!d->ref.deref())
        Data::deallocate(d);
    d = other.d;
    return *this;
}

可以看到代码中对变量进行了重新分配内存的操作,这也就是多线程不安全的根本原因。为什么要重新分配内存呢,其实没有必要。

官方在qt6修复了这个问题。在qt6中QString的赋值操作源码如下:

QString &QString::operator=(const QString &other) noexcept
{
    d = other.d;
    return *this;
}

扩展讨论

Qt几乎所有的非基础数据类型都使用了隐式共享机制,可能会导致多线程不安全的问题。此情况下,尽量使用基础数据类型。如果要使用容器,比如QMap,要以接口的形式进行使用,方便加线程保护等操作。

具体来讲,Qt中以下类使用了隐式共享机制:

QBitArray, QByteArray, QCharRef, QColor, QCollator, QDateTime, QDir, QDirIterator, QFileInfo, QFile, QHash, QLocale, QModelIndex, QString, QStringList, QTextCodec, QTextCursor, QTextDocument, QTextFormat, QTextLayout, QTextOption, QTimer, QVariant

Qt的隐式共享机制

Qt 的隐式共享机制是其非常重要的特性之一,这个机制被应用于很多 Qt 类中,以提供高效的数据存储和管理方式。下面就来详细介绍一下 Qt 的隐式共享机制。

概念

隐式共享机制是一种用于管理数据内存的机制,在该机制下,对象实例之间可以共享底层的数据,而不需要复制整个数据。通俗地说,就是多个对象可以“分享”同一个底层数据结构。使用这种方式,可以大幅减少内存占用,并且避免无谓的数据拷贝,从而提升程序的执行效率。

实现

在 Qt 中,隐式共享机制的实现方式主要包括以下两个步骤:

(1)引用计数:每个共享对象都附带有一个引用计数(reference count),用于统计当前有多少对象正在共享该数据区域。每当一个新对象共享该数据时,引用计数加 1,当某个对象停止共享该数据时,引用计数减 1。引用计数为 0 时,该数据区域即会被销毁释放。

(2)深度复制:由于隐式共享机制使得多个对象可能在底层共享同一数据区域,因此每个对象都必须有自己的私有拷贝。当一个共享对象被修改时,Qt 会首先检查该对象的引用计数,如果当前只有这一个对象在使用该数据区域,那么就可以直接修改该数据区域。否则,Qt 会先为该对象分配一段新的内存,并将底层数据区域的内容复制到该新的内存中,然后再对新对象进行修改。

应用

隐式共享机制广泛应用于 Qt 的很多类库中,例如 QString、QImage、QVariant 等等。以 QString 类为例,通过使用隐式共享机制,QString 实例之间可以共享同一字符串数据,而不需要进行无谓的数据拷贝和内存分配。这既提高了程序的效率,又方便了开发者使用字符串对象。

总之,Qt 的隐式共享机制通过高效的内存管理方式、灵活的引用计数和深度复制实现了数据精细化管理和共享,极大地提升了 Qt 应用程序的性能和可维护性。

多线程问题

Qt 的隐式共享机制是一种优秀的设计模式。但是需要注意,由于多线程环境下有可能并发地修改同一个对象,应用隐式共享机制时要特别小心以避免出现数据竞争或线程安全问题。

Qt 隐式共享机制的本质是引用计数,一个共享对象被创建时放置于堆上,然后计数为1。当这个对象被复制时,所有副本都指向同一个数据,并且计数器增加。当任何一个对象超过了整体的引用时,剩余对象会将引用计数减1,最终由一个对象删除数据。这就保证了只在必须时(即仍存在有效数据引用时)对共享对象进行复制拷贝,避免了不必要的开销和拷贝次数。

在多线程环境下,由于存在并发访问的情况,需要对共享资源进行加锁,以此保护对象内部的数据和计数器。如果没有正确地使用锁机制,在多线程环境下会导致计数器失效,从而导致释放析构对象时可能会出现竞态条件(race condition)等问题。

因此,我们需要非常小心地使用 Qt 隐式共享机制,在多线程环境下要特别注意加锁以保护共享数据的正确性。Qt 提供了一些工具类和机制,如 QMutex、QReadWriteLock 等,来帮助我们实现对象的线程安全共享。

在释放析构对象时可能会存在竞态条件,一种典型的情况是当一个线程正在执行删除操作时,另外一个线程也在同时引用这个对象,此时就可能出现竞争,导致后续的引用或删除操作处于不确定状态,最终导致程序崩溃或异常退出。

因此,在多线程环境下使用Qt隐式共享机制时,我们应该确保对共享对象的每一次引用都能够得到正确的保护,即Mutex的独占式加锁(Mutex Lock)和解锁(Mutex Unlock)操作能够匹配。同时,除非必须,最好避免在多线程环境下使用共享对象的析构函数来释放堆上内存,可以使用智能指针等手段避免出现潜在的竞态条件和内存泄漏问题。

总之,Qt 的隐式共享机制在多线程环境下需要小心谨慎使用,并考虑加锁操作来保护共享数据,以避免出现引用计数的竞争和释放析构对象的竞态条件等问题。

你可能感兴趣的:(QT,c++,qt,开发语言,c++)