并行计算(MPI + OpenMP)

news2024/11/18 8:18:18

文章目录

  • 并行计算
  • MPI(进程级并行)
    • 基本结构
    • 数据类型
    • 点对点通信
      • 阻塞
      • 非阻塞
      • 非连续数据打包
    • 聚合通信
    • Communicator & Cartisen Grid
  • OpenMP(线程级并行)
    • 简介
    • 基本制导语句
    • worksharing construct
      • Sections
      • Single
      • For
    • 临界区 & 原子操作
    • Task

并行计算

并行类型:

  • 进程级并行:网络连接,内存不共享
  • 线程级并行:共享内存,同构 vs 异构
  • 线程内并行:指令级并行(流水线、多发射),向量化(SIMD,AVX)

基本方法:

  1. 分解。数据划分、任务定义
  2. 协调。通信、同步、任务调度

基本原则:

  1. 平衡。处理器之间负载平衡、众核之间负载平衡
  2. 压榨。尽量使得各个部件同时运行

任务划分,

  • 按数据划分:按输入数据(列主序)、按输出数据(行主序)、按输入输出数据(棋盘式)、按中间数据(矩阵乘,列行外积)
  • 递归划分:归并排序、快速排序
  • 探索式划分:对状态空间搜索,每个线程一种搜索策略,并不知道下一步会有多少任务
  • 猜测式划分:并行处理不同时间的事件,如果时间来到某个事件改变了状态,那么就回滚
  • 混合划分

并行模式,

  • 数据并行模式:矩阵计算、数据处理等
  • 任务图模式:使用任务依赖图
  • 主从模式:主线程产生任务,分配给各个工作线程
  • 流水线 / 生产者-消费者模式:每个处理部件完成一个阶段的任务,多个部件同时处理不同数据的不同阶段(GPU stream)
  • 混合模式

MPI(进程级并行)

Message Passing Interface(MPI):一种基于信息传递的并行编程技术,定义了一组具有可移植性的编程接口标准(并非一种语言或者接口)。支持点对点通信和广播。MPI 的目标是高性能、大规模性、可移植性,在今天仍为高性能计算的主要模型。OpenMPI 函数库,微软 MPI 文档

基本结构

程序结构(C语言版)

MPI_Init(&argc, &argv); //初始化MPI
MPI_Comm_size(MPI_COMM_WORLD, &nprocs); //设置进程数
MPI_Comm_rank(MPI_COMM_WORLD, &myrank); //获取进程ID
... ...
MPI_Finalize(); //结束MPI

编译:

mpicc -o test ./test.c
mpicxx -o hello++ hello++.cxx
mpif90 -o pi3f90 pi3f90.f90

运行:

mpirun –np 4 –host HOST1,HOST2,HOST3,HOST4	
mpirun -np 4 -hostfile hosts ./test 

数据类型

MPI 数据类型C语言数据类型
MPI_INTint
MPI_FLOATfloat
MPI_DOUBLEdouble
MPI_SHORTshort
MPI_LONGlong
MPI_CHARchar
MPI_UNSIGNED_CHARunsigned char
MPI_UNSIGNED_SHORTunsigned short
MPI_UNSIGNEDunsigned
MPI_UNSIGNED_LONGunsigned long
MPI_LONG_DOUBLElong double
MPI_BYTEunsigned char
MPI_PACKED

点对点通信

阻塞

阻塞式消息发送

int MPI_Send(const void *buf, int count, MPI_Datatype datatype, 
             int dest, int tag, MPI_Comm comm);
  • buf:发送缓冲区的首地址
  • count:需要发送的数据项个数
  • datatype:每个被发送元素的数据类型
  • dest:目标进程的进程号(rank)
  • tag:消息标识(接收端要使用同样的标号,否则无法传递)
  • comm:通信域(Communicator,指出哪些进程参与通信)
  • 返回值:函数成功时返回 MPI_SUCCESS,否则返回错误代码

阻塞式消息接收

int MPI_Recv(void *buf, int count, MPI_Datatype datatype, 
             int source, int tag, MPI_Comm comm, MPI_Status *status);
  • buf:接收缓冲区的首地址
  • count:接收缓冲区最多存放多少个数据项
  • datatype:每个被接收元素的数据类型
  • source:发送进程的进程号(若设为 MPI_ANY_SOURCE,则可以传递任意进程的消息)
  • tag:消息标识(若设为 MPI_ANY_TAG,则可以传递任意标号的消息)
  • comm:通信域
  • status:函数返回时,存放发送方的进程号、消息 tag 等
    • status->MPI_source
    • status->MPI_tag
    • status->MPI_error
  • 返回值:函数成功时返回 MPI_SUCCESS,否则返回错误代码

同时发送与接收

如果两个进程先执行 MPI_Send 后执行 MPI_Recv,那么有可能出现死锁

int MPI_Sendrecv(const void *sendbuf, int sendcount, MPI_Datatype sendtype, int dest, int sendtag, 
                 void *recvbuf, int recvcount, MPI_Datatype recvtype, int source, int recvtag, 
                 MPI_Comm comm, MPI_Status *status);
int MPIAPI MPI_Sendrecv_replace(void *buf, int count, MPI_Datatype datatype, 
                                int dest, int sendtag, int source, int recvtag, 
                                MPI_Comm comm, MPI_Status *status);

前者分别设置发送缓冲区、接收缓冲区,后者的发送和接收缓冲区是同一个。

非阻塞

非阻塞消息发送

int MPI_Isend(void *buf, int count, MPI_Datatype datatype, 
              int dest, int tag, MPI_Comm comm, MPI_Request *request);
  • 大部分参数与 MPI_Send 一样
  • request:未完成的 MPI (发送)请求的句柄

非阻塞消息接收

int MPI_Irecv(void *buf, int count, MPI_Datatype datatype, 
              int source, int tag, MPI_Comm comm, MPI_Request *request);
  • 大部分参数与 MPI_Recv 一样
  • request:未完成的 MPI (接收)请求的句柄

等待完成(阻塞)

int MPI_Wait(MPI_Request *request, MPI_Status *status);
  • request:未完成的 MPI (发送 / 接收)请求的句柄
  • status:函数返回时,存放发送方的进程号、消息 tag 等

检验完成(非阻塞)

int MPI_Test(MPI_Request *request, int *flag, MPI_Status *status);
  • request:未完成的 MPI (发送 / 接收)请求的句柄
  • flag:函数返回时, 非零值指示请求已完成
  • status:函数返回时,存放发送方的进程号、消息 tag 等

非连续数据打包

数据打包

int MPI_Pack(void *inbuf, int incount, MPI_Datatype datatype, 
             void *outbuf, int outsize, int *position, MPI_Comm comm);
  • inbuf:输入缓冲区
  • incount:输入数据项个数
  • datatype:输入数据项的类型
  • outbuf:输出缓冲区
  • outsize:输出缓冲区字节长度
  • position:缓冲区当前字节位置;函数返回时,存放缓冲区新的位置
  • comm:通信域

数据解包

int MPI_Unpack(void *inbuf, int insize, int *position, 
               void *outbuf, int outcount, MPI_Datatype datatype, MPI_Comm comm);
  • inbuf:输入缓冲区
  • insize:输入缓冲区字节长度
  • position:缓冲区当前字节位置;函数返回时,存放缓冲区新的位置
  • outbuf:输出缓冲区
  • outcount:输出数据项个数
  • datatype:输出数据项的类型
  • comm:通信域

聚合通信

为了更高效地完成多个进程间的消息传递,可以使用组通信。

基本组播

int MPI_Bcast(void *buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm);

在这里插入图片描述

分散

int MPI_Scatter(void *sendbuf, int sendcnt, MPI_Datatype sendtype, 
                void *recvbuf, int recvcnt, MPI_Datatype recvtype, int root, MPI_Comm comm);

在这里插入图片描述

归约

int MPI_Reduce(void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, 
               MPI_Op op, int root, MPI_Comm comm);

在这里插入图片描述

全部归约

int MPI_Allreduce (void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, 
                   MPI_Op op, MPI_Comm comm);

在这里插入图片描述

聚集

int MPI_Gather (void *sendbuf, int sendcnt, MPI_Datatype sendtype,
                void *recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm);

在这里插入图片描述

全部聚集

 int MPI_Allgather (void *sendbuf, int sendcount, MPI_Datatype sendtype,
                    void *recvbuf, int recvcount, MPI_Datatype recvtype, MPI_Comm comm);

在这里插入图片描述

全到全

 int MPI_Alltoall(void *sendbuf, int sendcount, MPI_Datatype sendtype,
                  void *recvbuf, int recvcnt, MPI_Datatype recvtype, MPI_Comm comm);

在这里插入图片描述

Communicator & Cartisen Grid

将若干结点自动排布成多维网格 dims

int MPI_Dims_create(int nnodes, int ndims, int dims[]);

创建一个笛卡尔拓扑结构的通信域 comm_cart

int MPI_Cart_create(MPI_Comm comm_old, int ndims, const int dims[], const int periods[], int reorder, MPI_Comm *comm_cart);

得到当前进程的笛卡尔坐标 coords

int MPI_Cart_coords(MPI_Comm comm, int rank, int maxdims, int coords[]);

得到当前笛卡尔空间通信域的信息 dims, periods, coords

int MPI_Cart_get(MPI_Comm comm, int maxdims, int dims[], int periods[], int coords[]);

得到某个笛卡尔坐标上进程的进程号 rank

int MPI_Cart_rank(MPI_Comm comm, const int coords[], int *rank);

将一个通信域按照颜色 color 分裂成多个通信域 newcomm,自己在新通信域中的进程号是 key

int MPI_Comm_split(MPI_Comm comm, int color, int key, MPI_Comm *newcomm);

用途:通用矩阵乘法(General Matrix Multiply, GEMM)的多进程并行。特殊的矩阵向量乘法可以:按列分割按行分割棋盘式分割

在这里插入图片描述

OpenMP(线程级并行)

OpenMP 是用于共享内存并行系统的多处理器程序设计的一套指导性编译处理方案。程序员通过在源代码中加入专用的 #pragma 来指明自己的意图,由此编译器可以自动将程序进行并行化,并在必要之处加入同步互斥以及通信。OpenMP 官网

简介

OpenMP 的运行模式为:Fork and Join

在这里插入图片描述

支持 C/C++,其编译指令为

gcc -fopenmp somepcode.c -o somepcode_openmp

OpenMP基本制导语句:

  • Omp sections
  • Omp parallel
  • Omp parallel for
  • Omp critical
  • ……

运行时函数:

  • omp_get_thread_num
  • omp_set_thread_num
  • omp_get_num_threads

环境变量:

  • OMP_NUM_THREADS

基本制导语句

#pragma omp parallel [clause[ [,] clause] ... ]
{
    //SPMD
}

clause 有如下的常用选择:

  • if([parallel :] scalar-expression):表达式为真,才会作用于此段代码;if 作用于 parallel 语句
  • num_threads(integer-expression)指定线程数
  • default(shared | none):缺省情况下数据都是 shared,设为 none 表示必须指定数据是否 shared
  • private(list)私有数据列表
  • firstprivate(list):私有数据继承前面的值
  • shared(list)共享数据列表
  • copyin(list)
  • reduction([reduction-modifier ,] reduction-identifier : list):对此数据做规约操作
  • proc_bind(master | close | spread):与 CPU 核的绑定方式
  • allocate([allocator :] list):选择 allocator

worksharing construct

共享工作结构:被一组 threads 恰好执行一次。For C/C++, worksharing constructs are for, sections, and single.

Sections

#pragma omp sections
{
    #pragma omp section
    //one calculation
    ...
    #pragma omp section
    //another calculation
}

Single

#pragma omp single
{
    ...
}

For

#pragma omp for [clause[ [,] clause] ... ]
for-loops{
    ...
}

clause 有如下的常用选择:

  • private(list)私有变量列表
  • firstprivate(list):私有数据继承前面的值
  • lastprivate([lastprivate-modifier:] list):私有数据,最后一次迭代结果写回原变量
  • linear(list[: linear-step])
  • reduction([reduction-modifier,]reduction-identifier : list):归约
  • schedule([modifier [, modifier]:]kind[, chunk_size]):调度方式
  • collapse(n)合并 n 层循环后,再分配任务给各线程(注意,合并前后的 parivate(list) 不一定相同)
  • ordered[(n)]
  • nowait循环末尾不用等待其他线程(否则,默认在每轮循环末尾加 barrier 同步各线程)
  • allocate([allocator :]list)
  • order(concurrent)

这条 #pragma omp for 标记将会在编译时,派生N个线程,每个线程有自己的上下文(私有数据、共享数据)

  1. 循环变量是私有数据
  2. 其他数据缺省均为共享(必要时手动设置 private(list)

一般地,可以将 parallel 与 for 合用

//分开写
void simple(int n, float *a, float *b) {
    int i; 
    #pragma omp parallel num_threads(5)
    {
        #pragma omp for nowait
        for (i = 1; i < n; i++) /* i is private by default */
            b[i] = (a[i] + a[i – 1]) / 2.0;
    }
}

//合用
void simple(int n, float *a, float *b) {
    int i; 
    #pragma omp parallel for num_threads(5) nowait private(i)
    for (i = 1; i < n; i++) /* i is private by default */
        b[i] = (a[i] + a[i – 1]) / 2.0;
}

schedule() 可以有 static, dynamic, guided 等多种调度方式,

在这里插入图片描述

临界区 & 原子操作

Critical:临界区内的操作是原子的,无竞争

int i,j;
int b[N][M];
int x = 0;
#pragma omp parallel for private(j)
{
    for(i = 0; i < N; i++){  
        int m = i * i;
        for(j = 0; j < M, j++) 
            b[i][j] = m*j;
        #pragma omp critical 
        {
            x += b[i][j]; //临界区内,至多只有一个线程
        }
    }
}

Reduce:归约(类似 MPI),性能比 Critical 好

int i,j;
int b[N][M];
int x = 0;
#pragma omp parallel for private(j) reduction(+: x)
{
    for(i = 0; i < N; i++){ 
        int m = i * i;
        for(j = 0; j < M, j++) 
            x += m*j;
    }
}

Atomic:原子操作

#pragma omp atomic [clause[[[,] clause] ... ] [,]] atomic-clause [[,] clause [[[,] clause] ... ]]
expression-stmt

Atomic-clause 可以是 read, write, update, capture

Atomic 仅作用于制导语句下面的语句。与临界区相比,不要求仅有一个线程进入,只需保证所修饰语句操作的原子性。例如:

int x[n];
int i;
#pragma omp parallel for shared(x, y, index, n)
for (i=0; i<n; i++) {
    #pragma omp atomic update
    x[index[i]] += work1(i); //多个threads可以同时访问数组x[n]
    y[i] += work2(i);
}

Task

使用 task 制导语句,定义任务,实现更加灵活的并行方式

  • task 内可以再派生 task
  • 可以自定义 task 的调度方式、优先级、任务间依赖关系
  • Included task / undeterred task:串行化的任务
  • 可以对任务加更多限制:mergeable,untied,final
int fib(int n){
    int i, j;
    if (n<2)
        return n;
    else{
        #pragma omp task shared(i)
        i=fib(n-1);
        #pragma omp task shared(j)
        j=fib(n-2);
        #pragma omp taskwait
        return i+j;
    }
}

int main(){
    #pragma omp parallel 
    #pragma omp master
    fib(n); //只能由一个线程生成task,任务分给各个线程执行
}

Taskgroup:产生一组任务,在结构结尾加上 barrier 同步所有任务

#pragma omp taskgroup [clause[[,] clause] ...]
structured-block

clause 有如下的常用选择:

  • task_reduction(reduction-identifier : list):任务归约
  • allocate([allocator :] list)
//递归生成task
void compute_tree(tree_type tree)
{
    if (tree->left){
        #pragma omp task
        compute_tree(tree->left);
    }
    if (tree->right){
        #pragma omp task
        compute_tree(tree->right);
    }
    #pragma omp task
    compute_something(tree);
}

int main()
{
    int i;
    tree_type tree;
    init_tree(tree);
    #pragma omp parallel
    #pragma omp single
    {
        #pragma omp task
        start_background_work();
        for (i = 0; i < max_steps; i++) //由一个线程产生task
        {
            #pragma omp taskgroup
            {
                #pragma omp task
                compute_tree(tree);
            } //wait on tree traversal in this step
            check_step();
        }
    } //only now is background work required to be complete
    print_results();
    return;
}

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

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

相关文章

React 学习笔记总结(三)

文章目录1. React( v16.8 版本) 生命周期2. React( v16.8 版本) 生命周期 更新流程2.1 三个更新流程2.2 setState()的生命周期流程(对应上图2号线)2.3 forceUpdate()的生命周期流程(对应上图3号线)2.4 父组件render()渲染的生命周期流程(对应上图1号线)2.5 React( v16.8 版本)生…

“学了一个我不感兴趣的专业,要不要转行IT?”

“这个专业太无聊了&#xff0c;我想转行……” “你想转去干什么&#xff1f;” “我对金融感兴趣&#xff0c;听说金融很赚钱&#xff0c;我想学金融……” “你感兴趣为什么课后不去钻研&#xff0c;而要选择打游戏&#xff1f;” “以后再慢慢来嘛……” “可是你已经…

报表工具-FineReport JS实现参数面板显示对应数据

1. 概述 1.1 版本 报表服务器版本 功能变更 11.0 -- 1.2 预期效果 1.3 实现思路 对照填报界面的章节&#xff0c;在参数界面也用 SQL 语句实现对应数据的展示 &#xff0c;当第一次打开模板时&#xff0c;标签不显示可以通过JS 控制实现。 2. 示例 2.1 新建模板 新建普通…

设计模式-牛刀小试01

前言 本文为datawhale2022年12月组队学习《大话设计模式》task4打卡学习&#xff0c;本次完成homework1。 【教程地址】https://github.com/datawhalechina/sweetalk-design-pattern 一、任务描述 1.1 背景 小李已经是一个工作一年的初级工程师了&#xff0c;他所在的公司是…

靶机练习——vulnstack1

下载地址&#xff1a;http://vulnstack.qiyuanxuetang.net/vuln/detail/2/ 注意事项 密码一定要设置为不同的 部署环境 根据红日安全出具的wp&#xff0c;我们只需要设置两个C段即可&#xff0c;分别是外网的72和内网的52&#xff0c;这里直接添加新的网络以及子网地址即可&…

十三、DockerFile构建增强版本centos7

1、概述 在Docker 常用命令篇中&#xff0c;我们已经知道了2中构建镜像的方式 export\import 和 commit方式。这两种方式都需要先运行并创建容器&#xff0c;然后在容器中安装vim、ifconfig等命令&#xff0c;然后再重新构建加强版的镜像&#xff0c;比较麻烦。Dockerfile通过…

2022CTF培训(九)MIPS PWN环境搭建MIPS PWN入门

附件下载链接 环境搭建 在 ARM PWN 环境搭建 的基础上&#xff0c;首先安装具备MIPS交叉编译gcc与MIPS程序动态链接库&#xff1a; sudo apt-get install gcc-mips-linux-gnu sudo apt-get install gcc-mipsel-linux-gnu sudo apt-get install gcc-mips64-linux-gnuabi64 su…

【CANN训练营第三季】Ascend平台体验Pytorch笔记

模型迁移 手册地址&#xff1a;https://www.hiascend.com/document/detail/zh/CANNCommunityEdition/600alpha002/ptmoddevg/ptmigr/ptmigr_000009.html 主要修改&#xff1a; 导入相关库 import torch import torch_npu #1.8.1及以上需要指定NPU设备 通过device()指定 …

JDBC编程步骤、JDBC API详解和数据库连接池

前言&#xff1a; JDBC 就是使用Java语言操作关系型数据库的一套API &#xff0c;全称&#xff1a;( Java DataBase Connectivity ) Java 数据库连接。官方&#xff08;sun公司&#xff09;定义的一套操作所有关系型数据库的规则&#xff0c;即 接口各个数据库厂商去实现这套…

工具及方法 - Process Explorer以及类似工具,用来获取系统运行的进程信息

下载Process explorer&#xff1a; Process Explorer - Sysinternals | Microsoft Learn Process explorer简介 有没有想过哪个程序打开了一个特定的文件或目录&#xff1f;现在你可以找到了。Process Explorer向你显示关于进程打开或加载的句柄和DLL的信息。 Process Explore…

[Python图像处理] 使用 HSV 色彩空间检测病毒对象

使用 HSV 色彩空间检测病毒对象前言检测病毒对象相关链接前言 在本节中&#xff0c;我们将学习如何使用 OpenCV 在 HSV 色彩空间中使用特定颜色检测感兴趣对象。我们需要通过指定颜色值范围识别和提取感兴趣的对象&#xff0c;使用具有病毒的血细胞图像&#xff0c;我们的目标…

Python 自动化测试(五): Pytest 结合 Allure 生成测试报告

本文节选自霍格沃玆测试学院测试开发内部教材&#xff0c;进阶学习文末加群&#xff01; 测试报告在项目中是至关重要的角色&#xff0c;一个好的测试报告&#xff1a; 可以体现测试人员的工作量&#xff1b; 开发人员可以从测试报告中了解缺陷的情况&#xff1b; 测试经理可…

7.Linux实用操作(2)

文章目录零、学习目标一、进程管理1、概念2、查看进程3、查看指定进程4、关闭进程二、主机状态1、查看系统资源占用2、top命令内容详解3、top命令选项4、top交互式选项5、磁盘信息监控三、上传、下载1、上传、下载2、rz、sz命令四、压缩、解压1、压缩格式3、tar命令4、tar 命令…

后端开发浅学react

博客笔记来自于学习 柴柴老师在b站分享的react学习视频&#xff0c;仅供学习参考&#xff0c;学习视频是来自于b站的&#xff1a;柴柴_前端教书匠&#xff0c;视频链接&#xff1a;React入门到实战(2022全网最新&#xff09;_哔哩哔哩_bilibili 和 react官网 开始 – React …

谁会嫌钱多啊,最适合学生党的Python兼职攻略以及接私活经验

大家好&#xff0c;我是小八 这次我想谈谈一个非常热门的话题&#xff0c;就是如何在学习python的同时去赚钱。在这篇文章中&#xff0c;你会学习到如何通过学习python来赚取副业收入。 相信大家都对钱感兴趣吧&#xff0c;如果你和马云爸爸对钱不敢兴趣的话&#xff0c;那这…

Lambda表达式从用到底层原理

文章目录前言一、lambda函数基本使用参数列表返回类型函数体捕获列表值捕获引用捕获隐式捕获混合方式捕获修改值捕获变量的值异常说明二、lambda表达式使用的注意事项避免默认捕获模式三、lambda表达式底层实现原理采用值捕获采用引用捕获前言 lambda式作为一种创建函数对象的…

Python tkinter -- 第18章 画布控件之多边形

18.2.19 create_polygon(coords, **options) 根据 coords 给定的坐标&#xff0c;在画布上绘制一个多边形。 &#xff08;1&#xff09;coords&#xff1a;给定多边形的坐标 &#xff08;2&#xff09;options&#xff1a;选项的具体含义&#xff1a; 选项含义activedash当鼠标…

JavaEE- JVM八股文(JVM垃圾回收机制GC)

JVM垃圾回收的目标&#xff1a;主要针对内存中的堆空间进行垃圾回收。 Java中&#xff0c;大量的内存都在堆中。 程序计数器&#xff1a;固定大小&#xff0c;不涉及释放 栈&#xff1a;函数执行完毕&#xff0c;对应栈的空间就自动释放了&#xff0c;不需要垃圾回收 方法区&…

07-Golang中标识符的命名规则

Golang中标识符的命名规则标识符概念标识符的命名规则保留关键字介绍预定义标识符介绍标识符命名注意事项标识符概念 1.Golang对各种变量、方法等命名时使用的字符序列称为标识符 2.凡是自己可以起名字的地方都叫标识符 标识符的命名规则 1.由26个英文字母大小写&#xff0…

华为云桌面之下的“冰山”:技术底座x繁荣生态加速模式进化

在新兴技术迭代升级持续加速的背景下&#xff0c;很多产品类别的内涵和外延都在不断演进——虽然名字没什么变化&#xff0c;但实际所指已有云泥之别。 “云桌面”即是如此。从早期的无盘工作站&#xff0c;到VDI、IDV和VOI等技术流派的群雄并起&#xff0c;云桌面的江湖总是“…