C++之写时复制(CopyOnWrite)

news2024/9/24 0:29:44

设计模式专栏: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 header

    void *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) // !isSharable
            return false;
#endif
        if (count != -1) // !isStatic
            atomic.ref();
        return true;
    }

    inline bool deref() Q_DECL_NOTHROW { //减少引用计数
        int count = atomic.load();
#if !defined(QT_NO_UNSHARABLE_CONTAINERS)
        if (count == 0) // !isSharable
            return false;
#endif
        if (count == -1) // isStatic
            return 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.coloradmin.cn/o/1616275.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

编译支持播放H265的cef控件

接着在上次编译的基础上增加h265支持编译支持视频播放的cef控件&#xff08;h264&#xff09; 测试页面&#xff0c;直接使用cef_enhancement,里边带着的那个html即可&#xff0c;h265视频去这个网站下载elecard,我修改的这个版本参考了里边的修改方式&#xff0c;不过我的这个…

Reactor 模式

目录 1. 实现代码 2. Reactor 模式 3. 分析服务器的实现具体细节 3.1. Connection 结构 3.2. 服务器的成员属性 3.2. 服务器的构造 3.3. 事件轮询 3.4. 事件派发 3.5. 连接事件 3.6. 读事件 3.7. 写事件 3.8. 异常事件 4. 服务器上层的处理 5. Reactor 总结 1…

开源啦!一键部署免费使用!Kubernetes上直接运行大数据平台!

市场上首个K8s上的大数据平台&#xff0c;开源啦&#xff01; 智领云自主研发的首个 完全基于Kubernetes的容器化大数据平台 Kubernetes Data Platform (简称KDP) 开源啦&#x1f680;&#x1f680; 开发者只要准备好命令行工具&#xff0c;一键部署 Hadoop&#xff0c;Hi…

JavaScript(二)

JavaScript的语法 1.JavaScript的大小写 在JavaScript中&#xff0c;大小写是敏感的&#xff0c;这意味着大小写不同的标识符被视为不同的变量或函数。例如&#xff0c;myVariable 和 myvariable 被视为两个不同的变量。因此&#xff0c;在编写JavaScript代码时&#xff0c;必…

函数声明与调用:接口原型、参数传递顺序、返回值

示例&#xff1a; /*** brief how about function-declare-call? show you here.* author wenxuanpei* email 15873152445163.com(query for any question here)*/ #define _CRT_SECURE_NO_WARNINGS//support c-library in Microsoft-Visual-Studio #include <stdio.h&…

上位机图像处理和嵌入式模块部署(树莓派4b实现多进程通信)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 和mcu固件、上位机软件不太一样的地方&#xff0c;嵌入式设备上面上面的工业软件一般都是多进程的形式。相比较多线程而言&#xff0c;整个系统就不…

springcloudgateway集成knife4j

上篇我们聊聊springboot是怎么继承knife4j的。springboot3 集成knife4j-CSDN博客 本次我们一起学习springcloudgateway集成knife4j。 环境介绍 java&#xff1a;17 SpringBoot&#xff1a;3.2.0 SpringCloud&#xff1a;2023.0.0 knife4j &#xff1a; 4.4.0 引入maven配置…

# 从浅入深 学习 SpringCloud 微服务架构(四)Ribbon

从浅入深 学习 SpringCloud 微服务架构&#xff08;四&#xff09;Ribbon 段子手168 一、ribbon 概述以及基于 ribbon 的远程调用。 1、ribbon 概述&#xff1a; Ribbon 是 Netflixfa 发布的一个负载均衡器,有助于控制 HTTP 和 TCP客户端行为。 在 SpringCloud 中 Eureka …

就业班 第三阶段(负载均衡) 2401--4.19 day3 nginx3

二、企业 keepalived 高可用项目实战 1、Keepalived VRRP 介绍 keepalived是什么keepalived是集群管理中保证集群高可用的一个服务软件&#xff0c;用来防止单点故障。 ​ keepalived工作原理keepalived是以VRRP协议为实现基础的&#xff0c;VRRP全称Virtual Router Redundan…

用python selenium实现短视频一键推送

https://github.com/coolEphemeroptera/VIVI 效果如下 demo 支持youtube视频搬运

iPerf 3 测试UDP和TCP方法详解

文章目录 前言一、What is iPerf / iPerf3 ?二、功能1. TCP and SCTP2. UDP3. 其他 三、 Iperf的使用1.Iperf的工作模式2. 通用指令3. 服务端特有选项4. 客户端特有选项5. -t -n参数联系 四、Iperf使用实例1. 调整 TCP 连接1. 1TCP 窗口大小调节1. 2 最大传输单元 (MTU)调整 2…

【python项目推荐】键盘监控--统计打字频率

原文&#xff1a;https://greptime.com/blogs/2024-03-19-keyboard-monitoring 代码&#xff1a;https://github.com/GreptimeTeam/demo-scene/tree/main/keyboard-monitor 项目简介 该项目实现了打字频率统计及可视化功能。 主要使用的库 pynput&#xff1a;允许您控制和监…

kafka 命令行使用 消息的写入和读取 quickstart

文章目录 Intro命令日志zookeeper serverkafka servercreate topic && describe topic Intro Kafka在大型系统中可用作消息通道&#xff0c;一般是用程序语言作为客户端去调用kafka服务。 不过在这之前&#xff0c;可以先用下载kafka之后就包含的脚本文件等&#xff0…

在Spring Boot应用中实现阿里云短信功能的整合

1.程序员必备程序网站 天梦星服务平台 (tmxkj.top)https://tmxkj.top/#/ 2.导入坐标 <dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId><version>4.5.0</version></dependency><…

Spring IOC 和 DI详解

目录 一、IOC介绍 1、什么是IOC 2、通过案例来了解IoC 2.1 传统程序开发 2.2 问题分析 2.3 解决方案 2.4 IoC程序开发 2.5 IoC 优势 二、DI介绍 三、IOC 详解 3.1 Bean的存储 3.1.1 Controller&#xff08;控制器存储&#xff09; 3.1.2 Service&#xff08;服务存…

照片相似性搜索引擎Embed-Photos;赋予大型语言模型(LLMs)视频和音频理解能力;OOTDiffusion的基础上可控制的服装驱动图像合成

✨ 1: Magic Clothing Magic Clothing是一个以可控制的服装驱动图像合成为核心的技术项目&#xff0c;建立在OOTDiffusion的基础上 Magic Clothing是一个以可控制的服装驱动图像合成为核心的技术项目&#xff0c;建立在OOTDiffusion的基础上。通过使用Magic Clothing&#xf…

hadoop安装记录

零、版本说明 centos [rootnode1 ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core)jdk [rootnode1 ~]# java -version java version "1.8.0_311" Java(TM) SE Runtime Environment (build 1.8.0_311-b11) Java HotSpot(TM) 64-Bit Server VM (…

STL_List与萃取

List 参考文章: https://blog.csdn.net/weixin_45389639/article/details/121618243 List源码 List中节点的定义&#xff1a; list是双向列表&#xff0c;所以其中节点需要包含指向前一节点和后一节点的指针&#xff0c; data是节点中存储的数据类型 template <class _Tp&g…

海康Visionmaster-常见问题排查方法-启动阶段

VM试用版启动时&#xff0c;弹窗报错&#xff1a;加密狗未安装或检测异常&#xff1b;  问题原因&#xff1a;安装VM 的时候未选择软加密&#xff0c;选择了加密狗驱动&#xff0c;此时要使用软授权就出现了此现象。  解决方法&#xff1a; ① 首先确认软加密驱动正确安装…

网络工程师----第十一天

OSPF&#xff1a; 对称加密算法&#xff1a; 也称为私钥加密或单密钥算法&#xff0c;是一种加密方式&#xff0c;其中加密和解密使用相同的密钥。这种算法的优点包括加密解密速度快、计算量小&#xff0c;适用于大量数据的加密。然而&#xff0c;它的缺点是密钥的安全性难以保…