【UE5 C++课程系列笔记】22——多线程基础——FRunnable和FRunnableThread

目录

1、FRunnable

1.1 概念

1.2 主要成员函数

(1)Init 函数

(2)Run 函数

(3)Stop 函数

(4)Exit 函数

2、FRunnableThread

2.1 概念

2.2 主要操作

(1)创建线程

(2)停止线程

3、 使用示例

(1)解决线程竞争方式1:加锁

(2)解决线程竞争方式2:线程安全布尔类型FThreadSafeBool

案例源代码


1、FRunnable

1.1 概念

  FRunnable 是一个抽象基类,定义了线程执行任务的基本接口和生命周期相关的函数,它提供了一种规范,让开发者可以基于这个抽象类派生出自己的类,并重写其中的方法来定义具体在线程中要执行的任务以及处理线程的启动、停止、暂停等生命周期相关操作。通过实现 FRunnable 接口,虚幻引擎能够以统一的方式管理和调度这些自定义的可运行任务,方便地将复杂的计算、耗时操作等放到单独的线程中执行,提高程序的并发性能和响应速度,同时避免阻塞主线程影响用户体验。

1.2 主要成员函数

(1)Init 函数

        函数原型:virtual bool Init();

        作用:该函数在线程启动时被调用,用于进行一些初始化操作,例如初始化线程中需要使用的资源(如打开文件、创建数据库连接、初始化数据结构等)。如果初始化成功,函数应该返回 true;如果初始化过程中出现错误,导致线程无法正常启动运行,应该返回 false,此时线程将不会继续执行后续的操作。

        示例代码:

class MyRunnable : public FRunnable
{
public:virtual bool Init() override{// 初始化一些资源,比如创建一个数组等MyDataArray.Empty();MyDataArray.Add(10);MyDataArray.Add(20);return true;}
};

(2)Run 函数

        函数原型:virtual uint32 Run();

        作用:这是线程执行主体任务的函数,在线程启动并且 Init 函数成功执行后被调用。开发者需要在这个函数中编写具体的线程执行逻辑,例如进行复杂的数学计算、数据处理、网络通信等耗时操作。函数返回值通常表示线程的退出码(一般返回 0 表示正常退出,其他非零值可以根据具体需求定义为不同的错误码等情况)。

        示例代码:

class MyRunnable : public FRunnable
{
public:virtual uint32 Run() override{for (int i = 0; i < 1000; ++i){// 模拟一些耗时的计算操作,比如累加计算Result += i;FPlatformProcess::Sleep(0.001);  // 模拟每次计算间隔一点时间,避免 CPU 占用过高}return 0;}
private:int Result = 0;
};

(3)Stop 函数

        函数原型:virtual void Stop();

        作用:当线程需要停止时(比如外部调用了停止线程的相关函数,或者线程执行完了预设的任务等情况),这个函数会被调用,用于进行一些清理资源的操作,例如关闭文件句柄、释放动态分配的内存、断开网络连接等,确保线程在结束时不会遗留未处理的资源,避免造成内存泄漏或其他资源相关的问题。

示例代码:

class MyRunnable : public FRunnable
{
public:virtual void Stop() override{// 释放之前在 Init 或者其他地方动态分配的内存if (MyAllocatedMemory!= nullptr){FMemory::Free(MyAllocatedMemory);MyAllocatedMemory = nullptr;}}
private:void* MyAllocatedMemory = nullptr;
};

(4)Exit 函数

        函数原型:virtual void Exit();

        作用:这个函数在整个线程生命周期结束后被调用,主要用于一些最后的清理或者记录线程执行相关信息等操作,相对来说它的使用场景比 Stop 函数更偏向于整个线程完全结束后的一些收尾工作,不过在很多简单的实现中,可能和 Stop 函数的功能有一定重叠,具体取决于开发者的需求和实际的业务逻辑。

class MyRunnable : public FRunnable
{
public:virtual void Exit() override{// 记录线程执行的一些统计信息,比如执行时间等FDateTime EndTime = FDateTime::Now();double ExecutionTime = (EndTime - StartTime).GetTotalSeconds();UE_LOG(LogTemp, Log, TEXT("线程执行时间为: %f 秒"), ExecutionTime);}
private:FDateTime StartTime;
};

2、FRunnableThread

2.1 概念

  FRunnableThread 是用于实际创建和管理线程的类,它提供了与操作系统底层线程创建机制的接口,能够基于不同的操作系统(如 Windows、Linux、macOS 等)创建对应的线程,并将 FRunnable 派生类中定义的任务(也就是 Run 函数里的内容)放到所创建的线程中去执行。它屏蔽了不同操作系统线程创建和管理的差异,让开发者可以用统一的方式在虚幻引擎中使用多线程功能,并且可以对线程进行一些基本的控制,比如启动、暂停、恢复、停止等操作。

2.2 主要操作

(1)创建线程

        在如下示例代码中,首先创建了 MyCustomRunnable 类的实例对象 runnable,这个类继承自 FRunnable 并定义了线程的具体任务等逻辑。然后通过 FRunnableThread::Create 函数创建了一个名为 "MyCustomThread"、优先级为正常的线程,将 runnable 对象传递进去,使得这个线程启动后会执行 MyCustomRunnable 类中 Run 函数里定义的任务内容。

MyCustomRunnable* runnable = new MyCustomRunnable();
FRunnableThread* thread = FRunnableThread::Create(runnable, TEXT("MyCustomThread"), TPri_Normal);

(2)停止线程

        要停止线程的运行,可以调用停止线程的相关函数,这会触发 FRunnable 派生类中的 Stop 函数被调用,然后线程会正常结束执行,释放相关资源并退出。

thread->Stop();  

3、 使用示例

        通过结合前面学到的子系统、共享指针和本节的多线程内容,实现在线程中循环输出打印信息的功能。 

1. 首先新建一个空白C++ 类,这里命名为“ThreadSubsystem”

让类“ThreadSubsystem”继承“UGameInstanceSubsystem”,并添加必要的反射信息

重写父类“UGameInstanceSubsystem”的“ShouldCreateSubsystem”、“Initialize”、“Deinitialize”方法。其中“ShouldCreateSubsystem”用于询问是否应该实际创建该子系统,“Initialize”用于进行各种初始化操作,“Deinitialize”用于在子系统生命周期结束,需要被销毁时进行清理操作。

添加创建线程和回收线程的方法,这里分别命名为“InitSimpleThread”和“ReleaseSimpleThread”

在创建子系统时创建线程,在子系统销毁时回收线程。

2. 再新建一个空白C++ 类,这里命名为“SimpleRunnable”

让“SimpleRunnable”类继承“FRunnable”,然后重写父类的“Init”、“Run”、“Exit”、“Stop”函数

定义一个FString类型的线程名称SimpleRunnableThreadName,默认值为None,再通过构造函数初始化成员变量 SimpleRunnableThreadName

由于SimpleRunnableThreadName是受保护的,因此再定义一个方法“GetThreadName”用于获取线程名称。

3. 在“ThreadSubsystem”中引入“SimpleRunnable”,然后定义一个TSharedPtr智能指针类型的成员变量SimpleRunnable,用于管理 FSimpleRunnable 类的实例对象。通过使用智能指针,可以方便地对 FSimpleRunnable 实例的生命周期进行管理,避免手动处理内存释放等问题,同时在多线程环境下也能更好地保证对象的有效性和安全性。当多个地方需要访问和使用FSimpleRunnable实例时,智能指针可确保只有在所有引用都释放后,实例对象才会被真正销毁,有效地防止了内存泄漏和悬空指针等问题,并且在创建、赋值、传递等操作上更加便捷和安全。

再声明了一个名为 SimpleThreadPtr 的指针变量,类型为 FRunnableThread

实现方法“InitSimpleThread”、“ReleaseSimpleThread”如下。

在第25行代码中,使用 MakeShared 函数创建了一个 FSimpleRunnable 类型的实例,并通过智能指针 SimpleRunnable来管理实例的生命周期。在创建 FSimpleRunnable 实例时,传入字符串Thread000作为线程名称。

在第26行代码中,调用 FRunnableThread::Create 函数来创建实际的线程对象。第一个参数 SimpleRunnable.Get() 用于获取 SimpleRunnable 智能指针所指向的 FSimpleRunnable 实例对象的地址,使得创建的线程知道要执行的具体任务逻辑是由这个 FSimpleRunnable 实例定义的(也会执行 FSimpleRunnable 类中重写的 Run 函数里的内容)。第二个参数 *SimpleRunnable->GetThreadName() 是获取前面创建的 FSimpleRunnable 实例中设置的线程名称。最终,将 FRunnableThread::Create 函数返回的指向新创建的线程对象的指针赋值给 SimpleThreadPtr,后续就可以通过这个指针来操作该线程(比如启动、暂停、停止等操作)

在“ReleaseSimpleThread”函数中首先通过 SimpleRunnable.IsValid() 检查 SimpleRunnable 智能指针所指向的 FSimpleRunnable 实例是否有效。如果有效,就调用该实例的 Stop 函数。再通过判断 SimpleThreadPtr 是否为非空指针来确定之前创建的线程对象是否存在。如果存在,就调用 SimpleThreadPtr->WaitForCompletion() 函数,该函数的作用通常是让当前调用线程阻塞等待,直到被 SimpleThreadPtr 指向的线程执行完成(也就是线程执行完了 FSimpleRunnable 类中 Run 函数里定义的任务逻辑,并且执行了相关的停止和清理操作后正常退出)。这样做可以确保在后续对线程相关资源进行进一步清理或者对整个子系统进行状态更新时,线程已经完全结束,避免出现数据不一致或者资源访问冲突等问题。

4. 在“SimpleRunnable”类中重写Run函数如下。通过while循环确保线程一直执行,再调用FPlatformProcess::Sleep(0.5)函数,让线程暂停 0.5 秒,使得 CPU 有时间去处理其他线程或者主线程的任务。

5. 编译后,新建一个Actor类,这里命名为“BP_ThreadActor”,在事件图表中调用“ThreadSubsystem”的“InitSimpleThread”和“ReleaseSimpleThread”方法

运行后可以看到一直在打印日志

但是当想结束运行时发现点击结束按钮后卡住。

下面开始改进,首先定义一个打印信息的方法“PrintWaring”,其中AsyncTask 是虚幻引擎提供的一个用于在指定线程中异步执行任务的函数,这里传入了 ENamedThreads::GameThread 表示要将打印任务放到游戏线程中去执行。

再定义一个打印线程信息的方法“PrintThreadInfo”

需要引入“ThreadManager”

定义一个布尔变量bRunning ,只有bRunning 为true的时候循环才继续执行,否则退出循环并结束线程。

6. 重新编译后,在关卡蓝图中设置通过1、2键控制线程的启动和停止

运行结果如下,可以看到线程成功启动,执行一段时间后,成功被手动停止。

但是此时还会存在线程竞争问题。这是因为Run函数和Stop函数的执行不在同一个线程,在多线程环境下,对于 bRunning 变量的访问存在并发访问冲突的风险。因为 Run 函数中的循环依赖于 bRunning 的值来判断是否继续执行,而 Stop 函数会修改 bRunning 的值,如果多个线程同时访问或者修改这个变量(比如一个线程尝试通过 Stop 函数停止线程,而另一个线程正在 Run 函数中读取 bRunning 的值来判断循环条件),就可能导致线程的行为出现异常,比如线程无法正常停止或者出现意外的提前停止等情况。

为了解决线程竞争问题,这里提供两种解决方法。

(1)解决线程竞争方式1:加锁

这里我们通过加“临界区”解决这个问题。先定义一个临界区。

FScopeLock Lock(&CriticalSection)创建一个 FScopeLock 类型的对象 Lock,并将其与一个 FCriticalSection 类型的临界区对象 CriticalSection 相关联。

FScopeLock 类的设计遵循了 RAII 原则,在其构造函数中,会获取传入的 FCriticalSection 对象所代表的临界区锁,使得从构造函数执行完开始,对应的临界区就处于被锁定状态,其他线程若试图访问同一个临界区保护的代码或资源时,就会被阻塞,直到当前持有锁的线程释放该锁为止。当 FScopeLock 对象的生命周期结束(也就是离开其所在的代码块范围时,例如函数执行到对应的代码块末尾,或者遇到 return 语句等情况导致代码块结束),它的析构函数会自动被调用,在析构函数中,会释放之前在构造函数里获取的临界区锁,将临界区的访问权限开放给其他线程,使得其他线程有机会获取锁并访问受保护的资源。

此时可以看到无论我们开关多少次线程也不会存在安全风险

(2)解决线程竞争方式2:线程安全布尔类型FThreadSafeBool

引入所需库 

 

将 bRunning 变量的类型由普通的bool类型改为FThreadSafeBool类型

此时不再需要加锁

案例源代码

“SimpleRunnable.h”

// Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h"
#include "HAL/Runnable.h"
#include "HAL/ThreadManager.h"class STUDY_API FSimpleRunnable : public FRunnable
{
public:FSimpleRunnable(FString InThreadName) :SimpleRunnableThreadName(InThreadName) {};~FSimpleRunnable() {};virtual bool Init() override;virtual uint32 Run() override;virtual void Exit() override;virtual void Stop() override;FString GetThreadName();static void PrintWaring(FString InStr);protected:void PrintThreadInfo();FString SimpleRunnableThreadName = TEXT("None");bool bRunning = true;  //用于判断线程是否继续执行FCriticalSection CriticalSection;
};

 “SimpleRunnable.cpp”

// Fill out your copyright notice in the Description page of Project Settings.#include "Thread/SimpleRunnable.h"bool FSimpleRunnable::Init()
{PrintWaring(TEXT("Thread Init"));PrintThreadInfo();return true;
}uint32 FSimpleRunnable::Run()
{PrintWaring(TEXT("Thread Run"));PrintThreadInfo();while (bRunning){FPlatformProcess::Sleep(0.5);FScopeLock Lock(&CriticalSection);if (bRunning == false){break;}PrintWaring(TEXT("Thread Working"));}return uint32();
}void FSimpleRunnable::Exit()
{PrintWaring(TEXT("Thread Exit"));PrintThreadInfo();
}void FSimpleRunnable::Stop()
{FScopeLock Lock(&CriticalSection);bRunning = false;PrintWaring(TEXT("Thread Stop"));PrintThreadInfo();
}FString FSimpleRunnable::GetThreadName()
{return SimpleRunnableThreadName;
}void FSimpleRunnable::PrintWaring(FString InStr)
{AsyncTask(ENamedThreads::GameThread, [InStr]() {UE_LOG(LogTemp, Warning, TEXT("ThreadLog:[%s]"), *InStr);});
}void FSimpleRunnable::PrintThreadInfo()
{uint32 CurrentID = FPlatformTLS::GetCurrentThreadId();FString CurrentThread = FThreadManager::Get().GetThreadName(CurrentID);FString Info = FString::Printf(TEXT("--CurrentThreadID:[%d]--CurrentThreadName:[%s]"), CurrentID, *CurrentThread);PrintWaring(Info);
}

“ThreadSubsystem.h”

// Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h"
#include "SimpleRunnable.h"
#include "ThreadSubsystem.generated.h"UCLASS()
class STUDY_API UThreadSubsystem : public UGameInstanceSubsystem
{GENERATED_BODY()public:virtual bool ShouldCreateSubsystem(UObject* Outer) const override;virtual void Initialize(FSubsystemCollectionBase& Collection) override;virtual void Deinitialize() override;public:UFUNCTION(BlueprintCallable)void InitSimpleThread();UFUNCTION(BlueprintCallable)void ReleaseSimpleThread();protected:TSharedPtr<FSimpleRunnable> SimpleRunnable;FRunnableThread* SimpleThreadPtr = nullptr;};

“ThreadSubsystem.cpp”

// Fill out your copyright notice in the Description page of Project Settings.#include "Thread/ThreadSubsystem.h"bool UThreadSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{return true;
}void UThreadSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{Super::Initialize(Collection);//InitSimpleThread();
}void UThreadSubsystem::Deinitialize()
{//ReleaseSimpleThread();Super::Deinitialize();
}void UThreadSubsystem::InitSimpleThread()
{SimpleRunnable = MakeShared<FSimpleRunnable>(TEXT("Thread000"));SimpleThreadPtr = FRunnableThread::Create(SimpleRunnable.Get(), *SimpleRunnable->GetThreadName());
}void UThreadSubsystem::ReleaseSimpleThread()
{if (SimpleRunnable.IsValid()){SimpleRunnable->Stop();}if (SimpleThreadPtr){SimpleThreadPtr->WaitForCompletion();}
}

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

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

相关文章

《图解HTTP》 学习日记

1.了解WEB以及网络基础 1.1使用HTTP协议访问WEB web页面显示:根据web浏览器地址栏中输入指定的URL,web浏览器从web服务端获取文件资源(resource)等信息&#xff0c;从而显示出web页面 1.2网络基础TCP/IP 通常使用的网络(包括 互联网)是在tcp/ip协议族的基础上运作的&#xf…

【Docker】docker compose 安装 Redis Stack

注&#xff1a;整理不易&#xff0c;请不要吝啬你的赞和收藏。 前文 Redis Stack 什么是&#xff1f; 简单来说&#xff0c;Redis Stack 是增强版的 Redis &#xff0c;它在传统的 Redis 数据库基础上增加了一些高级功能和模块&#xff0c;以支持更多的使用场景和需求。Redis…

kubesphere前端源码运行

一、下载源码 源码是react&#xff0c;下载地址是 GitHub - kubesphere/console at v3.3.2 然后直接用git下拉就可以了 下拉完成后差不多是这样一个目录结构&#xff0c;记得切分支到3.3.2 二、下载依赖 1、node & yurn 想要运行源码首先需要node&#xff0c;使用刚才…

蓝桥杯历届真题 #分布式队列 (Java,C++)

文章目录 题目解读[蓝桥杯 2024 省 Java B] 分布式队列题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示 思路完整代码 题目解读 题目链接 [蓝桥杯 2024 省 Java B] 分布式队列 题目描述 小蓝最近学习了一种神奇的队列&#xff1a;分布式队列。简单来说&#x…

PySide6 Qt for Python Qt Quick参考网址

Qt QML BOOK&#xff1a; 《Qt for Python》 -Building an Application https://www.qt.io/product/qt6/qml-book/ch19-python-build-app#signals-and-slots Qt for Python&#xff1a;与C版本的差异即BUG处理&#xff08;常见的DLL文件确实的问题等&#xff09; Qt for Pyt…

如何稳定使用 O1 / O1 Pro,让“降智”现象不再困扰?

近期&#xff0c;不少朋友在使用 O1 或 O1 Pro 模型时&#xff0c;都会碰到“降智”或“忽高忽低”的智力波动&#xff0c;比如无法识图、无法生成图片、甚至回答准确度也不稳定。面对这些问题&#xff0c;你是不是也感到头疼呢&#xff1f; 为了找到更可靠的解决办法&#xf…

用户界面的UML建模11

然而&#xff0c;在用户界面方面&#xff0c;重要的是要了解《boundary》类是如何与这个异常分层结构进行关联的。 《exception》类的对象可以作为《control》类的对象。因此&#xff0c;《exception》类能够聚合《boundary》类。 参见图12&#xff0c;《exception》Database…

记录一次面试中被问到的问题 (HR面)

文章目录 一、你对公司的了解多少二、为什么对这个岗位感兴趣三、不能说的离职原因四、离职原因高情商回复五、你的核心优势是什么六、你认为你比其他面试候选人的优势是什么七、不要提及情感 一、你对公司的了解多少 准备要点&#xff1a; 在面试前&#xff0c;对公司进行充分…

前端 图片上鼠标画矩形框,标注文字,任意删除

效果&#xff1a; 页面描述&#xff1a; 对给定的几张图片&#xff0c;每张能用鼠标在图上画框&#xff0c;标注相关文字&#xff0c;框的颜色和文字内容能自定义改变&#xff0c;能删除任意画过的框。 实现思路&#xff1a; 1、对给定的这几张图片&#xff0c;用分页器绑定…

pandas系列----DataFrame简介

DataFrame是Pandas库中最常用的数据结构之一&#xff0c;它是一个类似于二维数组或表格的数据结构。DataFrame由多个列组成&#xff0c;每个列可以是不同的数据类型&#xff08;如整数、浮点数、字符串等&#xff09;。每列都有一个列标签&#xff08;column label&#xff09;…

安装完docker后,如何拉取ubuntu镜像并创建容器?

1. 先docker拉取ubuntu镜像 docker search ubuntu #搜索ubuntu 镜像 docker pull ubuntu:22.04 #拉取ubuntu 镜像 docker images #下载完成后&#xff0c;查看已经下载的镜像 docker run --name ubuntu_container -dit ubuntu:22.04 /bin/bash # docker container -l 2.…

Qt监控系统远程网络登录/请求设备列表/服务器查看实时流/回放视频/验证码请求

一、前言说明 这几个功能是近期定制的功能&#xff0c;也非常具有代表性&#xff0c;核心就是之前登录和设备信息都是在本地&#xff0c;存放在数据库中&#xff0c;数据库可以是本地或者远程的&#xff0c;现在需要改成通过网络API请求的方式&#xff0c;现在很多的服务器很强…

服务器攻击方式有哪几种?

随着互联网的快速发展&#xff0c;网络攻击事件频发&#xff0c;已泛滥成互联网行业的重病&#xff0c;受到了各个行业的关注与重视&#xff0c;因为它对网络安全乃至国家安全都形成了严重的威胁。面对复杂多样的网络攻击&#xff0c;想要有效防御就必须了解网络攻击的相关内容…

Transformer 中缩放点积注意力机制探讨:除以根号 dk 理由及其影响

Transformer 中缩放点积注意力机制的探讨 1. 引言 自2017年Transformer模型被提出以来&#xff0c;它迅速成为自然语言处理&#xff08;NLP&#xff09;领域的主流架构&#xff0c;并在各种任务中取得了卓越的表现。其核心组件之一是注意力机制&#xff0c;尤其是缩放点积注意…

Linux下部署SSM项目

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 Linux部署SSM项目 打包项目 1、修改pom.xml文件&#xff0c;打包方式改为war <packaging>war</packaging>2、idea 通过maven的clean&#xff0c;…

Bytebase 3.0.1 - 可配置在 SQL 编辑器执行 DDL/DML

&#x1f680; 新功能 新增环境策略&#xff0c;允许在 SQL 编辑器内直接执行 DDL/DML 语句。 支持为 BigQuery 数据脱敏。 在项目下新增数据访问控制及脱敏管理页面。 在数据库页面&#xff0c;支持回滚到变更历史的某个版本。 &#x1f514; 兼容性变更 禁止工单创建…

ansible 知识点【回顾梳理】

ansible 知识点 1. 剧本2. facts变量3. register变量4. include功能5. handlers6. when 条件7. with_items 循环8. Jinja2模板9. group_vars10. roles :star::star::star: 看起来字数很多&#xff0c;实际有很多是脚本执行结果&#xff0c;内容不多哦 1. 剧本 剧本很重要的就是…

LLM之RAG实战(五十一)| 使用python和Cypher解析PDF数据,并加载到Neo4j数据库

一、必备条件&#xff1a; python语言Neo4j数据库python库&#xff1a;neo4j、llmsherpa、glob、dotenv 二、代码&#xff1a; from llmsherpa.readers import LayoutPDFReaderfrom neo4j import GraphDatabaseimport uuidimport hashlibimport osimport globfrom datetime …

MLU上使用MagicMind GFPGANv1.4 onnx加速!

文章目录 前言一、平台环境准备二、环境准备1.GFPGAN代码处理2.MagicMind转换修改env.sh修改run.sh参数解析运行 3.修改后模型运行 前言 MagicMind是面向寒武纪MLU的推理加速引擎。MagicMind能将人工智能框架&#xff08;TensorFlow、PyTorch、Caffe与ONNX等&#xff09;训练好…

关于大数据的基础知识(一)——定义特征结构要素

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【14后&#x1f60a;///计算机爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于大数据的基础知识&#xff08;一&a…