隐性共享
Qt 中的许多 C++ 类都使用隐式数据共享,以最大限度地提高资源利用率并减少复制。隐式共享类在作为参数传递时既安全又高效,因为只传递指向数据的指针,只有在函数写入数据时才复制数据,即写时复制。
概述
共享类由指向共享数据块的指针组成,共享数据块包含引用计数和数据。
每当有新对象引用共享数据时,引用计数就会递增,而当对象取消引用共享数据时,引用计数就会递减。当引用计数变为 0 时,共享数据将被删除。
在处理共享对象时,有两种复制对象的方法。我们通常说的是深拷贝和浅拷贝。深度拷贝意味着复制一个对象。浅层拷贝是引用拷贝,即只是指向共享数据块的指针。深度拷贝需要耗费大量内存和 CPU。浅层拷贝的速度非常快,因为它只需要设置一个指针并增加引用计数。
隐式共享对象的对象赋值(使用 operator=())就是通过浅层拷贝实现的。
共享的好处是,程序无需不必要地重复数据,从而减少了内存使用和数据复制。对象可以很容易地进行赋值、作为函数参数发送以及从函数中返回。
隐式共享大多发生在幕后,程序员很少需要担心。不过,Qt 的容器迭代器的行为与 STL 的容器迭代器不同。阅读隐式共享迭代器问题。
在多线程应用程序中,隐式共享会发生,详见《线程和隐式共享类》(Threads and Implicitly Shared Classes)。
在实现您自己的隐式共享类时,请使用QSharedData 和QSharedDataPointer 类。
线程和隐式共享类
Qt 对其许多值类(尤其是QImage 和QString )使用了一种称为隐式共享的优化方法。从 Qt 4 开始,隐式共享类可以像其他值类一样安全地跨线程复制。它们是完全可重入的(不是线程安全)。隐式共享确实是隐式的。
在很多人的印象中,隐式共享和多线程是不相容的概念,因为引用计数通常是这样进行的。然而,Qt 使用原子引用计数来确保共享数据的完整性,避免了引用计数器的潜在损坏。
请注意,原子引用计数并不能保证线程安全。在线程间共享隐式共享类的实例时,应使用适当的锁定。这是对所有重入类的相同要求,无论是否共享。不过,原子引用计数确实能保证在隐式共享类的本地实例上工作的线程是安全的(意思是不同对象在不同线程里是安全,同一对象在多线程不是安全的)。我们建议使用信号和槽在线程间传递数据,因为这样做无需任何显式锁定。
总而言之,Qt 4 中的隐式共享类确实是隐式共享的。即使在多线程应用程序中,您也可以像使用普通的、非共享的、基于值的可重入类一样安全地使用它们。
隐式共享的工作原理
共享数据块
多个对象内部指向同一份数据块,数据块包含 数据内容 和 引用计数器。引用计数
当新对象通过拷贝构造函数或赋值操作符创建时,引用计数加 1,不实际复制数据。写时复制(COW)
当某个对象尝试修改数据时,检查引用计数:若引用计数为 1(唯一所有者),直接修改数据。
若引用计数大于 1(共享数据),创建数据的独立副本,修改副本,并将引用计数减 1。
隐式共享详解
如果对象即将发生变化且引用计数大于 1,隐式共享会自动将对象从共享块中分离。(这通常称为写时复制 或值语义)。
隐式共享类可控制其内部数据。在任何修改其数据的成员函数中,它都会在修改数据前自动脱离。不过,请注意容器迭代器的特殊情况;请参阅隐式共享迭代器问题。
使用隐式共享的QPen 类在所有修改内部数据的成员函数中都会脱离共享数据。
代码片段:
void QPen::setStyle(Qt::PenStyle style)
{
detach(); // detach from common data
d->style = style; // set the style member
}
void QPen::detach()
{
if (d->ref != 1) {
... // perform a deep copy
}
}
类列表
如果要更改对象,下面列出的类会自动从公共数据中分离出来。程序员甚至不会注意到这些对象是共享的。因此,应将它们的独立实例视为独立对象。它们的行为始终与独立对象无异,但却具有尽可能共享数据的额外好处。因此,您可以将这些类的实例作为参数按值传递给函数,而不必担心复制开销。
示例
QPixmap p1, p2;
p1.load("image.bmp");
p2 = p1; // p1 and p2 share data
QPainter paint;
paint.begin(&p2); // cuts p2 loose from p1
paint.drawText(0,50, "Hi");
paint.end();
在本例中,p1
和p2
共享数据,直到QPainter::begin() 被调用为p2
,因为绘制像素图会修改数据。