RDMA高性能网络通信实践

RDMA高性能网络通信实践

    • 一、背景介绍
    • 二、方法设计
      • A.实现方案
      • B.关键技术点
    • 三、代码及注释
    • 四、注意事项

一、背景介绍

远程直接内存访问(RDMA)技术通过绕过操作系统内核和CPU直接访问远程内存,实现了超低延迟、高吞吐量的网络通信。该技术广泛应用于高性能计算、分布式存储和机器学习等领域。本文通过一个完整的代码示例,演示如何利用RDMA核心组件(QP、MR、CQ等)实现跨节点内存直接读写。

二、方法设计

A.实现方案

  1. 控制平面:使用TCP协议交换RDMA连接参数
  2. 数据平面:基于IB Verbs接口实现零拷贝传输
  3. 混合模式:客户端主动写入,服务端被动读取

B.关键技术点

  • 内存注册机制实现安全访问
  • QP状态机转换确保通信可靠性
  • 完成队列轮询实现异步通知
  • 端到端流控通过TCP协议实现

三、代码及注释

/*----------------------------- 头文件包含 -----------------------------*/
// 标准库和网络相关头文件
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <assert.h>
#include <errno.h>
#include <sys/time.h>
#include <byteswap.h>// RDMA相关头文件
#include <arpa/inet.h>
#include <infiniband/verbs.h>#ifdef USE_VACC
#include "vaccrt.h"
#include <vaccrt_mem_management.h>
#endif#ifdef USE_CUDA
#include <cuda.h>
#include <cuda_runtime.h>
#endif/**********************************************************************A.关键概念解释:1.保护域(PD):资源隔离单元,所有资源(QP、MR等)必须属于某个PD2.内存区域(MR):注册的内存区域,只有注册的内存才能用于RDMA操作3.队列对(QP):包含发送队列和接收队列,是通信的基本单元4.工作请求(WR):描述要执行的操作(发送/接收/RDMA读写)5.完成队列(CQ):用于通知操作完成状态6.QP状态转换:a.INIT:初始状态,设置基本参数b.RTR(Ready to Receive):准备好接收数据c.RTS(Ready to Send):准备好发送数据B.程序流程总结:1.通过TCP交换RDMA连接参数2.初始化IB资源(PD、CQ、MR、QP)3.交换QP信息(地址、密钥等)4.进行QP状态转换(INIT->RTR->RTS)5.执行RDMA写/读操作6.轮询完成队列确认操作完成7.清理资源C.使用说明:1.编译命令: gcc -o cuda -DUSE_CUDA -ggdb main.c -pthread -libverbs \-I /usr/local/cuda/include \-L /usr/local/cuda/lib64 -Wl,-rpath=/usr/local/cuda/lib64 \-lcudart -lcudadevrt -lcuda  2.服务端  : ./cuda 192.168.1.100 mlx5_03.客户端  : ./cuda 192.168.1.101 mlx5_0 192.168.1.100 ***********************************************************************//*----------------------------- 全局配置 -----------------------------*/
#define MAX_POLL_CQ_TIMEOUT 6000    // CQ轮询超时时间(毫秒)
#define MSG "Hello,World"           // 要传输的测试消息
#define MSG_SIZE (64<<10)// 配置参数结构体
struct config {const char *dev;      // IB设备名称char *local_addr;     // 本地IP地址u_int32_t port;       // TCP端口号int ib_port;          // IB端口号(默认1)int gid_idx;          // GID索引(-1表示不使用RoCEv2)
} config = { NULL, NULL, 12025, 1, -1 };/*----------------------------- 资源结构体 -----------------------------*/
// 包含所有RDMA相关资源
struct resources {struct ibv_context *ctx;        // IB上下文struct ibv_pd *pd;              // 保护域(Protection Domain)struct ibv_cq *cq;              // 完成队列(Completion Queue)struct ibv_qp *qp;              // 队列对(Queue Pair)struct ibv_mr *mr;              // 内存区域(Memory Region)void *buf;                      // 数据缓冲区指针int sock;                       // TCP套接字uint64_t remote_addr;           // 远程内存地址uint32_t remote_rkey;           // 远程内存访问密钥struct ibv_port_attr port_attr; // IB端口属性
};/*----------------------------- 辅助函数 -----------------------------*/
/*** 建立TCP连接(客户端)或监听(服务端)* @param server 本地地址(服务端模式时使用)* @param port TCP端口号* @param remote_addr 远程地址(客户端模式时使用)* @return 成功返回套接字fd,失败返回-1*/
int sock_connect(const char *server, int port,const char *remote_addr) {struct addrinfo hints = { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM };struct addrinfo *res, *p;int sock = -1;char port_str[6];const char * p_addr=server;if(remote_addr) p_addr=remote_addr;sprintf(port_str, "%d", port);if (getaddrinfo(p_addr, port_str, &hints, &res)) return -1;for (p = res; p; p = p->ai_next) {sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol);int reuse = 1;if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse))) return -1;        if (sock < 0) continue;if (remote_addr) {if (connect(sock, p->ai_addr, p->ai_addrlen)) { close(sock); sock = -1; }else break;} else {if (bind(sock, p->ai_addr, p->ai_addrlen) || listen(sock, 1)) { close(sock); sock = -1; }else { sock = accept(sock, NULL, 0); break; }}}freeaddrinfo(res);return sock;
}/*** 通过TCP同步交换数据* @return 成功返回0,失败返回-1*/
int sock_sync(int sock, int size, void *local, void *remote) {if (write(sock, local, size) != size) return -1;return read(sock, remote, size) == size ? 0 : -1;
}/*** 轮询完成队列直到操作完成或超时* @return 成功返回0,失败返回-1*/
int poll_cq(struct ibv_cq *cq) {struct ibv_wc wc;unsigned long start = time(NULL) * 1000;while (time(NULL) * 1000 - start < MAX_POLL_CQ_TIMEOUT) {if (ibv_poll_cq(cq, 1, &wc) && wc.status == IBV_WC_SUCCESS) return 0;}return -1;
}/*** 提交发送工作请求(Work Request)* @param res 资源结构体指针* @param op 操作类型(IBV_WR_RDMA_WRITE/READ)*/
void post_send(struct resources *res, int op) {struct ibv_send_wr sr;struct ibv_sge sge;struct ibv_send_wr *bad_wr = NULL;memset(&sge, 0, sizeof(sge));sge.addr = (uintptr_t)res->buf;sge.length = res->mr->length;sge.lkey = res->mr->lkey;memset(&sr, 0, sizeof(sr));sr.next = NULL;sr.wr_id = 0;sr.sg_list = &sge;sr.num_sge = 1;sr.opcode = op;sr.send_flags = IBV_SEND_SIGNALED;sr.wr.rdma.remote_addr = res->remote_addr;sr.wr.rdma.rkey = res->remote_rkey;assert(0==ibv_post_send(res->qp, &sr, &bad_wr));
}/*----------------------------- 主函数 -----------------------------*/
int main(int argc, char **argv) {struct resources res = {0};char *remote_addr=0;//分配HOST或设备内存#ifdef USE_CPUres.buf = malloc(MSG_SIZE);memset(res.buf,0,MSG_SIZE);#endif    #ifdef USE_CUDAcuInit(0);CUdevice cuDevice;cuDeviceGet(&cuDevice, 0);CUcontext cuContext;assert(0 == cuCtxCreate(&cuContext, 0, cuDevice));assert(0 == cudaMallocHost(&res.buf, MSG_SIZE));struct cudaPointerAttributes cu_attrs;assert(0==cudaPointerGetAttributes(&cu_attrs,res.buf));unsigned cuflags = 1;assert(0==cuPointerSetAttribute(&cuflags, CU_POINTER_ATTRIBUTE_SYNC_MEMOPS,(CUdeviceptr)cu_attrs.devicePointer));assert(res.buf);cudaMemset(res.buf,0,MSG_SIZE);#endif    #ifdef USE_VACCint DevId=0;assert(0==vaccrtSetDevice(DevId));   assert(0==vaccrtMalloc(64<<10,MSG_SIZE,&res.buf));assert(0==vaccrtMemset(res.buf,0,MSG_SIZE)); int mem_handle=0;assert(0==vaccrtMemGetHandleForAddressRange(&mem_handle,res.buf, MSG_SIZE,VACCRT_MEM_RANGE_HANDLE_TYPE_DMA_BUF_FD,0));printf("mem_handle:%d ddr:%p\n",mem_handle,res.buf);#endifprintf("init done\n");// 参数解析:本地地址必须,设备名必须,远程地址可选config.local_addr = argv[1];    // 本地IP地址char *dev_name = argv[2];       // IB设备名(如mlx5_0)if(argc > 3) remote_addr = argv[3];  // 远程IP地址(客户端模式)/*=============== 阶段1:建立TCP连接 ===============*/// 用于交换RDMA连接参数(QPN、LID、内存地址等)    res.sock = sock_connect(config.local_addr, config.port,remote_addr);assert(res.sock>=0);/*=============== 阶段2:初始化IB资源 ===============*/// 1. 获取IB设备列表并打开指定设备    int num_devices=0;struct ibv_device * cur_dev=NULL;struct ibv_device **dev_list = ibv_get_device_list(&num_devices);for (int i = 0; i < num_devices; i++) {const char *name = ibv_get_device_name(dev_list[i]);printf("%d:%s\n",i,name);if (dev_name && strcmp(name, dev_name) == 0) {res.ctx = ibv_open_device(dev_list[i]);  cur_dev=dev_list[i];break;}}    assert(res.ctx);// 2. 查询IB端口属性(获取LID等信息)assert(0==ibv_query_port(res.ctx,1, &res.port_attr));// 3. 创建保护域(PD),用于管理资源权限res.pd = ibv_alloc_pd(res.ctx);assert(res.pd);// 4. 创建完成队列(CQ),用于接收操作完成通知res.cq = ibv_create_cq(res.ctx, 1, NULL, NULL, 0);assert(res.cq);// 5. 注册内存区域(MR),允许远程访问   int mr_flags=IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ;#ifdef USE_CPUres.mr = ibv_reg_mr(res.pd, res.buf, MSG_SIZE,mr_flags );#endif#ifdef USE_VACCres.mr = ibv_reg_dmabuf_mr(res.pd,0,MSG_SIZE,(uint64_t)res.buf,mem_handle,mr_flags);#endif #ifdef USE_CUDAres.mr = ibv_reg_mr(res.pd, res.buf, MSG_SIZE,mr_flags);   #endifif(0==res.mr){printf("ibv_reg_mr %s (错误码=%d)\n", strerror(errno), errno);return -1;}// 6. 创建队列对(QP),RC(可靠连接)类型struct ibv_qp_init_attr qp_init_attr;memset(&qp_init_attr, 0, sizeof(qp_init_attr));qp_init_attr.qp_type = IBV_QPT_RC;qp_init_attr.sq_sig_all = 1;qp_init_attr.send_cq = res.cq;qp_init_attr.recv_cq = res.cq;qp_init_attr.cap.max_send_wr = 1;qp_init_attr.cap.max_recv_wr = 1;qp_init_attr.cap.max_send_sge = 1;qp_init_attr.cap.max_recv_sge = 1;res.qp = ibv_create_qp(res.pd, &qp_init_attr);assert(res.qp);/*=============== 阶段3:交换QP参数 ===============*/// 通过TCP交换双方的QP信息(地址、密钥等)    union ibv_gid my_gid;assert(0==ibv_query_gid(res.ctx, 1,0,&my_gid));// 本地参数打包struct { uint64_t addr;    // 内存地址uint16_t lid;     // 本地标识符uint32_t rkey;    // 内存访问密钥uint32_t qpn;     // QP编号uint8_t gid[16];  // 全局标识符(RoCEv2需要)} local = {(uintptr_t)res.buf,res.port_attr.lid,res.mr->rkey,res.qp->qp_num},remote;memcpy(local.gid, &my_gid, 16);// 参数交换(TCP同步)assert(0 == sock_sync(res.sock, sizeof(local), &local, &remote));// 保存远程参数res.remote_addr = remote.addr;res.remote_rkey = remote.rkey;printf("local: addr=%p, lkey=0x%x, rkey=0x%x\n", res.mr->addr, res.mr->lkey, res.mr->rkey);printf("remote: addr=%ld, rkey=0x%x\n", remote.addr, remote.rkey);/*=============== 阶段4:QP状态转换 ===============*/// QP需要经历三个状态变化:INIT -> RTR(准备接收) -> RTS(准备发送)// 1. INIT状态设置struct ibv_qp_attr attr;memset(&attr, 0, sizeof(attr));attr.qp_state = IBV_QPS_INIT;attr.port_num = 1;attr.pkey_index = 0;attr.qp_access_flags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE;int flags = IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT |IBV_QP_ACCESS_FLAGS;assert(0==ibv_modify_qp(res.qp, &attr, flags));// 2. RTR状态设置(准备接收)memset(&attr, 0, sizeof(attr));attr.qp_state = IBV_QPS_RTR;attr.path_mtu = IBV_MTU_256;attr.dest_qp_num = remote.qpn;attr.rq_psn = 0;attr.max_dest_rd_atomic = 1;attr.min_rnr_timer = 0x12;attr.ah_attr.is_global = 1;attr.ah_attr.dlid = remote.lid;attr.ah_attr.sl = 0;attr.ah_attr.src_path_bits = 0;attr.ah_attr.port_num = 1;attr.ah_attr.is_global = 1;attr.ah_attr.port_num = 1;// 如果是RoCEv2需要设置GRHattr.ah_attr.grh.flow_label = 0;attr.ah_attr.grh.hop_limit = 1;attr.ah_attr.grh.sgid_index = 0;attr.ah_attr.grh.traffic_class = 0;memcpy(&attr.ah_attr.grh.dgid, remote.gid, 16);flags = IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN |IBV_QP_RQ_PSN | IBV_QP_MAX_DEST_RD_ATOMIC |IBV_QP_MIN_RNR_TIMER;assert(0==ibv_modify_qp(res.qp, &attr,flags));// 3. RTS状态设置(准备发送)memset(&attr, 0, sizeof(attr));attr.qp_state = IBV_QPS_RTS;attr.timeout = 0x12;attr.retry_cnt = 6;attr.rnr_retry = 0;attr.sq_psn = 0;attr.max_rd_atomic = 1;flags = IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT |IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC;assert(0==ibv_modify_qp(res.qp, &attr,flags ));/*=============== 阶段5:数据传输 ===============*/if (remote_addr) {// 客户端模式,执行RDMA Write#ifdef USE_CPUstrcpy(res.buf, MSG);#endif#ifdef USE_CUDAassert(0==cudaMemcpy(res.buf, MSG, strlen(MSG), cudaMemcpyHostToDevice));#endif#ifdef USE_VACCassert(0==vaccrtMemcpy(MSG,strlen(MSG),res.buf,kHost2Ddr));  #endifpost_send(&res, IBV_WR_RDMA_WRITE);// 将本地数据写入远程内存} else {                               // 服务端模式,执行RDMA Readpost_send(&res, IBV_WR_RDMA_READ); // 从远程内存读取数据到本地}// 等待操作完成assert(0==poll_cq(res.cq));#ifdef USE_VACC{char buffer[MSG_SIZE];memset(buffer,0,MSG_SIZE);assert(0==vaccrtMemcpy(res.buf,strlen(MSG),buffer,kDdr2Host));  printf("Message: %s\n", buffer);}#endif#ifdef USE_CUDA{char buffer[MSG_SIZE];memset(buffer,0,MSG_SIZE);assert(0==cudaMemcpy(buffer,res.buf,strlen(MSG),cudaMemcpyDeviceToHost));  printf("Message: %s\n", buffer);}#endif#ifdef USE_CPUprintf("Message: %s\n", (const char*)res.buf);#endifprintf("end,Enter exit..\n");getchar(),getchar();/*=============== 阶段6:资源清理 ===============*/// 注意销毁顺序:QP -> MR -> CQ -> PD -> 上下文ibv_destroy_qp(res.qp);ibv_dereg_mr(res.mr);#ifdef USE_CUDAcudaFree(res.buf);#endif#ifdef USE_VACCvaccrtFree(res.buf);#endif#ifdef USE_CPUfree(res.buf);#endifibv_destroy_cq(res.cq);ibv_dealloc_pd(res.pd);ibv_close_device(res.ctx);close(res.sock);ibv_free_device_list(dev_list);return 0;
}

四、注意事项

  • 1.ibv_reg_dmabuf_mr需要传入之前分配的设备地址,否则会出
    mlx5_0/1: QP 574 error: local protection error (0x3b 0x0 0x9d)
    
  • 2.MAX_POLL_CQ_TIMEOUT值太小,会超时
  • 3.cudaMalloc的内存在ibv_reg_mr时提示地址非法
  • 4.收发完毕后才能释放资源

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

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

相关文章

ndarray数组掩码操作,True和False获取数据

#数组掩码的表示方法 def testht05():a np.arange(1,10)mask [True,False,True,True,False,True,False,True,True]print(a[mask]) 另外的用法&#xff1a; #掩码操作获取子集 def testht06():a np.arange(1,100)print(a[a%3 0 & (a%7 0)] )b np.array([A,"B&qu…

索引工具explain

EXPLAIN 是 MySQL 中一个非常有用的工具,用于分析查询的执行计划。通过 EXPLAIN,你可以了解 MySQL 是如何执行查询的,包括它如何使用索引、表的扫描方式等。这有助于优化查询性能。以下是 EXPLAIN 输出的各个字段的详细解释: 基本用法 EXPLAIN SELECT * FROM table_name …

Git回顾

参考视频:【GeekHour】一小时Git教程 一句话定义&#xff1a;Git是一个免费开源的分布式版本控制系统。 版本控制系统可以分为两种&#xff0c;1.集中式&#xff08;SVN&#xff0c;CVS&#xff09;&#xff1b;2.分布式&#xff08;git&#xff09; git的工作区域和文件状态…

python打卡day20

特征降维------特征组合&#xff08;以SVD为例&#xff09; 知识点回顾&#xff1a; 奇异值的应用&#xff1a; 特征降维&#xff1a;对高维数据减小计算量、可视化数据重构&#xff1a;比如重构信号、重构图像&#xff08;可以实现有损压缩&#xff0c;k 越小压缩率越高&#…

GuPPy-v1.2.0安装与使用-生信工具52

GuPPy&#xff1a;Python中用于光纤光度数据分析的免费开源工具 01 背景 Basecalling 是将原始测序信号转换为碱基序列的过程&#xff0c;通俗地说&#xff0c;就是“把碱基识别出来”。这一过程在不同代测序技术中各不相同&#xff1a; 一代测序是通过解析峰图实现&#xff1…

47. 全排列 II

题目 给定一个可包含重复数字的序列 nums &#xff0c;按任意顺序 返回所有不重复的全排列。 示例 1&#xff1a; 输入&#xff1a;nums [1,1,2] 输出&#xff1a; [[1,1,2],[1,2,1],[2,1,1]] 示例 2&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[1,2,3…

ERP系统操作流程,如何快速搭建流程体系

ERP流程图&#xff0c;如何搭建和建立&#xff0c;ERP系统操作流程&#xff0c;ERP系统操作流程图&#xff0c;采购流程&#xff0c;销售流程&#xff0c;仓库流程&#xff0c;MRP流程&#xff0c;PMC流程&#xff0c;财务流程&#xff0c;应收流程&#xff0c;应付流程&#x…

class path resource [] cannot be resolved to absolute file path

问题情景 java应用程序在IDE运行正常&#xff0c;打成jar包后执行却发生异常&#xff1a; java.io.FileNotFoundException: class path resource [cert/sync_signer_pri_test.key] cannot be resolved to absolute file path because it does not reside in the file system:…

19、HashTable(哈希)、位图的实现和布隆过滤器的介绍

一、了解哈希【散列表】 1、哈希的结构 在STL中&#xff0c;HashTable是一个重要的底层数据结构, 无序关联容器包括unordered_set, unordered_map内部都是基于哈希表实现 哈希表又称散列表&#xff0c;一种以「key-value」形式存储数据的数据结构。哈希函数&#xff1a;负责将…

基于 Flask的深度学习模型部署服务端详解

基于 Flask 的深度学习模型部署服务端详解 在深度学习领域&#xff0c;训练出一个高精度的模型只是第一步&#xff0c;将其部署到生产环境中&#xff0c;为实际业务提供服务才是最终目标。本文将详细解析一个基于 Flask 和 PyTorch 的深度学习模型部署服务端代码&#xff0c;帮…

Vue3 + Node.js 实现客服实时聊天系统(WebSocket + Socket.IO 详解)

Node.js 实现客服实时聊天系统&#xff08;WebSocket Socket.IO 详解&#xff09; 一、为什么选择 WebSocket&#xff1f; 想象一下淘宝客服的聊天窗口&#xff1a;你发消息&#xff0c;客服立刻就能看到并回复。这种即时通讯效果是如何实现的呢&#xff1f;我们使用 Vue3 作…

MySQL数据库与表结构操作指南

前言&#xff1a;本文系统梳理MySQL核心操作语句。内容覆盖建库建表、结构调整、数据迁移全流程&#xff08;包含创建/修改/删除/备份场景&#xff09;。希望它们能帮你快速解决问题。 库结构操作 一、库的创建 一个库的简单创建&#xff1a; create database 库名; 注意&am…

【WEB3】区块链、隐私计算、AI和Web3.0——数据民主化(1)

区块链、隐私计算、AI&#xff0c;是未来Web3.0至关重要的三项技术。 1.数据民主化问题 数据在整个生命周期&#xff08;生产、传输、处理、存储&#xff09;内的隐私安全&#xff0c;则是Web3.0在初始阶段首要解决的问题。 数据民主化旨在打破数据垄断&#xff0c;让个体能…

C语言—指针2

1. const 修饰变量 1.1 const修饰变量 变量被const修饰时&#xff0c;变量此时为常变量&#xff0c;本质为常量&#xff0c;语法上不可被修改&#xff0c;但是如果此时需要修改变量值&#xff0c;可以通过指针的方式修改。 虽然此时通过指针的方式确实修改了变量的值&#xff…

高级架构软考之网络OSI网络模型

高级架构软考之网络&#xff1a; 1.OSI网络模型&#xff1a; a.物理层&#xff1a; a.物理传输介质物理连接&#xff0c;负责数据传输&#xff0c;并监控数据 b.传输单位&#xff1a;bit c.协议&#xff1a; d:对应设备&#xff1a;中继器、集线器 b.数据链路层&#xff1a; a.…

el-table计算表头列宽,不换行显示

1、在utils.js中封装renderHeader方法 2、在el-table-column中引入&#xff1a; 3、页面展示&#xff1a;

MySQL OCP和Oracle OCP怎么选?

近期oracle 为庆祝 MySQL 数据库发布 30 周年&#xff0c;Oracle 官方推出限时福利&#xff1a;2025 年 4 月 20 日至 7 月 31 日期间&#xff0c;所有人均可免费报考 MySQL OCP&#xff08;Oracle Certified Professional&#xff09;认证考试&#xff08;具体可查看MySQL OCP…

2025最新免费视频号下载工具!支持Win/Mac,一键解析原画质+封面

软件介绍 适用于Windows 2025 最新5月蝴蝶视频号下载工具&#xff0c;免费使用&#xff0c;无广告且免费&#xff0c;支持对原视频和封面进行解析下载&#xff0c;亲测可用&#xff0c;现在很多工具都失效了&#xff0c;难得的几款下载视频号工具&#xff0c;大家且用且珍…

Python学习之路(八)-多线程和多进程浅析

在 Python 中,多线程(Multithreading) 和 多进程(Multiprocessing) 是实现并发编程的两种主要方式。它们各有优劣,适用于不同的场景。 一、基本概念 特性多线程(threading)多进程(multiprocessing)并发模型线程共享内存空间每个进程拥有独立内存空间GIL(全局解释器锁…

Spark缓存--persist方法

1. 功能本质 persist&#xff1a;这是一个通用的持久化方法&#xff0c;能够指定多种不同的存储级别。存储级别决定了数据的存储位置&#xff08;如内存、磁盘&#xff09;以及存储形式&#xff08;如是否序列化&#xff09;。 2. 存储级别指定 persist&#xff1a;可以通过传入…