2024年3月

Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍如何运用
QProcess
组件实现针对进程的控制管理等。

当你在使用Qt进行跨平台应用程序开发时,经常需要与外部进程进行交互,这时就可以利用Qt的
QProcess
模块。
QProcess
模块提供了启动和控制外部进程的功能,能够执行外部命令、运行其他可执行文件,以及与外部进程进行通信。通过
QProcess
,可以方便地执行命令行命令、调用系统工具、执行脚本等。
QProcess
还可以捕获外部进程的输出,以及监视外部进程的运行状态,从而实现更灵活、高效的进程管理。

以下是
QProcess
类的一些常用函数及其解释的表格:

函数 描述
start(const QString &program, const QStringList &arguments) 启动一个新的进程,
program
参数指定要执行的程序,
arguments
参数指定传递给程序的参数列表。
startDetached(const QString &program, const QStringList &arguments) 启动一个新的进程,但不会等待进程退出,也不会将输出传递给调用进程。
waitForStarted(int msecs = 30000) 等待进程启动,如果在指定时间内进程没有启动,将返回false。
waitForFinished(int msecs = 30000) 等待进程退出,如果在指定时间内进程没有退出,将返回false。
readAllStandardOutput() 读取进程的标准输出,并返回为
QByteArray
readAllStandardError() 读取进程的标准错误输出,并返回为
QByteArray
write(const QByteArray &data) 向进程的标准输入写入数据。
closeWriteChannel() 关闭进程的标准输入。
kill() 终止进程。
terminate() 终止进程。
start(const QString &program) 启动一个新的进程,
program
参数指定要执行的程序。
setWorkingDirectory(const QString &dir) 设置进程的工作目录。
state() 返回进程的当前状态。
error() 返回进程的错误状态。
pid() 返回进程的进程ID。
waitForBytesWritten(int msecs = 30000) 等待写入到进程的数据已经被完全写入。
waitForReadyRead(int msecs = 30000) 等待进程有数据可读。
startDetached(const QString &program) 启动一个新的进程,但不会等待进程退出,也不会将输出传递给调用进程。
setProcessChannelMode(QProcess::ProcessChannelMode mode) 设置进程通信模式,可选值包括
QProcess::SeparateChannels

QProcess::MergedChannels

这些函数提供了控制进程的各种方法,可以实现启动、监视、控制和与外部进程进行交互的功能。

进程控制模块可以实现对特定进程的启动关闭,本章将以执行命令行为例,通过调用
Start()
可以拉起一个第三方进程。

QProcess
类的
start()
函数有几种不同的重载形式,但最常用的是以下形式:

bool QProcess::start(
    const QString &program, 
    const QStringList &arguments, 
    QIODevice::OpenMode mode = ReadWrite
)

函数用于启动一个新的进程,并执行指定的程序(
program
参数)。
arguments
参数指定了传递给程序的参数列表,它是一个
QStringList
类型的参数,可以为空。
mode
参数指定了启动进程时打开的模式,默认为
ReadWrite
。函数返回一个
bool
类型的值,表示进程是否成功启动。

当调用
start()
执行命令后,我们则可以通过
readAllStandardOutput()
函数从进程的标准输出中读取所有可用的数据,并将其返回为
QByteArray
对象。

QByteArray QProcess::readAllStandardOutput()

这个函数没有参数,它会立即返回当前可用的标准输出数据,并将输出数据作为字节数组返回。如果没有可用的输出数据,它将返回一个空的字节数组。

当然了,与之对应的
readAllStandardError()
是函数,该函数可以用于从进程的标准错误输出中读取所有可用的数据,并将其返回为
QByteArray
对象。

QByteArray QProcess::readAllStandardError()

该函数同样没有参数,它会立即返回当前可用的标准错误输出数据,并将输出数据作为字节数组返回。如果没有可用的错误输出数据,它将返回一个空的字节数组。

1.1 获取进程信息

此处我们以输出系统进程信息为例,通常可以调用
tasklist /FO CSV
来获取系统中的进程列表,并将其输出为
CSV
格式,通过调用如下函数则可以获取到系统进程信息。

process.start("tasklist", QStringList() << "/FO" << "CSV");

此时通过调用
readAllStandardOutput
函数我们可以将缓冲区内的数据读出并将其放入到一个
QString
类型变量内;

QString output = process.readAllStandardOutput();

当具备了这个列表后,就可以根据冒号来逐行读入并切割,通过循环的方式将其追加到
treeWidget
组件内,并以此来实现展示的效果;

void MainWindow::on_pushButton_clicked()
{
    CallProcess();

    ui->treeWidget->clear();

    QProcess process;
    process.start("tasklist", QStringList() << "/FO" << "CSV");

    if (process.waitForFinished())
    {
        QString output = process.readAllStandardOutput();
        output.replace("\"", "");

        QStringList lines = output.split("\n");

        // 跳过第一行标题
        for(int i = 1; i < lines.size(); ++i)
        {
            QStringList fields = lines[i].split(",");

            // 确保至少有五个字段
            if(fields.size() >= 5)
            {
                QStringList rowData;
                for(int j = 0; j < 5; ++j)
                {
                    rowData << fields[j].trimmed();
                }
                ui->treeWidget->addTopLevelItem(new QTreeWidgetItem(rowData));
            }
        }

        // 设置列标题
        ui->treeWidget->setHeaderLabels(QStringList() << "进程名称" << "PID" << "会话名称" << "Session"<< "内存占用");
    } else
    {
        QTreeWidgetItem *item = new QTreeWidgetItem(ui->treeWidget);
        item->setText(0, "Failed to execute tasklist command.");
    }
}

运行后当点击输出系统进程时则可以看到完整的进程输出效果,如下图所示;

使用此方法我们可以很好的读取到系统中的各种信息,只要能够合理的过滤出想要的字段即可,当需要输出系统信息时我们可以通过
process.start("systeminfo")
调用系统命令获取到,如下代码所示;

void MainWindow::on_pushButton_2_clicked()
{
    ui->treeWidget->clear();

    // 获取系统信息
    QProcess process;
    process.start("systeminfo");

    if (process.waitForFinished())
    {
     QByteArray output = process.readAllStandardOutput();

     // 使用正确的文本编码对输出进行解码
     QTextCodec *codec = QTextCodec::codecForName("GBK");
     QString text = codec->toUnicode(output);

     QStringList lines = text.split("\n");
     for (const QString &line : lines)
     {
         // 解析系统信息,添加到 QTreeWidget 中
         QStringList fields = line.split(":", Qt::SkipEmptyParts);
         if (fields.size() >= 2)
         {
             QString property = fields[0].trimmed();
             QString value = fields[1].trimmed();

             QTreeWidgetItem *item = new QTreeWidgetItem(ui->treeWidget);
             item->setText(0, property);
             item->setText(1, value);
         }
     }

     // 设置列标题
     ui->treeWidget->setHeaderLabels(QStringList() << "系统信息" << "数值");
    } else
    {
     QTreeWidgetItem *item = new QTreeWidgetItem(ui->treeWidget);
     item->setText(0, "Failed to execute systeminfo command.");
    }
}

运行后当用户点击输出系统信息按钮时,因
systeminfo
运行时间较长所以需要等待一段时间,输出效果如下图所示;

云原生技术正重塑IT领域,本文深度剖析了其发展历程、核心概念、生态系统及实践案例,展望未来趋势,揭示了这一技术如何引领企业转型与创新。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

file

一、引言

file
在数字化浪潮中,
云原生技术
已成为推动企业快速创新的重要动力。本文将深入探讨云原生的核心理念、技术架构以及其在实际业务环境中的应用,带领读者深入理解云原生技术的复杂性和优势。

云原生技术的定义

云原生
(Cloud Native)是指利用云计算提供的灵活性、可伸缩性和敏捷性来构建和运行应用的一种方法。它不仅仅是技术的一个组合,更是一种软件开发和部署的哲学。云原生的核心组成包括:

  • 微服务架构
    :将应用分解成小的、独立的服务,每个服务实现特定的业务功能。
  • 容器化
    :使用Docker等技术将应用及其依赖打包在一起,实现一致的运行环境。
  • 动态管理
    :利用Kubernetes等容器编排工具,自动化容器的部署、扩展和管理。
  • 持续交付
    :通过自动化的CI/CD流程,快速、频繁地将软件部署到生产环境。

技术进展

随着技术的不断进步,云原生技术已经从概念阶段走向深入的实践应用。例如,
服务网格
(如Istio)的出现,为微服务提供了精细的流量控制和安全策略,而
无服务器计算
(如AWS Lambda)则进一步抽象了基础设施层,使得开发者可以专注于代码而不是服务器。

实际案例

  • Netflix
    :作为云原生技术的典型代表,Netflix通过微服务架构和持续交付模式,实现了其全球流媒体服务的快速发展和稳定运营。
  • 阿里巴巴
    :阿里云利用Kubernetes实现了在“双11”这样的大规模促销活动中处理数百万级别的并发订单的能力。

通过这些生动的案例,我们可以看到云原生技术如何在不同行业和场景中被应用,以及它们如何支持业务的快速迭代和扩展。

本文旨在深入剖析云原生技术,展示其在当代IT策略中的重要性,并提供对于企业实施云原生转型的深刻洞察。


二、云原生技术的历史

file
云原生技术并非一夜之间出现,它是随着互联网技术的发展而逐渐演化的结果。本节将回顾这一技术从孕育到成熟的关键历程。

从物理服务器到虚拟化

在云计算的早期阶段,企业依赖于物理服务器来部署应用。这种方式虽然稳固,但缺乏灵活性和可扩展性。随后,
虚拟化技术
的出现,如VMware,允许单个物理服务器托管多个虚拟机(VMs),这标志着向更高效利用资源的第一步迈进。

### 早期发展
- 1990s: 物理服务器的主导时期。
- 2000s: 虚拟化技术的兴起,资源利用率提高。

容器化技术的兴起

虚拟化技术提高了硬件的利用率,但仍受限于较重的资源负担。
容器化
技术,如Docker在2013年的推出,开启了更加轻量级和高效的部署时代。容器直接在操作系统层面运行,共享同一内核,同时保持了隔离性。

### 容器化
- 2013: Docker引领容器化时代,改变了应用部署的方式。

微服务架构的发展

随着复杂应用的出现,单体架构开始显得笨重且难以维护。
微服务架构
应运而生,它允许开发者将应用拆分为独立部署和缩放的小型服务。这促进了敏捷开发和持续交付。

### 微服务的崛起
- 2014: 微服务架构开始流行,为大型分布式系统的开发提供了新模式。

持续集成与持续部署(CI/CD)的演进

持续集成(CI)和持续部署(CD)是云原生架构的关键实践,它们促进了自动化测试和部署,加快了从开发到生产的软件交付速度。

### CI/CD的发展
- 2010s: CI/CD工具,如Jenkins、GitLab CI和Travis CI,成为软件开发的标准配套设施。


三、云原生技术的核心概念

file
云原生技术是构建和运行应用程序的一种方法,它围绕着一系列的核心概念展开,这些概念旨在充分利用云计算的特性,如服务的弹性、自动化、可伸缩性和敏捷性。

容器与容器编排

容器是轻量级的、可执行的软件包,包含了应用及其所有依赖,保证了环境一致性和操作的简便性。

### 容器
- **定义**:封装应用代码及其依赖的标准化单元。
- **优势**:简化了部署、扩展、迁移和管理。

### 容器编排
- **工具**:如Kubernetes和Docker Swarm。
- **功能**:自动化容器部署、管理、扩展和网络配置。

微服务与服务网格

微服务架构是将复杂应用程序拆分为一组小的、独立的服务,每个服务执行特定的功能。

### 微服务
- **特点**:独立部署、独立扩展、敏捷开发。
- **挑战**:服务间通信、数据一致性和复杂性管理。

### 服务网格
- **实例**:Istio、Linkerd。
- **职能**:提供服务发现、负载均衡、故障恢复、度量和监控。

动态管理与自动化

动态管理是指自动化资源的配置和优化,确保应用程序的高可用性和高性能。

### 动态管理
- **实现**:自动化的监控、弹性伸缩、自我修复。
- **工具**:如Prometheus、Autoscaling功能。

无服务器架构

无服务器架构从根本上抽象了服务器和基础设施的概念,开发者可以只关注业务逻辑的编写。

### 无服务器计算
- **平台**:如AWS Lambda、Azure Functions。
- **优点**:极大的灵活性、按需计费、自动扩展。


四、当前云原生生态系统

file
云原生生态系统是由广泛的技术、工具和服务组成的,它们共同支持云原生应用的全生命周期管理。这一生态系统不断发展,以适应技术进步和市场需求的变化。

Kubernetes生态系统概览

Kubernetes已成为容器编排的事实标准,围绕它形成了一个庞大的生态系统,包括网络、存储、安全和监控等多个方面。

### Kubernetes核心组件
- **主节点**:集群管理和调度决策。
- **工作节点**:运行应用容器。

### 生态系统工具
- **Helm**:Kubernetes的包管理器。
- **Operators**:用于自动化应用管理的Kubernetes扩展。

云服务提供商的云原生服务

主要的云服务提供商,如Amazon Web Services (AWS), Microsoft Azure, 和Google Cloud Platform (GCP),都提供了广泛的云原生服务,从基础设施建设到应用层面的服务。

### 云服务提供商
- **AWS**:提供EKS、Lambda等服务。
- **Azure**:提供AKS、Azure Functions等服务。
- **GCP**:提供GKE、Cloud Functions等服务。

云原生项目与开源工具

云原生计算基金会(CNCF)托管了一系列开源项目,这些项目支持和完善了云原生技术的应用。

### CNCF项目
- **Prometheus**:监控和警报工具。
- **Envoy**:高性能边缘和服务代理。
- **Fluentd**:数据收集和日志处理工具。

安全性在云原生中的角色

随着技术的演进,安全已成为云原生生态系统中不可或缺的部分。企业必须采取措施保护其基础设施和应用。

### 安全实践
- **DevSecOps**:将安全实践整合到开发和运维流程中。
- **安全工具**:如Aqua Security、Sysdig Secure等。


五、云原生技术的实践案例

云原生技术的实践案例反映了这些技术如何在现实世界中解决具体问题,提升企业效率和市场竞争力。以下案例展示了云原生技术的广泛应用。

企业转型故事

企业通过采用云原生技术,成功实现了数字化转型,增强了市场适应性和竞争力。

### 组织转型
- **Target**:从传统的IT基础设施转向Kubernetes,提高了部署速度和市场响应速度。
- **中国银行**:采用云原生平台,实现了服务的快速迭代和优化,增强了金融服务的灵活性和客户体验。

创新应用案例分析

云原生技术支持了一系列创新应用的开发,推动了新产品和服务的快速上市。

### 创新应用
- **TikTok**:利用云原生技术应对大规模流量,实现全球用户的低延迟视频服务。
- **星巴克**:通过云原生技术,使其订单系统能够在高峰时段高效处理大量订单。

性能优化与成本管理

云原生技术通过自动化和优化资源使用,帮助企业提升性能和降低成本。

### 性能和成本
- **Pinterest**:通过迁移到Kubernetes,减少了基础设施成本,同时提升了服务的稳定性和扩展性。
- **Airbnb**:使用云原生技术优化了其服务部署流程,显著提升了资源利用率和开发效率。


六、云原生技术的未来趋势

云原生技术的未来发展充满了无限的可能性,预计将在自动化、集成、安全性和可持续性方面取得显著进展。以下趋势基于当前的技术进展和市场需求预测。

技术创新前瞻

未来的云原生技术将继续向更高层次的抽象和自动化进化,使得开发和运维工作更加高效。

### 自动化和AI的融合
- **预测性自动化**:借助机器学习,系统将能够预测并自动响应性能问题,如Google Cloud的自动化资源优化。

### 无服务器和边缘计算
- **边缘无服务器**:随着IoT和5G的发展,无服务器架构将扩展到边缘计算,以实现更低延迟和更高效的数据处理,如AWS Lambda@Edge。

行业发展预测

随着企业数字化转型的加速,云原生技术将进一步渗透到各个行业,成为支持业务创新的基石。

### 金融科技
- **云原生银行**:银行业将通过云原生技术提供更加个性化和安全的服务,如摩根大通采用云原生微服务架构支持其业务平台。

### 制造业
- **智能制造**:利用Kubernetes等云原生技术,制造业能够实现更灵活的生产线自动化和优化,如宝马集团的智能工厂。

挑战与机遇

未来的云原生技术发展也将面临诸多挑战,同时这些挑战也带来了新的机遇。

### 安全性和合规性
- **数据隐私和安全**:随着法规如GDPR的实施,云原生技术需提供更强大的数据保护机制,如使用Kubernetes Secrets管理敏感信息。

### 可持续性
- **绿色计算**:云原生技术将更注重能效,以降低碳足迹,如Google Cloud的碳智能计算平台。


七、总结

云原生技术的旅程从虚拟化的曙光到当下的全面云环境演进,展现了技术发展的非凡步伐。在这一过程中,我们见证了从物理硬件到服务抽象化的根本转变,这不仅改变了应用的开发和部署方式,更重塑了企业的业务模式。

云原生技术的演进是企业持续创新的动力源泉。它赋予了企业以前所未有的速度和敏捷性,这在Netflix的快速迭代和阿里巴巴的“双11”大规模处理能力中得到了充分的体现。

Kubernetes等容器编排工具的普及预示着未来应用的开发将更加模块化、标准化。这一变化不仅提高了开发效率,也促成了全球开发人员和组织之间更紧密的合作。

随着AI和机器学习技术的融合,云原生生态系统将更加智能化。这种趋势在Google Cloud的自动化资源优化等服务中已初露端倪,它预示着未来的云原生平台将能够实现更高级别的自我管理和自我优化。

安全性将在云原生技术的发展中扮演更加核心的角色。随着法规的日益严格和数据隐私的高度重视,技术如Kubernetes Secrets成为保护敏感信息的关键工具,显示出云原生技术必须内建更强大的安全机制。

可持续性将成为评价云原生技术的新标准。在全球碳排放目标的压力下,绿色计算和能效优化将成为云服务提供商和企业选择技术解决方案的重要因素。

云原生技术不仅是IT行业的一次变革,它是一场全方位的业务革命。未来,随着技术的进一步成熟,我们可以预见到一个更加智能、安全和可持续的云原生时代的到来。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

如有帮助,请多关注
TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。

QPointer

QPointer
是一种受保护的指针,当其引用的对象被销毁时,它会被自动清除(但是,销毁引用对象还是必须手动delete)。QPointer所指向的对象必须是QObject或其派生类对象。

当多个指针指向同一个 Object 对象时,引用的对象可能被释放掉,这时使用 QPointer 就可以安全的测试引用对象是否有效,防止发生指针空悬。

注意:Qt5 之前,QPointer 指向一个 QWidget 对象(或子类对象)时,QPointer 由 QWidget 的析构函数清除,Qt5 之后 由 QObject 的析构函数清除。在析构函数销毁被跟踪 QWidget 的子项之前,任何跟踪 QWidget 的 QPointers 都不会被清除。

QPointer 提供的函数和运算符与普通指针的函数和运算符相同,但算术运算符+、-、 ++ 和 --除外(它们通常仅用于对象数组)。

创建 QPointer 指针,可以使用构造函数、用 T * 赋值或相同类型的其他 QPointer 。QPointer 比较可以使用 == 和 !=,或使用 isNull() 进行测试。可以使用 nullptr 或 *xx->member 取消引用。

QPointer 和普通指针可以混用,QPointer会自动转换为指针*。可以把 QPointer
对象传递给需要 QWidget * 参数的函数。因此,声明函数时没有必要用 QPointer 作为参数,只需使用普通指针即可。

#include <QCoreApplication>
#include <QTimer>
#include <QDebug>
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QTimer *timer = new QTimer;
    // QPointer<QTimer> timer = new QTimer;
    delete timer;
    // 不使用QPointer时需要设置timer为NULL
    // timer=NULL;
    if(timer)
    {
        qInfo()<<"timer is not null";
    }
    else
    {
        qInfo()<<"timer is null";
    }
    return a.exec();
}

不使用QPointer时,输出“Label is not null”,原因是,delete后未置空,易造成指针悬空。使用 QPointer 时,输出"timer is null"。
在QPointer中 .是指针的属性,-> 是对象的属性。

QScopedPointer

QScopedPointer 类似于 C++ 11 中的 unique_ptr 用于管理动态分配的对象的独占所有权,即同一时间只能有一个QScopedPointer指向该对象。

QScopedPointer使用了RAII(资源获取即初始化)技术,当QScopedPointer被销毁时,它将自动释放所管理的对象的内存。QScopedPointer不支持拷贝和赋值操作,这是为了避免在多个指针之间共享所有权的问题。如果需要在多个指针之间转移所有权,应该使用QSharedPointer或QWeakPointer。

手动管理堆中分配的对象非常困难而且容易出错,容易导致内存泄露。QScopedPointer是一个简化内存管理的工具类,它通过将堆分配的空间赋值给基于栈的内存,通常称为RAII(resource acquisition is initialization)。
QScopedPointer保证指向的对象在当前范围消失时自动删除。
下面的函数分配了堆内存,使用后手动删除。

void myFunction(bool useSubClass)
 {
     MyClass *p = useSubClass ? new MyClass() : new MySubClass;
     QIODevice *device = handsOverOwnership();

     if (m_value > 3) {
         delete p;
         delete device;
         return;
     }

     try {
         process(device);
     }
     catch (...) {
         delete p;
         delete device;
         throw;
     }

     delete p;
     delete device;
 }

使用QScopedPointer可以简化上面的代码:

void myFunction(bool useSubClass)
 {
     // assuming that MyClass has a virtual destructor
     QScopedPointer<MyClass> p(useSubClass ? new MyClass() : new MySubClass);
     QScopedPointer<QIODevice> device(handsOverOwnership());

     if (m_value > 3)
         return;

     process(device);
 }

编译器为QScopedPointer生成的代码与手动编写的代码相同。delete代码是QScopedPointer的选项之一。QScopedPointer没有赋值构造函数或赋值操作,以便清楚的表达所有权和生命周期。
C++ 中const修饰的指针也可以使用QScopedPointer来表示。

    const QWidget *const p = new QWidget();
    // 等价于
    const QScopedPointer<const QWidget> p(new QWidget());

    QWidget *const p = new QWidget();
    // 等价于
    const QScopedPointer<QWidget> p(new QWidget());

    const QWidget *p = new QWidget();
    // 等价于
    QScopedPointer<const QWidget> p(new QWidget());

自定义清理处理器:
使用malloc分配的数组指针不能使用delete删除,QScopedPointer的第二个模板参数可以用于自定义清理处理器。QT提供了以下自定义清理处理器:

  • QScopedPointerDeleter 默认处理器,使用delete删除指针。
  • QScopedPointerArrayDeleter 使用delete []删除。用于处理使用new [] 创建的指针。
  • QScopedPointerPodDeleter 使用free()删除指针。用于处理使用malloc()创建的指针。
  • QScopedPointerDeleteLater 使用deleteLater()删除指针。用于处理QEventLoop中使用的QObject指针。
    自定义清理处理器中必须提供一个public的静态函数:public static function void cleanup(T *pointer)
 // QScopedPointer 使用 delete[] 删除数据
 QScopedPointer<int, QScopedPointerArrayDeleter<int> > arrayPointer(new int[42]);

 // QScopedPointer 使用 free()释放内存
 QScopedPointer<int, QScopedPointerPodDeleter> podPointer(reinterpret_cast<int *>(malloc(42)));

 // 该 struct 调用 "myCustomDeallocator" 删除指针
 struct ScopedPointerCustomDeleter
 {
    static inline void cleanup(MyCustomClass *pointer)
    {
        myCustomDeallocator(pointer);
    }
 };

 // QScopedPointer 使用自定义清理处理器:
 QScopedPointer<MyCustomClass, ScopedPointerCustomDeleter> customPointer(new MyCustomClass);

QScopedPointer可以使用前置声明的类,但是要保证QScopedPointer需要清理的时候前置声明的类的析构函数可用。否则,编译器将输出析构函数不可用的警告。

 class MyPrivateClass; // 前置声明 MyPrivateClass

class MyClass
{
private:
    QScopedPointer<MyPrivateClass> privatePtr; // QScopedPointer 使用前置声明类

public:
    MyClass(); // OK
    inline ~MyClass() {} // VIOLATION - 析构函数不能使用inline修饰

private:
    Q_DISABLE_COPY(MyClass) // OK - 设置拷贝构造函数和赋值操作不可用,编译器不会自动生成构造函数和赋值操作
};

成员函数

QScopedPointer::QScopedPointer(T *p = nullptr)

构造 QScopedPointer 实例,并指向p

QScopedPointer::~QScopedPointer()

销毁QScopedPointer对象。删除它指向的对象

T *QScopedPointer::data() const

返回QScopedPointer指向对象的值。QScopedPointer仍然指向该对象

T *QScopedPointer::get() const

与 data()功能相同

bool QScopedPointer::isNull() const

QScopedPointer指向的对象为nullptr时返回true

void QScopedPointer::reset(T *other = nullptr)

删除已指向的对象并指向新对象other

QScopedArrayPointer

QScopedArrayPointer 与 QScopedPointer 类似,但是删除指针时使用的时 delete[] 操作。QScopedArrayPointer 存储的指针指向动态分配的数组对象。如果我们指向的内存数据是一个数组,这时可以用 QScopedArrayPointer。例如:

 void foo()
 {
     QScopedArrayPointer<int> i(new int[10]);
     i[2] = 42;
     ...
     return; // 此时我们定义的 integer 数组会使用 delete[] 自动删除
 }

QSharedPointer

QSharedPointer 相当于C++11 标准中的 shared_ptr, 用于管理动态分配的对象的共享所有权,即多个 QSharedPointer 对象可以指向同一个对象,并共享该对象的内存管理。它使用引用计数来追踪对象的使用情况,当最后一个 QSharedPointer 对象被销毁时,它将自动删除它所持有的指针。由于使用了引用计数,QSharedPointer 能够自动处理指针的生命周期,避免内存泄漏和空悬指针等问题,因此是Qt中最常用的智能指针。

需要注意的是,QSharedPointer只能管理动态分配的对象的内存。如果我们将其用于指向栈对象或全局对象,那么它就不会自动释放对象的内存,这可能会导致程序崩溃或内存泄漏。

创建 QSharedPointer 对象可以用普通指针、另一个 QSharedPointer 对象,也可以通过将 QWeakPointer 对象提升为强引用来创建。

线程安全

QSharedPointer 和 QWeekPointer 是可重入类,如果不进行同步,多个线程无法同时访问指定的 QSharedPointer 对象或 QWeakPointer 对象。

多个 QSharedPointer 和 QWeekPointer 指向同一个对象的情况下,多个线程可以安全的访问这些 QSharedPointer 和 QWeekPointer对象。QSharedPointer 采用的引用计数机制是原子操作,不需要手动同步。但是要注意,QSharedPointer 和 QWeekPointer 所指向的对象不一定是线程安全的,需要采用线程安全和重入规则来保证 QSharedPointer 和 QWeekPointer 所指向对象的安全。

其他指针类

Qt还提供了另外两个指针包装类: QPointer 和 QSharedDataPointer。它们彼此不兼容,因为每个都有其非常不同的用例。

QSharedPointer 通过外部引用计数(即放置在对象外部的引用计数器)持有共享指针,指针值在 QSharedPointer 和 QWeekPointer 的所有实例之间共享。但是,指针指向的对象不应被视为共享的:都是同一个对象。QSharedPointer 不提供detach(隐式共享)或拷贝所引用对象的方法。

QSharedDataPointer 通过基类 QSharedData 内的引用计数持有共享数据的指针(共享数据派生自 QSharedData 类)。QSharedDataPointer 可以根据访问类型对受保护数据执行detach(隐式共享):如果不是读访问,则以原子方式创建一个副本以完成操作。

QExplicitlySharedDataPointer 是 QSharedDataPointer 的一个变量,QSharedDataPointer只有在 QExplicitlySharedDataPointer::detach() 时才会执行detach。

QScopedPointer 专为堆内分配、删除对象设计,它持有指向堆分配对象的指针,并在其析构函数中删除对象。 QScopedPointer 是轻量级的,它不使用额外的结构或引用计数。

QPointer 用于持有 QObject 派生对象的指针,但是一种弱引用。QWeakPointer 具有相同的功能,但不推荐使用该功能。

可选指针跟踪
编译调试时可以开启 QSharedPointer 的指针跟踪功能。启用后,QSharedPointer 会在全局集合中注册它跟踪的所有指针。这样就可以捕获错误(例如:将同一指针分配给两个 QSharedPointer 对象)。

开启指针跟踪功能需要在 include QSharedPointer 前定义宏QT_SHAREDPOINTER_TRACK_POINTERS。

即使没有启用指针跟踪功能进行代码编译,使用 QSharedPointer 跟踪指针也是安全的。如果编译的代码没有开启指针跟踪功能,QSharedPointer 会从跟踪器中删除指针。

注意,指针跟踪功能对多重继承或虚拟继承有限制(此时两个不同的指针可以引用同一对象)。在这种情况下,如果指针被强制转换为不同的类型并且其值发生更改,则 QSharedPointer 的指针跟踪机制可能无法检测到被跟踪的对象是否为同一对象。

QWeakPointer

QWeakPointer 是对指针的弱引用,相当于C++11 标准中的 weak_ptr。QWeakPointer 不影响指针引用计数,可以用于验证指针是否在已另一个上下文中被删除。QWeakPointer对象的创建只能通过QSharedPointer赋值。QWeakPointer 用于追踪指针,但并不代表指针本身,它不保证指针对象的有效性,也不提供转换操作。如果要访问 QWeakPointer 追踪的指针,需要向将其提升为 QSharedPointer 并验证该对象是否为 null,若对象不为 null,则可以使用该指针。QWeakPointer::toStrongRef()用于将 QWeakPointer 转换为 QSharedPointer。
QWeakPointer 指向 QSharedPointer 所管理的对象,但不会增加对象的引用计数,也不会影响对象的生命周期。当对象被释放时,QWeakPointer会自动被置为空指针,避免了空悬指针的问题。

#include <QSharedPointer>
#include <QWeakPointer>
#include <QDebug>

class MyClass
{
public:
    MyClass(int value) : m_value(value) {
        qDebug() << "MyClass constructor called with value" << m_value;
    }
    ~MyClass() {
        qDebug() << "MyClass destructor called with value" << m_value;
    }
    int getValue() const {
        return m_value;
    }

private:
    int m_value;
};

int main()
{
    QSharedPointer<MyClass> shared(new MyClass(20));
    QWeakPointer<MyClass> weak(shared);

    qDebug() << "Shared pointer value:" << shared->getValue();
    qDebug() << "Weak pointer value:" << weak.data()->getValue();

    shared.clear();
    // 此时,MyClass对象的引用计数为0,将被自动删除,而此时 QWeakPointer 对象 weak 也为null。

    if (weak.isNull()) {
        qDebug() << "Weak pointer is null - object has been deleted"; //执行
    } else {
        qDebug() << "Weak pointer is not null - object still exists";
    }

    return 0;
}

QSharedDataPointer

QSharedDataPointer
用于简化隐式共享类的实现。QSharedDataPointer 实现了线程安全的引用计数,保证了向可重入类添加 QSharedDataPointers 时不破坏的的可重入性。Qt 中很多类都采用了隐式共享来提升指针的访问速度和内存使用效率。Qt 的容器类都使用了隐式共享,如 QList 、QVarLengthArray 、QStack 、QQueue、 QSet、 QMap、 QMultiMap、 QHash、 QMultiHash 。
例如,实现 Employee 类的隐式共享,需要以下步骤:

  1. 定义 Employee 类,并声明一个 QSharedDataPointer
    成员变量;
  2. 定义一个 EmployeeData 并继承 QSharedData,将 Employee 需要的成员变量在 EmployeeData 中进行声明。
#include <QSharedData>
#include <QString>

// EmployeeData 继承自 QSharedData,QSharedData为其提供了一个引用计数器
// EmployeeData 要实现共享必须提供默认的构造函数、拷贝构造函数和析构函数
// 如果要对 Employee 类的使用者隐藏数据,应该把 EmployeeData 类声明到独立的 .h 文件中
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; }
    // 该构造函数调用了 setId(id) 和 setName(name), 2次修改共享数据,但是并不会触发 copy on write
    Employee(int id, const QString &name) {
        d = new EmployeeData;
        setId(id); // 此时 EmployeeData 的引用计数为 1
        setName(name); // 引用计数为 1 ,不会 copy 共享数据
    }
    // 这个拷贝构造函数可以不用定义,编译器会自动生成拷贝构造函数
    // 拷贝构造函数会对成员变量 d 进行赋值,赋值操作 =() 会增加共享数据的引用计数
    Employee(const Employee &other)
          : d (other.d)
    {
    }
    // 修改数据时,操作符 ->() 会自动调用 detach() ,如果共享数据的引用计数大于1,会自动copy一份数据用于修改。
    // 保证一个 Employee 对象对共享数据的修改不会影响其它Employee对象引用的共享数据
    void setId(int id) { d->id = id; }
    void setName(const QString &name) { d->name = name; }

    // 不修改 EmployeeData ,操作符 -> 不会触发detach()
    int id() const { return d->id; }
    QString name() const { return d->name; }

  private:
    // Employee 只用一个成员变量,其它数据在 EmployeeData 中定义
    QSharedDataPointer<EmployeeData> d;
};

在上面的场景中,Employee 对象拷贝、赋值或作为参数传递时,QSharedDataPointer 自动增加引用计数。Employee 对象删除或不在当前范围时, QSharedDataPointer 自动减少引用计数。当引用计数为 0 时,EmployeeData 对象自动被删除。当 Employee 的成员函数对成员变量 d 进行修改时,QSharedDataPointer 自动调用 detach() 操作,并为 Employee 对象拷贝出一份数据,保证 Employee 对象对数据的修改不会影响其它对象。如果 Employee 有多个成员函数都会修改成员变量 d,detach() 会被调用多次,但是只有首次调用 detach() 时才会拷贝共享数据,因为第一次调用 detach() 后成员变量 d 指向新拷贝的数据,它的引用计数为 1。

隐式共享需要注意的问题:
看下面的代码,如果 e1 和 e2 都代表 id 为 1001 的数据,那么将出现 1001 有 2 个不同 name 的问题,这在数据一致性上可能会有问题。这种问题需要使用显示共享来解决,即在 Employee 类中声明为显示共享 QExplicitlySharedDataPointer
d; 显示共享不会自动执行 copy on write 操作。

#include "employee.h"

int main()
{
    Employee e1(1001, "Albrecht Durer");
    // e1 和 e2 指向同一个共享数据(1001, "Albrecht Durer")
    Employee e2 = e1;
    // setName 后成员变量 e1 将指向另一个数据(1001, "Hans Holbein")
    e1.setName("Hans Holbein");
}

可以考虑使用宏Q_DECLARE_TYPEINFO()将隐式共享类声明为 movable 类型,例如上面的 Employee 类。

隐式共享的 iterator 问题:当使用 iterator 遍历容器时,不能进行容器拷贝。例如:

QList<int> a, b;
// 构造一个用 0 初始化的 list
a.resize(100000); 

QList<int>::iterator i = a.begin();
// 下面是 iterator i 的错误使用方式

b = a;

// 与 STL 容器不同的是,如果此时执行 *i = 4 会发生隐式拷贝

a[0] = 5;
// 此时 a 已经从共享数据中分离出来,虽然 i 是 容器 a 的遍历器,但实际上是 b 的遍历器,此时 (*i) == 0

b.clear(); 
// 此时遍历器 i 已经完全无效了

int j = *i; 
// 此时会出现 i 未定义的情况

// 如果使用的是 STL 的容器类 std::list<T> 不会发生上述情况,此时 (*i) == 5

QExplicitlySharedDataPointer

QExplicitlySharedDataPointer
用于简化显式共享类的实现。QExplicitlySharedDataPointer 实现了线程安全的引用计数,保证了向可重入类添加 QSharedDataPointers 时不破坏的的可重入性。
QExplicitlySharedDataPointer 与 QSharedDataPointer 类似,惟一不同的是 QExplicitlySharedDataPointer 的成员函数在出现写操作时不会自动 copy 共享数据。QExplicitlySharedDataPointer 的 detach() 方法只能手动调用。QExplicitlySharedDataPointers 自动进行引用计数并在引用计数变为 0 时删除共享数据。

参考文章:
Qt 智能指针介绍: QSharedPointer、QWeakPointer 、QScopedPointer 、QPointer(附实例)
Qt 中的智能指针
【图解】Qt中的智能指针
Qt 智能指针学习(7种QT的特有指针)

Google Colaboratory(Colab)是一个由 Google 提供的云端 Jupyter 编程笔记本,直接通过浏览器即可进行 Python 编程。Colab 充分利用谷歌的闲置云计算资源,为公众提供免费的的在线编程服务,以及免费的 GPU 资源,虽然在使用方面有一定的规则限制,但对于一般的研究和学习来说绰绰有余。

访问 Colab,可以新建笔记本,也可以从 Google Drive、Github 载入笔记本,或直接从本地上传。

Colab 免费提供的 Python 编程环境十分慷慨,如下图,足有 12 GB 的内存和 100 GB 的硬盘。

如果需要使用硬件加速,可以切换运行时类型,笔者的账户可以免费使用 T4 GPU 和 TPU。

不过需要注意,Colab 运行时是临时的,平台会监测运行时的活动状态,长时间的闲置和长时间的高强度使用,都会导致运行时被收回释放,所有数据都会被清空。

Colab 上的数据持久化

为了让 Colab 可以持久化地保存文件,人们通常会选择使用 Google Drive。如下图,使用时在界面左侧的文件管理中点击按钮即可将 Google Drive 挂载到运行时,把需要长期保留或重复使用的数据保存在里面,再次使用可以从 Google Drive 中加载,这就避免了运行被释放时丢失数据。

除了 Google Drive 以外,你还可以使用 JuiceFS 作为 Colab 笔记本的持久化存储,从而更为灵活地保存和共享更大规模的数据。

JuiceFS 与 Google Drive

这里先提供一个表格供读者参考,后文会展开介绍 JuiceFS 的技术架构以及如何创建一个适用于 Google Colab 的文件系统。

简言之,Google Drive 有平台优势,更容易集成到 Colab,也有多种容量规格以供扩容,但在使用上会有一些限制,比如单位时间的总上传量,总文件数量等。而 JuiceFS 是自建服务,没有此类限制,而且在费用方面可以通过灵活地组织资源来降低是用成本。

JuiceFS Google Drive
价格 弹性费用(取决于元数据引擎和对象存储的费用) 按固定容量订阅
集成到 Colab 简单 简单
是否需要维护 需要 不需要
扩容能力 无容量上限 15GB ~ 30TB
上传限制 无限制 24 小时内可向云端硬盘上传和复制 750 GB 数据
跨平台共享 灵活 一般

使用 JuiceFS

JuiceFS 是面向云的高性能分布式文件系统,它在 Apache-2.0 协议下开源,具有完备的 POSIX 兼容性,并支持 FUSE POSIX、HDFS、S3、Kubernetes CSI Driver、WebDAV 等多种访问方式。

在 Colab 中可以直接采用 FUSE POSIX 方式,以守护进程形式挂载到运行时中使用。

技术架构

一个典型的 JuiceFS 文件系统由一个负责存储数据的对象存储和一个负责存储元数据的元数据引擎组成。

在对象存储方面,JuiceFS 支持几乎所有公有云对象存储、私有部署的对象存储、NFS、SFTP 以及本地磁盘等。在元数据引擎方面,支持 Redis、Postgres、MySQL、TiKV、SQLite 等多种数据库。

因为采用了数据与元数据分离存储的架构,JuiceFS 文件系统的读和写操作会先在元数据引擎上进行查询和处理,只有涉及到实际数据时才会访问对象存储,这样就能极高效地处理海量的数据,相比于直接与对象存储交互,JuiceFS 会有更好的性能表现。

简言之,元数据引擎至关重要,想要提升 JuiceFS 文件系统的性能,一个黄金法则是“尽量靠近业务端部署元数据引擎”。以 Colab 为例,它的服务器大多位于美国,所以找一个美国的云服务器来部署 Redis 并搭配一个可靠的对象存储是构建 JuiceFS 文件系统相对理想的搭配。

适用于 Colab 的组合

元数据引擎

为了在 Colab 上尽量发挥 JuiceFS 的潜能,笔者选择了一个位于美国硅谷的云服务器,将它用于部署 Redis 作为 JuiceFS 的元数据引擎,配置如下:

  • 位置:美国硅谷
  • CPU:2 核
  • RAM:4GB
  • SSD: 60GB
  • 系统:Ubuntu Server 22.04
  • IP:18.18.18.18
  • 域名:redis.xxx.com

注:上述 IP 和域名均为演示目的编写,如有需要请替换成你的真实信息

按照 JuiceFS 官方提供的数据,采用 Redis 这类键值数据库作为元数据引擎时,存储占用大概为 300 字节/文件,那么 1GB 内存大概可以存储 350 万个文件的元数据,读者可以根据预期的文件总量来决定给服务器配置多少内存。

在本文中,笔者使用 Docker 部署 Redis,并通过 Let's Encrypt 签发了一个免费的 SSL 证书对服务端进行加密:

# 拉取 redis 镜像
sudo docker pull bitnami/redis:7.2

# 删除已存在的同名 redis 容器(如果存在的话)
sudo docker rm -f redis

# 创建新的 redis 容器
sudo docker run -d --name redis \
-p 6379:6379 \
-v redis_aof_data:/bitnami/redis/data \
-v ./ssl:/ssl \
-e REDIS_PASSWORD=abcdefg \
-e REDIS_TLS_ENABLED=yes \
-e REDIS_TLS_PORT_NUMBER=6379 \
-e REDIS_TLS_AUTH_CLIENTS=no \
-e REDIS_TLS_CERT_FILE=/ssl/redis.xxx.com.crt \
-e REDIS_TLS_KEY_FILE=/ssl/redis.xxx.com.key \
-e REDIS_TLS_CA_FILE=/ssl/ca.crt \
--restart unless-stopped \
bitnami/redis:7.2

该 Redis 实例启用了以下功能:

  • AOF(追加写到本地文件):它会将每个写操作都记录到本地磁盘,从而提高数据的安全性。
  • 服务端 SSL:Redis 服务器会使用 SSL/TLS 协议与客户端进行通信。将 SSL 证书放在 ssl 目录中即可,注意修改环境变量中的证书文件名称。

Redis 部署完毕后,还需要检查防火墙,确保服务器开放了 6379 端口的入站请求。这样,Redis 元数据引擎就准备完毕了。

对象存储

对象存储方面,笔者选择使用 Cloudflare R2,因为它上下行流量均免费,只需为存储和 API 请求付费,很适合 Colab 这种需要在外部访问 JuiceFS 的场景。特别是存储量较大的情况,每次都要将模型数据载入到 Colab 运行时,下行收费会产生不小的开支。

以下是演示目的编写的对象存储信息,请在实际配置时替换成自己的真实信息:

创建文件系统

元数据引擎和对象存储都准备好了,接下来使用 JuiceFS 客户端来创建文件系统。

这个步骤可以在任何一台支持安装 JuiceFS 客户端的电脑上执行,可以是你本地正在使用的电脑或是部署了 Redis 实例的那台云服务。

因为 JuiceFS 是基于云的,只要 JuiceFS 客户端能够访问到元数据引擎和对象存储就可以创建和使用。

首先,安装 JuiceFS 客户端:

# macOS 或 Linux 系统
curl -sSL https://d.juicefs.com/install | sh -

# Windows 系统(建议使用 Scoop)
scoop install juicefs

其他系统及安装方法请参考
JuiceFS 官方安装文档

然后,使用已准备的元数据引擎和对象存储来创建文件系统:

# 创建文件系统
juicefs format --storage s3 \
--bucket https://xxx.r2.cloudflarestorage.com/myjfs \
--access-key abcdefg \
--secret-key gfedcba \
rediss://:abcdefg@redis.xxx.com/1 \
myjfs

JuiceFS 文件系统只需要创建一次,然后就可以在任何安装了 JuiceFS 客户端的设备上挂载和使用,它是基于云的共享文件系统。可以多设备、跨地区、跨网络同时读写访问。

现在你可以在任何安装了 JuiceFS 客户端的设备上挂载使用这个文件系统,以下是几种常用的访问方式:

# 以 FUSE POSIX 方式挂载
juicefs mount rediss://:abcdefg@redis.xxx.com/1 mnt

# 以 S3 Gateway 形式挂载
export MINIO_ROOT_USER=admin  
export MINIO_ROOT_PASSWORD=12345678
juicefs gateway rediss://:abcdefg@redis.xxx.com/1 localhost:9000

# 以 WebDAV 形式挂载
juicefs webdav rediss://:abcdefg@redis.xxx.com/1 localhost:8000

可以看到,挂载 JuiceFS 文件系统时只需指定元数据引擎 URL,不再需要对象存储相关的信息。这是因为在创建文件系统的时候,对象存储相关的信息已经被写入了元数据引擎。

在 Colab 中挂载 JuiceFS

如下图,Colab 运行时的底层是一个 Ubuntu 系统,所以,只需要在 Colab 上安装 JuiceFS 客户端,执行挂载命令即可使用。

可以将安装命令和挂载命令放在一个代码块中,也可以像下图这样,将它们分成两个独立的代码块。

请注意,挂载 JuiceFS 时不要忘记 -d 选项,它的作用是让 JuiceFS 以守护进程的方式挂载到后台。因为 Colab 每次只允许一个代码块运行,如果不将 JuiceFS 挂载到后台,他就会一直让代码块处于运行状态,导致其他代码块无法运行。

如下图,左侧文件管理器中可以看到已挂载的 JuiceFS 文件系统。

使用举例

例一:用 JuiceFS 保存 Fooocus 模型

Fooocus 是开源的 AI 图片生成器,底层仍然使用 Stable Diffusion 模型,但将复杂的参数进行了调优和封装,让用户可以获得像 Midjourney 一样简单直观的作图体验。

你可以直接使用 Fooocus 官方提供的
Colab Notebook
,在其基础上添加安装和挂载 JuiceFS 文件系统的代码块。

也可以参考以下代码更灵活地创建和管理 Fooocus 相关的代码:

# 安装 JuiceFS 客户端
!curl -sSL https://d.juicefs.com/install | sh -

# 挂载 JuiceFS 文件系统
!juicefs mount rediss://:abcdefg@redis.xxx.com/1 myjfs -d

# 在 JuiceFS 中创建 Fooocus 模型目录结构
!mkdir -p myjfs/models/{checkpoints,loras,embeddings,vae_approx,upscale_models,inpaint,controlnet,clip_vision}

# 克隆 Fooocus 仓库
!git clone https://github.com/lllyasviel/Fooocus.git

在 Fooocus 项目根目录创建一个自定义的 config.txt 文件,让 Fooocus 以 JuiceFS 中的目录作为默认的模型存储目录:

{
  "path_checkpoints": "/content/myjfs/models/checkpoints",
  "path_loras": "/content/myjfs/models/loras",
  "path_embeddings": "/content/myjfs/models/embeddings",
  "path_vae_approx": "/content/myjfs/models/vae_approx",
  "path_upscale_models": "/content/myjfs/models/upscale_models",
  "path_inpaint": "/content/myjfs/models/inpaint",
  "path_controlnet": "/content/myjfs/models/controlnet",
  "path_clip_vision": "/content/myjfs/models/clip_vision"
}

启动 Fooocus

!pip install pygit2==1.12.2
%cd /content/Fooocus
!python entry_with_update.py --share

初次使用需要从公共仓库下载模型,这会需要一些时间,你可以在本地同时挂载 JuiceFS 文件系统,观察模型保存的情况。

再次使用时,只需挂载 JuiceFS 文件系统并确保 Fooocus 可以从中读取模型。程序会动态地从 JuiceFS 拉取所需的模型,尽管这仍然需要一些时间,但相比每次都从公共仓库完全重新下载要更加方便。特别是模型经过微调或产生了自定义数据情况,使用 JuiceFS 保存相应数据的优势就会更加明显。

例二:用 JuiceFS 保存 Chroma 向量数据库

在 Colab 上构建 RAG(检索增强生成)应用也是比较常见的,这往往涉及到要把各种资料生成的 embedding 数据保存到向量数据库。

Llamaindex 默认采用 OpenAI 的 text-embedding 模型对输入的数据进行向量化,如果不想每次都重新生成 embedding 数据,就需要将这些数据保存到向量数据库。比如使用开源的 Chroma 向量数据库,因为它默认将数据保存在本地磁盘,在 Colab 中需要注意数据库的保存位置,以防运行时收回造成数据丢失。

这里笔者提供一组 Colab 笔记本代码,让你可以将 Llamaindex 生成的 embedding 保存到 Chroma 数据库,而这个 Chroma 数据库将完全保存到 JuiceFS。

# 安装 JuiceFS 客户端
!curl -sSL https://d.juicefs.com/install | sh -

# 挂载 JuiceFS 文件系统
!juicefs mount rediss://:abcdefg@redis.xxx.com/1 myjfs -d

# 安装 Llamaindex 和 chroma 相关的包
!pip install llama-index chromadb kaleido python-multipart pypdf cohere

# 从 Colab 环境变量读取 OpenAI API 密钥
from google.colab import userdata
import openai

openai.api_key = userdata.get('OPENAI_API_KEY')

把需要转换成 embedding 的文件放在 myjfs/data/ 目录中,执行以下代码生成 embedding 并保存到 Chroma。

得益于 JuiceFS 基于云的共享访问特性,可以同时在本地挂载 JuiceFS 并将所需的资料放入相应的目录。

import chromadb
from llama_index import VectorStoreIndex, SimpleDirectoryReader
from llama_index.vector_stores import ChromaVectorStore
from llama_index.storage.storage_context import StorageContext

# load some documents
documents = SimpleDirectoryReader("./myjfs/data").load_data()

# initialize client, setting path to save data
db = chromadb.PersistentClient(path="./myjfs/chroma_db")

# create collection
chroma_collection = db.get_or_create_collection("great_ceo")

# assign chroma as the vector_store to the context
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)

# create your index
index = VectorStoreIndex.from_documents(
    documents, storage_context=storage_context,
    embed_model_name="text-embedding-3-small",
)

使用时,直接让 Chroma 从 JuiceFS 读取数据。

import chromadb
from llama_index import VectorStoreIndex, SimpleDirectoryReader
from llama_index.vector_stores import ChromaVectorStore
from llama_index.storage.storage_context import StorageContext

# initialize client, setting path to save data
db = chromadb.PersistentClient(path="./myjfs/chroma_db")

# create collection
chroma_collection = db.get_or_create_collection("great_ceo")

# assign chroma as the vector_store to the context
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)

# 从向量数据库加载索引
index = VectorStoreIndex.from_vector_store(
    vector_store, storage_context=storage_context
)

测试一下用自己 Index 作为知识库与 GPT 进行对话:

# create a query engine and query
query_engine = index.as_query_engine()
response = query_engine.query("这本书讲了什么?")
print(response)

这样一来,每次进入新的 Colab 运行时,只要挂载 JuiceFS 就可以直接使用这些已创建的向量数据。其实,不止是在 Colab,任何需要访问这些向量数据的地方都可以通过挂载 JuiceFS 来使用。

总结

本文介绍了如何在 Google Colab 中使用 JuiceFS 来持久化保存数据,通过实例介绍了如何为 JuiceFS 准备元数据引擎和对象存储来尽量发挥它的性能,以及在 Colab 中的安装和挂载方法。最后通过 Fooocus 和 Chroma 两个例子,演示了在实际应用中如何利用 JuiceFS 来更好地保存并重复利用数据。

希望这篇文章的内容能够对你起到一定的帮助,如果你有任何疑问,欢迎在评论区留言讨论。如果你对 JuiceFS 有兴趣,可以查看
官方文档
了解更多用法和性能调优方面的内容。

希望这篇内容能够对你有一些帮助,如果有其他疑问欢迎加入
JuiceFS 社区
与大家共同交流。

今天无聊,想起原来开发的待办列表TodoList里还缺个提醒声音,于是就添加了提供声音模块代码。然后想着记录一下,让更多的读者能够复用这个模块代码,于是就有了此博文。这个例子只是用于播放资源文件里的wav音频文件,如果要播放本地文件,那请读者自己添加代码,直接调用操作类里的函数即可。

1、
项目目录;

2、
源码介绍;

1) 播放资源文件操作类;

2) 使用操作类;

3、
运行界面;

弄了个简单的应用界面:

4、
使用介绍;

直接参考例子里的源码复制过去使用即可。

5、
源码下载;

提供源码下载:
https://download.csdn.net/download/lzhdim/88976439

6、
其它建议;

这个例子只是播放的资源文件里的声音,如果播放本地文件,则直接应用操作类里的其它函数。具体请读者自己修改代码。

这个例子简单的介绍了C#里播放资源音频文件,请需要的读者自己复用例子中的代码。