隐式共享Implicit Sharing

隐式共享Implicit Sharing

来源:Qt助手5.9.6  翻译人:王功民

在Qt中有很多C++类使用隐式数据共享来增强资源的利用和减少数据的复制。作为参数使用时,隐式共享的类既安全又高效,因为只有一个指向数据的指针在传递。数据当且仅当函数写入值的时候才复制。也就是写时复制技术:copy-on-wtite.

概述
一个共享类包含一个指向共享数据块的指针,共享数据块又包含一个数据的引用计数。
当一个共享对象被创建,数据块的引用计数是1.当一个新的对象引用了数据块,引用计数增加1,当一个对象不再引用该数据,引用计数减1。当引用计数为0的时候,共享数据块将会被删除。

对于共享对象,有两种方式来拷贝对象。我们通常叫做深拷贝和浅拷贝。深拷贝会复制对象,浅拷贝会复制引用。也就是说,一个指向共享数据块的指针,执行深拷贝会耗系统的内存和cpu,执行浅拷贝会非常快,因为它只涉及一个指针的设置和引用计数的增加。

对象复制(operator=())对于隐式共享对象来说,执行的是浅拷贝。

共享的一个好处是程序不需要执行不必要的复制,所以会降低内存消耗和减少数据拷贝。对象可以更简单的赋值、或者作函数参数、或者作函数返回值。

隐式共享实际上发生在幕后,程序员不需要关注。但是,Qt的容器迭代器有特殊的行为需要注意。
对于STL形式的迭代器,隐式共享有一个重要后果:你应该避免在迭代器指向一个容器的时候复制容器。迭代器指向的是对象的内部数据结构,如果你拷贝了一个容器,就需要非常小心了。例如:
  QVector<int> a, b;
  a.resize(100000); // 创建一个很大的容器并初始化为所有值为0.

  QVector<int>::iterator i = a.begin();
  // 错误使用 iterator i:
  b = a;
  /*
      现在i指向共享数据,要小心使用
      如果我们使用如下语句 *i = 4 ,那么我们改变了共享数据(两个容器都改了)
      这个行为与stl容器不同,在qt容器中要避免
  */

  a[0] = 5;
  /*
      容器a解绑了。
      即使i来源于容器a,但是它实际上是为容器b工作的。
      现在的情况是 (*i) == 0.
  */

  b.clear(); // 现在迭代器i完全非法了

  int j = *i; // 未定义的行为!
  /*
      The data from b (which i pointed to) is gone.
      This would be well-defined with STL containers (and (*i) == 5),
      but with QVector this is likely to crash.
  */

多线程应用程序的隐式共享

如果要实现自己的隐式共享类,请使用QSharedData and QSharedDataPointer.

隐式共享的细节
隐式共享会自动解绑对象数据块并修改引用计数,当数据被修改。
隐式共享类自己维护一个内部数据。任何成员函数对数据修改前,都会自动被解绑。但是,注意容器的迭代器。
Qpen类,使用隐式共享,所有成员函数对内部数据的修改前,都会解绑数据块。
代码段如下:
  void QPen::setStyle(Qt::PenStyle style)
  {
      detach();           // 解绑
      d->style = style;   // 改变数据
  }

  void QPen::detach()
  {
      if (d->ref != 1) {
          ...             // 执行深拷贝
      }
  }


隐式共享类列表
当数据被修改,这些类会自动解绑共享数据,所以程序员不需要特别关注对象是不是共享。你可以直接传递这些类的实例而不必考虑拷贝的效率问题。
QDebug
QDir
QJsonDocument
QString
QList
QMap
QImage
。。。


========================================================
网络翻译
https://blog.csdn.net/Amnes1a/article/details/69945878

Qt中的很多C++类都使用了隐式数据共享来最大化资源使用和最小化拷贝代价。隐式共享类在作为参数传递时,不仅安全而且高效,因为只是指向数据的指针被传递了,底层的数据只有在函数向它执行写动作时才会发生拷贝,即写时拷贝。

一个共享类是由一个指向共享数据块的的指针组成的,该数据块包含一个引用计数和实际数据。

当一个隐式共享类的对象被创建时,它会引用计数设为1。无论何时,当一个新的对象引用这个共享数据时,引用计数会增加,当一个对象解引用这个共享数据时,引用计数会减少。当引用计数变为0时,共享数据会被删除。

当处理共享数据时,通常有两种方式拷贝一个对象。我们通常称它们为深拷贝和浅拷贝。其中,深拷贝意味着复制一个对象;浅拷贝只拷贝引用,即只有指向共享数据的指针被复制。但执行一次深拷贝在内存和CPU方面的代价都是昂贵的。执行一次浅拷贝是非常快的,因为这只牵涉到设置一个指针和增加引用计数。隐式共享对象的赋值(使用operator=)被实现为浅拷贝。

共享的好处就在于程序不必执行不必要的拷贝,这就会促使更少的内存使用和更少的数据复制。这样,对象就可以简单的被赋值,作为函数的参数传递,作为函数的返回值。

隐式共享大多发生的背后,编程人员一般不需要关注它们。但是,隐式共享导致Qt的容器类和STL中的容器类有很大的不同。由于隐式共享,当复制一个容器时,它们其实是共享一份数据的。如下代码所示:

  QVector<int> a, b;
  a.resize(100000); // make a big vector filled with 0.
  QVector<int>::iterator i = a.begin();
  b = a;
此处,迭代器i的使用要格外小心,因为它指向了共享数据。如果我们指向*i = 4,我们改变的将会是共享的实体,即会影响到两个容器。
其实,在多线程应用程序中,隐式共享也会发生。从Qt4开始,隐式共享类就可以在线程间安全的拷贝,它们完全是可重入的。在很多人看来,考虑到引用计数的行为,隐式共享和多线程应该是不兼容的概念。但其实,Qt使用了原子引用计数来确保共享数据的完整性,避免了引用计数的潜在的错误。但原子引用计数并不能保证线程安全。当在线程间共享一个隐式共享类的实例时,还是应该使用合适的锁。在对于所有的可重入类来说都是必须的,无论共享或不共享。但是,原子引用计数可以保证线程作用于它自己本地的一个隐式共享类的实例是安全的。我们通常建议使用信号和槽在线程间传递数据,因为这不需要任何显式的锁机制。

当然,除了Qt自带的类的隐式共享,我们还可以使用QSharedData和QSharedDataPointer这两个类来实现我们自己的隐式共享。

如果对象将要被改变并且其引用计数大于1,隐式共享会自动的从共享块中分离该对象。(这经常被称为写时复制)

隐式共享类可以控制它自己的内部数据。在它的要修改数据的成员函数中,它会在修改数据之前自动的分离。但是,请注意,对于容器类的迭代器来说比较特殊,参见上面所讲。

下面,我们以一个员工类为例,来实现一个隐式共享类。步骤如下:

定义类Emplyee,该类只有一个唯一的数据成员,类型为QSharedDataPointer<EmployeeData>。
定义类EmployeeData类,其派生自QSharedData。该类中包含的就是原本应该放在Employee类中的那些数据成员。
类定义如下:
  #include <QSharedData>
  #include <QString>
 
  class EmployeeData : public QSharedData
  {
    public:
      EmployeeData() : id(-1) { }
      EmployeeData(const EmployeeData &other)
          : QSharedData(other), id(other.id), name(other.name) { }
      ~EmployeeData() { }
 
      int id;
      QString name;
  };
 
  class Employee
  {
    public:
      Employee() { d = new EmployeeData; }
      Employee(int id, const QString &name) {
          d = new EmployeeData;
          setId(id);
          setName(name);
      }
      Employee(const Employee &other)
            : d (other.d)
      {
      }
      void setId(int id) { d->id = id; }
      void setName(const QString &name) { d->name = name; }
 
      int id() const { return d->id; }
      QString name() const { return d->name; }
 
    private:
      QSharedDataPointer<EmployeeData> d;
  };
在Employee类中,要注意这个数据成员d。所有对employee数据的访问都必须经过d指针的operator->()来操作。对于写访问,operator->()会自动的调用detach(),来创建一个共享数据对象的拷贝,如果该共享数据对象的引用计数大于1的话。也可以确保向一个Employee对象执行写入操作不会影响到其他的共享同一个EmployeeData对象的Employee对象。
类EmployeeData继承自QSharedData,它提供了幕后的引用计数。
在幕后,无论何时一个Employee对象被拷贝、赋值或作为参数传,QSharedDataPointer会自动增加引用计数;无论何时一个Employee对象被删除或超出作用域,QSharedDataPointer会自动递减引用计数。当引用计数为0时,共享的EmployeeData对象会被自动删除。

      void setId(int id) { d->id = id; }
 
      void setName(const QString &name) { d->name = name; }
在Employee类的非const成员函数中,无论何时d指针被解引用,QSharedDataPointer都会自动的调用detach()函数来确保该函数作用于一个数据拷贝上。并且,在一个成员函数中,如果对d指针进行了多次解引用,而导致调用了多次detach(),也只会在第一次调用时创建一份拷贝。
      int id() const { return d->id; }
 
      QString name() const { return d->name; }
但在Employee的const成员函数中,对d指针的解引用不会导致detach()的调用。

还有,没必要为Employee类实现拷贝构造函数或赋值运算符,因为C++编译器提供的拷贝构造函数和赋值运算符的逐成员拷贝就足够了。因为,我们唯一需要拷贝的就是d指针,而该指针是一个QSharedDataPointer,它的operator=()仅仅是递增了共享对象EmployeeData的引用计数。

隐式共享 VS 显式共享
上面讲到的隐式共享,对于Employee类来说可能会有问题。考虑一种情况,如下代码所示:
  #include "employee.h"
 
  int main()
  {
      Employee e1(1001, "Tom");
      Employee e2 = e1;
      e1.setName("Jerry");
  }
在创建e2,并将e1赋值给它后,e1和d2都引用了同一个员工,即Tom,1001。这两个Employee对象指向了同一个EmployeeData实例,所以该实例的引用计数为2。紧接着执行e2.setName("Jerry")来改变员工名字,但因为此时的引用计数大于1,所以会在名字发生改变前执行一次写时复制,使e1和e2指向不同的EmployeeData对象,然后对e1执行名字修改动作。从而导致e1和e2有不同的名字,但有相同的ID,1001,这可能不是我们想要的。当然,如果我们确实想创建出第二个完全不同的员工,那么可以再在e1上调用setId(1002),修改其ID。但是,如果我们就是只想改变员工的名字呢?此时,我们就可以考虑使用显式共享来替代隐式共享。
如果我们在Employee类中的声明d指针时使用的是QExplicitySharedDataPointer<EmployeeData>,那么就是使用了显式共享,写时复制操作就不会自动发生了(即 在 非 const成员函数中不会自动调用detach())。这样一来,e1.setName("Jerry")执行之后,员工的名字被改变了,但e1和e2仍然引用同一个EmployeeData实例,故还是只有一个id为1001的员工。
--------------------- 
作者:求道玉 
来源:CSDN 
原文:https://blog.csdn.net/Amnes1a/article/details/69945878 
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值