4. 初探MPI——集体通信

news2024/9/28 7:17:52

系列文章目录

  1. 初探MPI——MPI简介
  2. 初探MPI——(阻塞)点对点通信
  3. 初探MPI——(非阻塞)点对点通信
  4. 初探MPI——集体通信

文章目录

  • 系列文章目录
  • 前言
  • 一、集体通信以及同步点
  • 二、`MPI_Bcast` 广播
    • 2.1 使用`MPI_Send` 和 `MPI_Recv` 来做广播
    • 2.2 `MPI_Bcast` 和 `MPI_Send` 以及 `MPI_Recv` 的比较
    • 2.3 Blocking or non-blocking ?
  • 三、`MPI Scatter`, `Gather`, and `Allgather`
    • 3.1 `MPI_Scatter`介绍
    • 3.2 `MPI_Gather` 的介绍
    • 3.3 使用 `MPI_Scatter` 和 `MPI_Gather` 来计算平均数
    • 3.4 `MPI_Allgather`
  • 四、并行排名
    • 4.1 问题概述
    • 4.2 并行排名API定义
    • 4.3 解决并行排名问题
      • 4.3.1 对所有进程中的数字进行排序
      • 4.3.2 排序数字并维护所属
      • 4.3.3 整合
    • 4.4 最终结果
  • 五、`MPI Reduce` and `Allreduce`
    • 5.1 归约/归化(reduce)简介
    • 5.2 `MPI_Reduce`
    • 5.3 使用`MPI_Reduce`计算均值
    • 5.4 `MPI_Allreduce`
      • 5.4.1 使用 `MPI_Allreduce` 计算标准差
  • 总结
  • 参考


前言

点对点通信的方式只会涉及两个不同进程之间的通信。而集体通信指的是涉及 communicator 里面所有进程的一个方法。

接下来的内容将要讲述:

  • Broadcast : One process sends a message to every other process
  • Reduction : One process gets data from all the other processes and applies an operation on it (sum, minimum, maximum, etc.
  • Scatter : A single process partitions the data to send pieces to every other process 单个进程将数据分区然后将数据块发送到其他进程
  • Gather : A single process assembles the data from different process in a buffer 单个进程将来自不同进程的数据组装在缓冲区中

一、集体通信以及同步点

同步点:这意味着所有的进程在执行代码的时候必须首先都到达一个同步点才能继续执行后面的代码。

MPI 有一个特殊的函数来做同步进程的这个操作。

MPI_Barrier(MPI_Comm communicator)

在这里插入图片描述

注意:始终记得每一个你调用的集体通信方法都是同步的。也就是说,如果没法让所有进程都完成 MPI_Barrier,那么你也没法完成任何集体调用。如果你在没有确保所有进程都调用 MPI_Barrier的情况下调用了它,那么程序会空闲下来。

二、MPI_Bcast 广播

广播 (broadcast) 是标准的集体通信技术之一。一个广播发生的时候,一个进程会把同样一份数据传递给一个 communicator 里的所有其他进程。广播的主要用途之一是把用户输入传递给一个分布式程序,或者把一些配置参数传递给所有的进程。

在这里插入图片描述
广播可以使用 MPI_Bcast 来做到,函数声明是:

MPI_Bcast(
    void* data,
    int count,
    MPI_Datatype datatype,
    int root,
    MPI_Comm communicator)

尽管根节点和接收节点做不同的事情,它们都是调用同样的这个 MPI_Bcast 函数来实现广播。

  • 当根节点(在我们的例子是节点0)调用 MPI_Bcast 函数时, data 变量里的值会被发送到其他的节点上。
  • 当其他的节点调用 MPI_Bcast 时,data 变量会被赋值成从根节点接受到的数据。

2.1 使用MPI_SendMPI_Recv 来做广播

粗略看的话,似乎 MPI_Bcast 仅仅是在 MPI_SendMPI_Recv 基础上进行了一层包装。事实上,我们就可以自己来做这层封装。我们的函数叫做 my_bcast。它跟 MPI_Bcast 接受一样的参数,看起来像这样:

void my_bcast(void* data, int count, MPI_Datatype datatype, int root, 
			  MPI_Comm communicator){
	int rank;
	MPI_Comm_rank(communicator, &rank);
	int size;
	MPI_Comm_size(communicator, &size);
	if (rank == root){
		for (int i = 0; i < size; i++){
		if (i != rank){
			MPI_Send(data, count, datatype, i, 0, communicator);
			}
		}
	} else {
		MPI_Recv(data, count, datatype, root, 0, communicator, MPI_STATUS_IGNORE);
	}
}

这个函数的时间复杂度应该是O(n)的
在这里插入图片描述采用树算法的时间复杂度是O(logn)。

2.2 MPI_BcastMPI_Send 以及 MPI_Recv 的比较

在这里插入图片描述

2.3 Blocking or non-blocking ?

这里仅给出阻塞版本。but you just need to add the I to switch to non-blocking mode (eg MPI_Bcast will become MPI_Ibcast). non-blocking globals require the use of MPI_Wait and MPI_Test to be completed correctly.

三、MPI Scatter, Gather, and Allgather

两个额外的机制来补充集体通信的知识 - MPI_Scatter 以及 MPI_Gather。还会讲一个 MPI_Gather 的变体:MPI_Allgather

3.1 MPI_Scatter介绍

MPI_BcastMPI_Scatter 的主要区别很小但是很重要。

  • MPI_Bcast 给每个进程发送的是同样的数据,
  • MPI_Scatter 给每个进程发送的是一个数组的一部分数据。

在这里插入图片描述
尽管根进程(进程0)拥有整个数组的所有元素,MPI_Scatter 还是会把正确的属于进程0的元素放到这个进程的接收缓存中。

MPI_Scatter 函数的原型:

MPI_Scatter(
    void* send_data,
    int send_count,
    MPI_Datatype send_datatype,
    void* recv_data,
    int recv_count,
    MPI_Datatype recv_datatype,
    int root,
    MPI_Comm communicator)
  1. send_data是在根进程上的一个数据数组
  2. send_countsend_datatype分别描述了发送给每个进程的数据数量和数据类型:
    如果send_count 是1,send_datatypeMPI_INT的话,进程0会得到数据里的第一个整数,以此类推。如果send_count是2的话,进程0会得到前两个整数,进程1会得到第三个和第四个整数,以此类推。在实践中,一般来说send_count会等于数组的长度除以进程的数量。除不尽怎么办?会在后面讲这个问题 。
  3. 函数定义里面接收数据的参数跟发送的参数几乎相同。recv_data 参数是一个缓存,它里面存了recv_countrecv_datatype数据类型的元素。
  4. rootcommunicator 分别指定开始分发数组的根进程以及对应的communicator。

3.2 MPI_Gather 的介绍

MPI_GatherMPI_Scatter 是相反的。MPI_Gather 从好多进程里面收集数据到一个进程上面而不是从一个进程分发数据到多个进程。这个机制对很多平行算法很有用,比如并行的排序和搜索。
在这里插入图片描述MPI_Scatter类似,MPI_Gather从其他进程收集元素到根进程上面。元素是根据接收到的进程的秩排序的。MPI_Gather的函数原型跟MPI_Scatter长的一样。

MPI_Gather(
    void* send_data,
    int send_count,
    MPI_Datatype send_datatype,
    void* recv_data,
    int recv_count,
    MPI_Datatype recv_datatype,
    int root,
    MPI_Comm communicator)

MPI_Gather中,只有根进程需要一个有效的接收缓存。所有其他的调用进程可以传递NULLrecv_data

注意:别忘记recv_count参数是从每个进程接收到的数据数量,而不是所有进程的数据总量之和。这一点对MPI初学者来说经常容易搞错。

3.3 使用 MPI_ScatterMPI_Gather 来计算平均数

这段代码展示了如何使用MPI来把工作拆分到不同的进程上,每个进程对一部分数据进行计算,然后再把每个部分计算出来的结果汇集成最终的答案。程序步骤是:

  1. 在根进程(进程0)上生成一个充满随机数字的数组。
  2. 把所有数字用MPI_Scatter分发给每个进程,每个进程得到的同样多的数字。
  3. 每个进程计算它们各自得到的数字的平均数。
  4. 根进程收集所有的平均数,然后计算这个平均数的平均数,得出最后结果。
// Author: Wes Kendall
// Copyright 2012 www.mpitutorial.com
// This code is provided freely with the tutorials on mpitutorial.com. Feel
// free to modify it for your own use. Any distribution of the code must
// either provide a link to www.mpitutorial.com or keep this header intact.
//
// Program that computes the average of an array of elements in parallel using
// MPI_Scatter and MPI_Gather
//
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <mpi.h>
#include <assert.h>

// Creates an array of random numbers. Each number has a value from 0 - 1
float *create_rand_nums(int num_elements) {
  float *rand_nums = (float *)malloc(sizeof(float) * num_elements);
  assert(rand_nums != NULL);
  int i;
  for (i = 0; i < num_elements; i++) {
    rand_nums[i] = (rand() / (float)RAND_MAX);
  }
  return rand_nums;
}

// Computes the average of an array of numbers
float compute_avg(float *array, int num_elements) {
  float sum = 0.f;
  int i;
  for (i = 0; i < num_elements; i++) {
    sum += array[i];
  }
  return sum / num_elements;
}

int main(int argc, char** argv) {
  if (argc != 2) {
    fprintf(stderr, "Usage: avg num_elements_per_proc\n");
    exit(1);
  }

  int num_elements_per_proc = atoi(argv[1]);
  // Seed the random number generator to get different results each time
  srand(time(NULL));

  MPI_Init(NULL, NULL);

  int world_rank;
  MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
  int world_size;
  MPI_Comm_size(MPI_COMM_WORLD, &world_size);

  // Create a random array of elements on the root process. Its total
  // size will be the number of elements per process times the number
  // of processes
  float *rand_nums = NULL;
  if (world_rank == 0) {
    rand_nums = create_rand_nums(num_elements_per_proc * world_size);
  }

  // For each process, create a buffer that will hold a subset of the entire
  // array
  float *sub_rand_nums = (float *)malloc(sizeof(float) * num_elements_per_proc);
  assert(sub_rand_nums != NULL);

  // Scatter the random numbers from the root process to all processes in
  // the MPI world
  MPI_Scatter(rand_nums, num_elements_per_proc, MPI_FLOAT, sub_rand_nums,
              num_elements_per_proc, MPI_FLOAT, 0, MPI_COMM_WORLD);

  // Compute the average of your subset
  float sub_avg = compute_avg(sub_rand_nums, num_elements_per_proc);

  // Gather all partial averages down to the root process
  float *sub_avgs = NULL;
  if (world_rank == 0) {
    sub_avgs = (float *)malloc(sizeof(float) * world_size);
    assert(sub_avgs != NULL);
  }
  MPI_Gather(&sub_avg, 1, MPI_FLOAT, sub_avgs, 1, MPI_FLOAT, 0, MPI_COMM_WORLD);

  // Now that we have all of the partial averages on the root, compute the
  // total average of all numbers. Since we are assuming each process computed
  // an average across an equal amount of elements, this computation will
  // produce the correct answer.
  if (world_rank == 0) {
    float avg = compute_avg(sub_avgs, world_size);
    printf("Avg of all elements is %f\n", avg);
    // Compute the average across the original data for comparison
    float original_data_avg =
      compute_avg(rand_nums, num_elements_per_proc * world_size);
    printf("Avg computed across original data is %f\n", original_data_avg);
  }

  // Clean up
  if (world_rank == 0) {
    free(rand_nums);
    free(sub_avgs);
  }
  free(sub_rand_nums);

  MPI_Barrier(MPI_COMM_WORLD);
  MPI_Finalize();
}

上面这段代码有点犯懒,我本人没有写,直接copy Wes Kendall的代码,我简单来讲讲这个代码的思路:

// 在根进程中,随机产生elements_per_proc * world_size个随机数字的数组
if (world_rank == 0) {
  rand_nums = create_rand_nums(elements_per_proc * world_size);
}

// 创建一个根进程传给其他进程的子数组作为缓存区,子数组的长度为elements_per_proc
float *sub_rand_nums = malloc(sizeof(float) * elements_per_proc);

// 创建好了子数组作为缓存区之后,根进程就开始分发数据了
MPI_Scatter(rand_nums, elements_per_proc, MPI_FLOAT, sub_rand_nums,
            elements_per_proc, MPI_FLOAT, 0, MPI_COMM_WORLD);

// 将每个进程得到的子数组的元素去平均值
float sub_avg = compute_avg(sub_rand_nums, elements_per_proc);

// 再创建一个缓存区用来存储其他进程上传它们计算得到的平均值
float *sub_avgs = NULL;
if (world_rank == 0) {
  sub_avgs = malloc(sizeof(float) * world_size);
}
// 根进程gather到了其他进程传过来的数据
MPI_Gather(&sub_avg, 1, MPI_FLOAT, sub_avgs, 1, MPI_FLOAT, 0,
           MPI_COMM_WORLD);
           
// 再去计算这些平均数的平均数们就能得到总的平均数
if (world_rank == 0) {
  float avg = compute_avg(sub_avgs, world_size);
}

3.4 MPI_Allgather

上面两个用来操作多对一或者一对多通信模式,也就是说多个进程要么向一个进程发送数据,要么从一个进程接收数据。

MPI_Allgather却是多个元素到多个进程(也就是多对多通信模式)。

在这里插入图片描述函数原型:

MPI_Allgather(
    void* send_data,
    int send_count,
    MPI_Datatype send_datatype,
    void* recv_data,
    int recv_count,
    MPI_Datatype recv_datatype,
    MPI_Comm communicator)

MPI_Allgather的方法定义跟MPI_Gather几乎一样,只不过MPI_Allgather不需要root这个参数来指定根节点。

四、并行排名

4.1 问题概述

在这里插入图片描述

4.2 并行排名API定义

在深入研究并行排名问题之前,首先确定函数的行为方式。

  1. 函数需要在每个进程上取一个数字,并返回其相对于所有其他进程中的数字的排名
  2. 需要正在使用的communicator
  3. 被排名的数字的数据类型

函数原型:

TMPI_Rank(
	void* send_data, //作为缓冲区
	void* recv_data, // send_datade 排名
	MPI_Datatype datatype,
	MPI_Comm comm)

4.3 解决并行排名问题

4.3.1 对所有进程中的数字进行排序

最简单的方法是将所有数字收集到一个进程中并对数字进行排序。gather_numbers_to_root 函数负责将所有数字收集到根进程(root process)。

// 为进程0的TMPI_Rank收集数字。为MPI的数据类型分配空间
// 对进程0返回 void * 指向的缓冲区
// 对所有其他进程返回NULL
void *gather_numbers_to_root(void *number, MPI_Datatype datatype,
                             MPI_Comm comm) {
  int comm_rank, comm_size;
  MPI_Comm_rank(comm, &comm_rank);
  MPI_Comm_size(comm, &comm_size);

  // 在根进程上分配一个数组
  // 数组大小取决于所用的MPI数据类型
  int datatype_size;
  MPI_Type_size(datatype, &datatype_size);
  void *gathered_numbers;
  if (comm_rank == 0) {
    gathered_numbers = malloc(datatype_size * comm_size);
  }

  // 在根进程上收集所有数字
  MPI_Gather(number, 1, datatype, gathered_numbers, 1,
             datatype, 0, comm);

  return gathered_numbers;
}
  • gather_numbers_to_root 函数获取要收集的数字(即 send_data 变量)、数字的数据类型 datatypecomm 通讯器。
  • 根进程必须在此函数中收集 comm_size 个数字,因此它会分配 datatype_size * comm_size 长度的数组.
  • 这里通过使用新的MPI函数- MPI_Type_size 来收集datatype_size变量。

4.3.2 排序数字并维护所属

  • 在我们的排名函数中,排序数字不一定是难题。CPP中提供了许多排序算法(我们也可以自己写)
  • 排序的困难在于,我们必须维护各个进程将数字发送到根进程的次序。 如果我们要对收集到根进程的数组进行排序而不给数字附加信息,则根进程将不知道如何将数字的排名发送回原来请求的进程!
  • 为了便于将所属进程附到对应数字上,我们在代码中创建了一个结构体(struct)来保存此信息。
    该结构体定义如下:
// 保存进程在通讯器中的次序(rank)和对应数字
// 该结构体用于数组排序,
// 并同时完整保留所属进程信息

typedef struct {
  int comm_rank;
  union {
    float f;
    int i;
  } number;
} CommRankNumber;

CommRankNumber 结构体保存了我们要排序的数字(记住它可以是浮点数或整数,因此我们使用联合体union),并且它拥有该数字所属进程在通讯器中的次序(rank)

get_ranks 函数,负责创建这些结构体并对它们进行排序。

// 这个函数在根进程上对收集到的数字排序
// 返回一个数组,数组按进程在通讯器中的次序排序
// 注意 - 该函数只在根进程上运行

int *get_ranks(void *gathered_numbers, int gathered_number_count,
               MPI_Datatype datatype) {
  int datatype_size;
  MPI_Type_size(datatype, &datatype_size);

  // 将收集到的数字数组转换为CommRankNumbers数组
  // 这允许我们在排序的同时,完整保留数字所属进程的信息
  
  CommRankNumber *comm_rank_numbers = malloc(
    gathered_number_count * sizeof(CommRankNumber));
  int i;
  for (i = 0; i < gathered_number_count; i++) {
    comm_rank_numbers[i].comm_rank = i;
    memcpy(&(comm_rank_numbers[i].number),
           gathered_numbers + (i * datatype_size),
           datatype_size);
  }

  // 根据数据类型对comm_rank_numbers排序
  if (datatype == MPI_FLOAT) {
    qsort(comm_rank_numbers, gathered_number_count,
          sizeof(CommRankNumber), &compare_float_comm_rank_number);
  } else {
    qsort(comm_rank_numbers, gathered_number_count,
          sizeof(CommRankNumber), &compare_int_comm_rank_number);
  }

  // 现在comm_rank_numbers是排好序的,下面生成一个数组,
  // 包含每个进程的排名,数组第i个元素是进程i的数字的排名
  
  int *ranks = (int *)malloc(sizeof(int) * gathered_number_count);
  for (i = 0; i < gathered_number_count; i++) {
    ranks[comm_rank_numbers[i].comm_rank] = i;
  }

  // 清理并返回排名数组
  free(comm_rank_numbers);
  return ranks;
}
  • get_ranks 函数首先创建一个CommRankNumber结构体数组,并附上该数字所属进程在通讯器中的次序。 如果数据类型为 MPI_FLOAT ,则对我们的结构体数组调用 qsort 时,会使用特殊的排序函数。类似的,如果数据类型为 MPI_INT ,我们将使用不同的排序函数。

  • 在对数字进行排序之后,我们必须以适当的顺序创建一个排名数组(array of ranks),以便将它们分散(scatter)回到请求的进程中。这是通过创建 ranks 数组并为每个已排序的 CommRankNumber 结构体填充适当的排名来实现的。

4.3.3 整合

现在有了两个主要函数,可以将它们全部整合到 TMPI_Rank 函数中。此函数将数字收集到根进程,并对数字进行排序以确定其排名,然后将排名分散回请求的进程。 代码如下所示:

// 获取send_data的排名, 类型为datatype
// 排名用recv_data返回,类型为datatype
int TMPI_Rank(void *send_data, void *recv_data, MPI_Datatype datatype,
             MPI_Comm comm) {
  // 首先检查基本情况 - 此函数只支持MPI_INT和MPI_FLOAT

  if (datatype != MPI_INT && datatype != MPI_FLOAT) {
    return MPI_ERR_TYPE;
  }

  int comm_size, comm_rank;
  MPI_Comm_size(comm, &comm_size);
  MPI_Comm_rank(comm, &comm_rank);

  // 为了计算排名,必须将数字收集到一个进程中
  // 对数字排序, 然后将排名结果分散传回
  // 首先在进程0上收集数字
  void *gathered_numbers = gather_numbers_to_root(send_data, datatype,
                                                  comm);

  // 获取每个进程的次序(rank)
  int *ranks = NULL;
  if (comm_rank == 0) {
    ranks = get_ranks(gathered_numbers, comm_size, datatype);
  }

  // 分散发回排名结果
  MPI_Scatter(ranks, 1, MPI_INT, recv_data, 1, MPI_INT, 0, comm);

  // 清理
  if (comm_rank == 0) {
    free(gathered_numbers);
    free(ranks);
  }
}

TMPI_Rank 函数使用我们刚刚创建的两个函数 gather_numbers_to_rootget_ranks 来获取数字的排名。然后,函数执行最后的 MPI_Scatter,以将所得的排名分散传回进程。

4.4 最终结果

最终代码参看tutorials/performing-parallel-rank-with-mpi/code/tmpi_rank.c

以下是整个数据流说明:
在这里插入图片描述

五、MPI Reduce and Allreduce

5.1 归约/归化(reduce)简介

这个概念在OpenMPI中介绍过,详情参见C/C++实现高性能并行计算——2.使用OpenMP进行共享内存编程

5.2 MPI_Reduce

函数原型:

MPI_Reduce(
    void* send_data,
    void* recv_data,
    int count,
    MPI_Datatype datatype,
    MPI_Op op,
    int root,
    MPI_Comm communicator)
  1. send_data 参数是每个进程都希望归约的 datatype 类型元素的数组。
  2. recv_data 仅与具有 root 秩的进程相关。 recv_data 数组包含归约的结果,大小为sizeof(datatype)* count
  3. op 参数是希望应用于数据的操作。 MPI 包含一组可以使用的常见归约运算。 尽管可以定义自定义归约操作,但这里不作介绍。

MPI定义的归约操作包括:
在这里插入图片描述
在这里插入图片描述

5.3 使用MPI_Reduce计算均值

在 第三节 中,展示了使用 MPI_ScatterMPI_Gather 计算平均值。 使用 MPI_Reduce 可以简化上一节的代码。

float *rand_nums = NULL;
rand_nums = create_rand_nums(num_elements_per_proc);

// Sum the numbers locally
float local_sum = 0;
int i;
for (i = 0; i < num_elements_per_proc; i++) {
  local_sum += rand_nums[i];
}

// Print the random numbers on each process
printf("Local sum for process %d - %f, avg = %f\n",
       world_rank, local_sum, local_sum / num_elements_per_proc);

// Reduce all of the local sums into the global sum
float global_sum;
MPI_Reduce(&local_sum, &global_sum, 1, MPI_FLOAT, MPI_SUM, 0,
           MPI_COMM_WORLD);

// Print the result
if (world_rank == 0) {
  printf("Total sum = %f, avg = %f\n", global_sum,
         global_sum / (world_size * num_elements_per_proc));
}
  1. 每个进程都会创建随机数并计算和保存在 local_sum
  2. 然后使用 MPI_SUMlocal_sum 归约至根进程。
  3. 全局平均值为 global_sum / (world_size * num_elements_per_proc)

5.4 MPI_Allreduce

如果所有进程而不是仅仅在根进程中访问归约的结果。 MPI_Allreduce 将归约值并将结果分配给所有进程。 函数原型如下:

MPI_Allreduce(
    void* send_data,
    void* recv_data,
    int count,
    MPI_Datatype datatype,
    MPI_Op op,
    MPI_Comm communicator)

在这里插入图片描述

5.4.1 使用 MPI_Allreduce 计算标准差

详情代码见tutorials/mpi-reduce-and-allreduce/code/reduce_stddev.c


总结

其实这篇文章到了后期我写的不是很满意,因为代码都是直接复制粘贴,不像之前的都是自己写再去对照别人的代码。可能是状态不好,也有可能是太急于求成了,罪过,罪过。太想进步啦!!!后期我可能会重新编辑这篇文章,代码我也要重新写一遍。

  1. 集体通信和同步点
  2. 广播MPI_Bcast
  3. 发散MPI_Scatter和收集MPI_Gather(所有进程都要一份)MPI_Allgather
  4. 归约/归化 MPI_Reduce,所有进程都要一份归化MPI_Allreduce

参考

  1. MPI Tutorials

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

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

相关文章

【一站式学会Kotlin】第四节默认参数和具名参数、unit返回值类型

作者介绍&#xff1a; 百度资深Android工程师T6&#xff0c;在百度任职7年半。 目前&#xff1a;成立赵小灰代码工作室&#xff0c;欢迎大家找我交流Android、微信小程序、鸿蒙项目。文章底部&#xff0c;csdn有为我插入微信的联络方式&#xff0c;欢迎大家联络我。 一&#x…

按键的短按、长按和连续的划分

在实际生活中&#xff0c;我们使用到的按键在短按、长按和按键松开时都会触发不同的功能。按键短按后松开和长按后松开的应用比短按和长按的应用较少&#xff0c;我了解的按键短按后松开和长按后松开的应用是在点动控制和长动控制中。这里主要讨论按键的短按、长按和连续这三种…

用Xinstall实现智能信息的无缝传递

在这个信息化的时代&#xff0c;智能信息的传递显得尤为重要。无论是对于个人还是企业&#xff0c;高效、准确的信息传递都是成功的关键。然而&#xff0c;随着科技的飞速发展&#xff0c;传统的信息传递方式已经无法满足我们的需求。这时&#xff0c;Xinstall应运而生&#xf…

Linux的命令(第二篇)

昨天学习到了第17个命令到 rm 命令&#xff08;作用删除目录和文件&#xff09;&#xff0c;今天继续往下里面了解其他命令以及格式、选项&#xff1a; &#xff08;17&#xff09;wc命令&#xff08;此wc非wc&#xff09; 作用&#xff1a;统计行数、单词数、字符分数。 格…

UEC++ FString做为参数取值时报错error:C4840

问题描述 用来取FString类型的变量时报错&#xff1a; 问题解决 点击错误位置&#xff0c;跳转到代码&#xff1a; void AMyDelegateActor::TwoParamDelegateFunc(int32 param1, FString param2) {UE_LOG(LogTemp, Warning, TEXT("Two Param1:%d Param2:%s"), param…

AutoCAD中密集的填充打散后消失的问题

有时候在AutoCAD中&#xff0c;图案填充的填充面积过大或填充太过密集时&#xff0c;将该填充打散&#xff0c;也就是执行Explode时&#xff0c;会发现填充图案消失了。 原因是打散后线条太大&#xff0c;系统就不显示了。可以通过设置&#xff1a;HPMAXLINES 值&#xff0c;来…

爬虫学习:XPath提取网页数据

目录 一、安装XPath 二、XPath的基础语法 1.选取节点 三、使用XPath匹配数据 1.浏览器审查元素 2.具体实例 四、总结 一、安装XPath 控制台输入指令&#xff1a;pip install lxml 二、XPath的基础语法 XPath是一种在XML文档中查找信息的语言&#xff0c;可以使用它在HTM…

echarts自定义图例显示名称、数值、占比

先上代码 legend: {orient: vertical,left: 10,top:20,data: data,textStyle: {color: #9FB7D5 // 设置图例文字颜色为白色},// type: plain, // 设置图例类型为普通类型itemWidth: 10, // 设置图例项的宽度itemHeight: 10, // 设置图例项的高度formatter: function(name) {let…

通用人工智能AGI,究竟是一个哲学问题还是技术问题?

引言 在探索人工智能的未来方向中&#xff0c;人工通用智能&#xff08;AGI&#xff09;的概念逐渐成为科技领域和哲学探讨的焦点。AGI旨在创建可以执行任何智能任务的机器&#xff0c;甚至在某些方面超越人类的能力。然而&#xff0c;关于AGI的研究不仅仅是技术问题&#xff…

WebSocket基础知识

WebSocket是什么&#xff1f; WebSocket 是一种网络通信协议&#xff0c;它提供了全双工通信机制&#xff0c;允许服务器主动向客户端发送消息&#xff0c;而不仅限于响应客户端的请求。它使用类似于 HTTP 的握手来建立连接&#xff0c;然后使用单独的持久连接来进行通信。这种…

vue2 八大组件通信,父子通信,跨层级通信,事件总线,vuex等

文章目录 什么是组件通信&#xff1f;父子通信流程propsProps 定义Props 作用特点数组写法对象写法&#xff08;props校验&#xff09;简写只验证数据类型&#xff1a;完整写法&#xff0c;完整的验证&#xff1a; props父向子传值用props父传子在子组件中修改props $emit子向父…

美国政府发布新的国际网络空间和数字政策战略(上)

文章目录 前言一、战略内容介绍二、数字团结的含义三、如何建立数字团结前言 美国务院5月6日正式发布《美国国际网络空间和数字政策战略:迈向创新、安全和尊重权利的数字未来》,旨在指导国际社会参与技术外交并推动《美国国家安全战略》和《美国国家网络安全战略》。 美国务…

无人售货机开启财富新机遇

创立属于自己的小本事业&#xff0c;却因经验匮乏及风险顾虑望而却步&#xff1f;探索零风险且潜在高收益的创业途径&#xff0c;无人售货机项目脱颖而出&#xff0c;成为理想之选&#xff01; 无人售货机&#xff1a;市场蓝海待挖掘 在消费升级和技术飞跃的双重驱动下&#…

【DDR 终端稳压器】Sink and Source DDR Termination Regulator [C] S0 S1 S2 S3 S4 S5 6状态

TPS51200A-Q1 器件通过 EN 功能提供 S3 支持。EN引脚可以连接到终端应用中的SLP_S3信号。当EN 高电平&#xff08;S0 状态&#xff09;时&#xff0c;REFOUT 和 VO 引脚均导通。当EN 低电平&#xff08;S3状态&#xff09;时&#xff0c;VO引脚关断并通过内部放电MOSFET放电时…

Photoshop中图层的应用

Photoshop中图层的应用 前言Photoshop中的图层面板Photoshop中图层的基本操作新建图层复制/剪切图层链接图层修改图层名称及颜色背景图层与普通图层栅格化图层图层的对齐与分布图层的合并 前言 图层在Photoshop中就像一层一层的透明纸&#xff0c;可以透过图层的透明区域看到下…

纯血鸿蒙APP实战开发——阅读翻页方式案例

介绍 本示例展示手机阅读时左右翻页&#xff0c;上下翻页&#xff0c;覆盖翻页的功能。 效果图预览 使用说明 进入模块即是左右翻页模式。点击屏幕中间区域弹出上下菜单。点击设置按钮&#xff0c;弹出翻页方式切换按钮&#xff0c;点击可切换翻页方式。左右翻页方式可点击翻…

显影不干净如何解决?

知识星球&#xff08;星球名&#xff1a;芯片制造与封测社区&#xff0c;星球号&#xff1a;63559049&#xff09;里的学员问&#xff1a;光刻工序完成后&#xff0c;晶圆表面有部分图形容易出现显影不净是什么原因&#xff1f;有什么好的解决办法吗&#xff1f; 光刻工序流程 …

武汉星起航引领跨境新浪潮,一站式解决方案助力卖家驰骋亚马逊

在全球化浪潮下&#xff0c;跨境电商已成为外贸发展的新引擎&#xff0c;为无数创业者提供了全新的商业机遇。而在这场跨境电商的浪潮中&#xff0c;武汉星起航电子商务有限公司以其专业的一站式解决方案&#xff0c;成为众多创业者和卖家的得力助手&#xff0c;引领着他们成功…

信息系统项目管理师计算题讲解,考前必看

信息系统项目管理师考试中的计算题主要涉及项目管理类的计算&#xff0c;重点在于进度和成本的计算。案例计算题所占分值较高&#xff0c;务必要熟练掌握&#xff01;根据近年考题内容来看&#xff0c;主要涉及挣值计算和网络图&#xff0c;当然也可能会涉及其他内容&#xff0…

图片转pdf的java代码实现

一、实现方式 采用itextpdf和itext包&#xff0c;使用java代码&#xff0c;把图片转换为pdf. 支持文件格式&#xff1a;png&#xff0c;jpg, jpeg,gif 二、java代码实现 1、maven依赖 <!-- https://mvnrepository.com/artifact/com.itextpdf/itextpdf --><dependen…