C++之写时复制(CopyOnWrite)

设计模式专栏:http://t.csdnimg.cn/4j9Cq

目录

1.简介

2.实现原理

3.QString的实现分析

3.1.内部结构

3.2.写入时复制

4.示例分析

5.使用场景

6.总结


1.简介

        CopyOnWrite (COW) 是一种编程思想,用于优化内存使用和提高性能。COW 的基本思想是,如果多个对象或变量共享相同的数据,那么它们最初可以共享同一份数据,而不是为每个对象创建独立的数据副本。如果任何一个对象想要修改数据,就会创建数据的副本,然后在副本上进行修改,而原始数据保持不变。

        这种技术在处理不可变数据结构或很少修改数据的情况下特别有用。通过最初共享数据,COW 避免了不必要的复制,提高了内存效率。它还减少了对共享数据的修改对其他对象的影响,因为它们继续引用原始数据,直到进行修改。

        Qt 的 QString 正是采用了 COW 思想,它是如何工作的呢?简单来说,就是平时查询的时候,都不需要加锁,随便访问,只有在更新的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。

2.实现原理

        写时复制的原理就是浅拷贝加引用计数。 当只是进行读操作时,就进行浅拷贝,如果需要进行写操作的时候,再进行深拷贝;再加一个引用计数,多个指针指向同一块空间,记录同一块空间的对象个数。

        1) QString之写时复制

当两个QString发生复制或者赋值时,不会复制字符串内容,而是增加一个引用计数,然后字符串指针进行浅拷贝,其执行效率为O(1)。只有当修改其中一个字符串内容时,才执行真正的复制。

        2) 引用计数

堆区,为了好获取将将引用计数与数据放在一起,并且最好在数据前面,这样当数据变化的时候不会移动引用计数的位置

3.QString的实现分析

3.1.内部结构

QString 内部的数据结构是 QTypedArrayData

template <class T>
struct QTypedArrayData: QArrayData
{...
};typedef QTypedArrayData<ushort> QStringData;class Q_CORE_EXPORT QString
{
public:typedef QStringData Data;...Data*  d;  //真正存储QString数据的对象
};

而 QTypedArrayData 继承自 QArrayData。

struct Q_CORE_EXPORT QArrayData
{QtPrivate::RefCount ref;  //引用计数int size;uint alloc : 31;uint capacityReserved : 1;qptrdiff offset; // in bytes from beginning of headervoid *data(){Q_ASSERT(size == 0|| offset < 0 || size_t(offset) >= sizeof(QArrayData));return reinterpret_cast<char *>(this) + offset;}const void *data() const{Q_ASSERT(size == 0|| offset < 0 || size_t(offset) >= sizeof(QArrayData));return reinterpret_cast<const char *>(this) + offset;}...
};

QArrayData 有个 QtPrivate::RefCount 类型的成员变量 ref,该成员变量记录着该内存块的引用。也就是说,QString 采用了 Copy On Write 的技术优化了存放字符串的内存块。

3.2.写入时复制

QtPrivate::RefCount的作用就是保存计数,从它的源码可以看出:

class RefCount
{
public:inline bool ref() Q_DECL_NOTHROW {  //增加引用计数int count = atomic.load();
#if !defined(QT_NO_UNSHARABLE_CONTAINERS)if (count == 0) // !isSharablereturn false;
#endifif (count != -1) // !isStaticatomic.ref();return true;}inline bool deref() Q_DECL_NOTHROW { //减少引用计数int count = atomic.load();
#if !defined(QT_NO_UNSHARABLE_CONTAINERS)if (count == 0) // !isSharablereturn false;
#endifif (count == -1) // isStaticreturn true;return atomic.deref();}...QBasicAtomicInt atomic;  //原子变量
};

QString::QString(const QString &other)复制构造函数中:

QString &QString::operator=(const QString &other) Q_DECL_NOTHROW
{other.d->ref.ref();    //other增加引用计数 [1]if (!d->ref.deref())    //自己减少引用计数 [2]Data::deallocate(d); //如果自己计数为0则释放内存 [3]d = other.d;            //直接指针赋值 [4]return *this;
}

通过4步完成了拷贝构造函数,相比深拷贝:

class String{
public:String(const String &rhs):m_pstr(new char[strlen(rhs) + 1]()){}
private:char* m_pstr;
};

减少了内存申请和拷贝的过程,从而大大的提高了运行效率。

在追加内容函数QString::append(const QString &str) 的实现也看出的确是采用了 COW 技术

QString &QString::append(const QString &str)
{if (str.d != Data::sharedNull()) {if (d == Data::sharedNull()) {operator=(str);} else {if (d->ref.isShared() || uint(d->size + str.d->size) + 1u > d->alloc)reallocData(uint(d->size + str.d->size) + 1u, true); //memcpy(d->data() + d->size, str.d->data(), str.d->size * sizeof(QChar));d->size += str.d->size;d->data()[d->size] = '\0';}}return *this;
}
void QString::reallocData(uint alloc, bool grow)
{auto allocOptions = d->detachFlags();if (grow)allocOptions |= QArrayData::Grow;if (d->ref.isShared() || IS_RAW_DATA(d)) {Data *x = Data::allocate(alloc, allocOptions);Q_CHECK_PTR(x);x->size = qMin(int(alloc) - 1, d->size);::memcpy(x->data(), d->data(), x->size * sizeof(QChar));x->data()[x->size] = 0;if (!d->ref.deref())Data::deallocate(d);d = x;} else {Data *p = Data::reallocateUnaligned(d, alloc, allocOptions);Q_CHECK_PTR(p);d = p;}
}

从上述代码可以看出,reallocData函数在重新写入数据时会重新分配内存,在新的内存上增加计数并减少原有内存技术,这正是COW的思想所在。

4.示例分析

在C++中,虽然标准库并没有直接提供写入时复制的实现,但你可以通过自定义数据结构来实现这种策略。下面是一个简单的示例,展示了如何在C++中实现一个写入时复制的数组:

#include <iostream>  
#include <vector>  
#include <memory>  template <typename T>  
class CopyOnWriteArray {  
private:  std::shared_ptr<std::vector<T>> data;  public:  CopyOnWriteArray() : data(std::make_shared<std::vector<T>>()) {}  // 获取数组的大小  size_t size() const {  return data->size();  }  // 获取指定位置的元素(只读)  const T& operator[](size_t index) const {  return (*data)[index];  }  // 修改指定位置的元素(写入时复制)  void set(size_t index, const T& value) {  if (data.unique()) {  // 如果当前是唯一持有者,则无需复制  } else {  // 否则,创建一个新的数据副本  data = std::make_shared<std::vector<T>>(*data);  }  (*data)[index] = value;  }  // 添加一个新元素到数组的末尾(写入时复制)  void push_back(const T& value) {  if (data.unique()) {  // 如果当前是唯一持有者,则直接在原数组上添加元素  data->push_back(value);  } else {  // 否则,创建一个新的数据副本,并在副本上添加元素  data = std::make_shared<std::vector<T>>(*data);  data->push_back(value);  }  }  
};  int main() {  CopyOnWriteArray<int> array;  array.push_back(1);  array.push_back(2);  array.push_back(3);  std::cout << "Size: " << array.size() << std::endl;  std::cout << "Element at index 1: " << array[1] << std::endl;  array.set(1, 100); // 写入时复制发生在这里  std::cout << "Element at index 1 after modification: " << array[1] << std::endl;  return 0;  
}

        这个示例中,CopyOnWriteArray 类使用 std::shared_ptr 来管理底层数据的生命周期。当多个 CopyOnWriteArray 对象共享同一个 std::vector 时,如果其中一个对象尝试修改数据,就会触发写入时复制。这是因为修改操作会检查 std::shared_ptr 的引用计数,如果计数大于1,就创建一个新的 std::vector 副本,并在副本上进行修改。这样,其他仍然引用原始 std::vector 的对象不会受到影响。

5.使用场景

        写入时复制(CopyOnWrite)的使用场景主要集中在需要高并发读操作,而写操作相对较少的场景。这种策略特别适用于那些读操作远多于写操作,且写操作不会频繁发生的情况。下面是一些具体的使用场景:

  1. 并发容器:当需要实现线程安全的容器,并且读操作远多于写操作时,可以使用基于写入时复制的并发容器。这种容器可以确保在读取数据时不需要加锁,从而提供高效的并发读取性能。只有在写入数据时,才会复制底层数据并进行修改,从而保持线程安全。

  2. 共享不可变数据:在某些情况下,多个线程或进程需要共享一些不可变的数据。当这些数据需要更新时,可以使用写入时复制策略来创建一个新的数据副本,并在副本上进行修改。这样,其他线程或进程仍然可以安全地访问原始数据,而不会受到修改的影响。

  3. 事件处理系统:在事件驱动的系统中,事件处理函数通常需要读取事件数据并进行处理。如果多个事件处理函数可以同时处理不同的事件,并且事件数据在事件处理过程中不会被修改,那么可以使用写入时复制的容器来存储事件数据。这样,每个事件处理函数都可以安全地读取事件数据,而不需要担心数据竞争或一致性问题。

  4. 日志记录:在日志记录系统中,通常需要记录大量的日志信息,并且这些日志信息主要是被读取和分析的,而不是被修改的。使用写入时复制的容器来存储日志信息可以提高并发写入的性能,因为多个线程可以同时写入不同的日志条目,而不需要进行复杂的同步操作。

        需要注意的是,写入时复制策略在写操作频繁或数据量非常大的情况下可能会导致较高的内存开销和性能下降。因此,在选择使用写入时复制时,需要仔细评估应用场景的读写比例、数据量和性能要求,以确保其适用性。此外,还需要注意在实现写入时复制策略时正确管理内存和引用计数,以避免内存泄漏和其他问题。

6.总结

使用CopyOnWrite思想有以下几个好处:

内存效率:CopyOnWrite允许多个对象共享相同的数据,避免了不必要的数据复制。这对于大型数据结构或多个对象需要引用相同数据的情况下,可以节省大量的内存。

 性能优化:对于很少修改数据的情况下,CopyOnWrite可以显著提高性能。由于读操作不需要加锁,多个线程可以同时访问共享数据,提高并发访问的效率。对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。

减少数据拷贝:CopyOnWrite只在写操作时进行数据拷贝,而在读操作时共享数据。这减少了不必要的数据拷贝开销,提高了性能。

尽管CopyOnWrite有一些优点,但也存在一些缺点或不足之处:

写操作开销:当有写操作发生时,CopyOnWrite需要进行数据的复制,这会引入一定的开销。复制大型数据结构可能会消耗较多的时间和内存,并且频繁的写操作可能会影响性能

不适合频繁修改的场景:由于CopyOnWrite需要进行数据复制,所以频繁的写操作会导致性能下降。对于需要频繁修改数据的场景,可能有更适合的数据结构或算法选择

        总的来说,CopyOnWrite适用于多个读操作、少量写操作的场景,可以提供高效的内存使用和线程安全的并发访问。但需要权衡其开销和适用性,根据具体情况选择使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/640657.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

开源博客项目Blog .NET Core源码学习(17:App.Hosting项目结构分析-5)

本文学习并分析App.Hosting项目中前台页面的作品展示页面和首页页面。 作品展示页面 作品展示页面总体上为上下布局&#xff0c;上方显示导航菜单&#xff0c;下方从左向右显示图片数据&#xff0c;支持放大查看图片和下载图片。整个页面使用了layui中的面包屑导航、弹出层、流…

[Linux][多线程][二][线程互斥][互斥量][可重入VS线程安全][常见锁概念]

目录 1.线程互斥1.互斥相关背景概念2.多个线程并发的操作共享变量&#xff0c;会带来一些问题3.互斥量mutex 2.互斥量的接口1.初始化互斥量2.销毁互斥量3.加锁4.解锁5.使用 -- 改善上面代码 3.互斥量实现原理探究1.加锁是如何保证原子性的&#xff1f;2.如何保证锁是原子性的&a…

[Algorithm][二分查找][山峰数组的峰顶索引][寻找峰值][寻找旋转排序数组中的最小值][0~n-1中缺失的数字]详细讲解

目录 1.山脉数组的峰顶索引1.题目链接2.算法原理详解3.代码实现 2.寻找峰值1.题目链接2.算法原理详解3.代码实现 3.寻找旋转排序数组中的最小值1.题目链接2.算法原理详解3.代码实现 4.0〜n-1 中缺失的数字1.题目链接2.算法原理详解3.代码实现 1.山脉数组的峰顶索引 1.题目链接…

智慧安防边缘计算硬件AI智能分析网关V4算法启停的操作步骤

TSINGSEE青犀视频智能分析网关V4内置了近40种AI算法模型&#xff0c;支持对接入的视频图像进行人、车、物、行为等实时检测分析&#xff0c;上报识别结果&#xff0c;并能进行语音告警播放。硬件管理平台支持RTSP、GB28181协议、以及厂家私有协议接入&#xff0c;可兼容市面上常…

山与路远程控制 一个基于electron和golang实现的远控软件

山与路远程控制 &#x1f3a5;项目演示地址 还在制作… ♻️项目基本介绍 山与路远程控制是基于electron(vue3)和golang实现的远程控制软件(项目界面主要模仿向日葵远程软件,如有侵权请告知),代码可能有点臃肿毕竟只花了一周左右写的无聊项目,如果对其感兴趣的大佬可以fork自…

鸿蒙开发实例:【配置OpenHarmony SDK】

配置OpenHarmony SDK 在设置OpenHarmony应用开发环境时&#xff0c;需要开发者在DevEco Studio中配置对应的SDK信息。 说明&#xff1a; 请注意&#xff0c;OpenHarmony SDK版本精简了部分工具链&#xff0c;因此不适用于HarmonyOS应用开发。 前提条件 已下载并安装好DevEco …

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单人脸检测/识别实战案例 之四 简单行人人体检测效果

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单人脸检测/识别实战案例 之四 简单行人人体检测效果 目录 Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单人脸检测/识别实战案例 之四 简单行人人体检测效果 一、简单介绍 二、简单行人人体检测效果实现原理 三、简…

网络安全数字孪生:一种新颖的汽车软件解决方案

摘要 随着汽车行业转变为数据驱动的业务&#xff0c;软件在车辆的开发和维护中发挥了核心作用。随着软件数量的增加&#xff0c;相应的网络安全风险、责任和监管也随之增加&#xff0c;传统方法变得不再适用于这类任务。相应的结果是整车厂和供应商都在努力应对汽车软件日益增加…

论文解读:(VPT)Visual Prompt Tuning

文章汇总 要解决的问题 大型模型应用于下游任务本身就存在挑战。最明显的(通常也是最有效的)适应策略是对预先训练好的模型进行全面的端到端微调。 动机 只微调参数的一个子集 解决的办法 只在输入空间中引入少量特定于任务的可学习参数&#xff0c;而在下游训练期间冻结…

visionTransformer window平台下报错

错误&#xff1a; KeyError: Transformer/encoderblock_0/MlpBlock_3/Dense_0kernel is not a file in the archive解决方法&#xff1a; 修改这个函数即可&#xff0c;主要原因是Linux系统与window系统路径分隔符不一样导致 def load_from(self, weights, n_block):ROOT f&…

idea文件菜单打不开,pom一直在加载。有些项目一直在加载。从文件打开,d盘进不去。

idea文件菜单打不开&#xff0c;pom一直在加载。有些项目一直在加载。从文件打开&#xff0c;d盘进不去。 在这里会有红色感叹号&#xff0c;进入吧插件禁止启用。就ok。我这里miniapp插件坏了。重启就行。

使用Go语言和chromedp库下载Instagram图片:简易指南

摘要/导言 本文将介绍如何使用Go语言配合chromedp库来下载Instagram上的图片。我们将通过一个简单的示例来展示整个过程&#xff0c;包括如何设置爬虫代理IP以绕过网络限制。 背景/引言 在数据采集和自动化测试领域&#xff0c;Go语言以其出色的执行效率、简洁的语法和卓越的…