C++并行计算:OpenMP与MPI全解析

在高性能计算领域,充分利用硬件资源的并行计算技术已成为刚需。从单节点多核到跨节点集群,开发者需要掌握不同的并行编程模型。本文将系统讲解两种主流并行技术:OpenMP(共享内存多核并行)与MPI(分布式内存集群并行),从基础语法到实战案例,全面覆盖其原理、实现与优化技巧,帮助开发者构建高效的并行应用。

一、并行计算基础与两种模型对比

1.1 并行计算的核心挑战

随着摩尔定律放缓,单核心性能提升受限,并行计算成为突破性能瓶颈的关键。并行计算的核心挑战包括:

  • 数据划分:如何将大规模数据均匀分配到多个计算单元
  • 通信开销:计算单元间的数据交互成本
  • 同步机制:保证并行操作的正确性与一致性
  • 负载均衡:避免部分计算单元空闲,最大化资源利用率

1.2 两种并行模型的本质区别

特性OpenMPMPI
内存模型共享内存(单节点内)分布式内存(跨节点)
并行单元线程(轻量级)进程(重量级)
通信方式共享变量访问显式消息传递
适用场景多核CPU单节点集群/超级计算机多节点
编程复杂度低(增量并行化)高(需设计通信逻辑)
扩展性有限(受单节点核心数限制)高(支持数千节点)
典型应用科学计算、数值模拟分布式机器学习、气象模拟

1.3 混合并行模型(OpenMP+MPI)

在大规模集群中,通常采用混合模型:节点内用OpenMP利用多核,节点间用MPI通信。这种组合兼顾了编程效率与扩展性,是当前高性能计算的主流方案。

二、OpenMP:共享内存多核并行

OpenMP(Open Multi-Processing)是基于共享内存的并行编程API,通过编译制导语句(#pragma)实现增量式并行化,无需重写整个程序,是多核单节点并行的首选技术。

2.1 OpenMP核心原理与编译环境

核心原理

OpenMP采用fork-join模型:

  1. 程序默认以单线程(主线程)开始执行
  2. 遇到并行区域(#pragma omp parallel)时,"fork"出多个子线程
  3. 并行区域内,所有线程协同工作
  4. 并行区域结束时,"join"所有子线程,回归单线程执行
编译环境配置
  • GCC/Clang:需添加-fopenmp编译选项
    g++ -fopenmp omp_demo.cpp -o omp_demo
    
  • MSVC:默认支持OpenMP,需在项目属性中启用(/openmp
  • 验证支持:通过宏_OPENMP判断编译器是否支持
    #ifdef _OPENMP
    #include <omp.h>
    #else
    #error "编译器不支持OpenMP"
    #endif
    

2.2 OpenMP基础语法:编译制导语句

2.2.1 并行区域(parallel)

最基本的并行构造,用于创建线程组:

#include <iostream>
#include <omp.h>int main() {std::cout << "主线程开始(单线程)\n";// 创建并行区域,默认线程数=CPU核心数#pragma omp parallel{// 每个线程执行该代码块int tid = omp_get_thread_num();  // 获取线程IDint nthreads = omp_get_num_threads();  // 获取总线程数std::cout << "线程" << tid << ":进入并行区域(共" << nthreads << "个线程)\n";}  // 所有线程在此同步,然后销毁std::cout << "主线程结束(单线程)\n";return 0;
}

注意cout输出可能混乱(多个线程同时写入),需同步处理。

2.2.2 循环并行化(for)

#pragma omp for用于将循环迭代分配到多个线程:

// 并行求和示例
#include <vector>
#include <iostream>
#include <omp.h>int main() {const int N = 1000000;std::vector<int> data(N, 1);  // 初始化100万个1long long sum = 0;#pragma omp parallel for reduction(+:sum)  // reduction自动处理sum的并行累加for (int i = 0; i < N; ++i) {sum += data[i];  // 多个线程同时累加,reduction保证结果正确}std::cout << "总和:" << sum << "(预期:" << N << ")\n";return 0;
}

关键参数

  • reduction(op:var):对变量var执行op+/*/max等)的归约,避免竞争
  • schedule(type, chunk):指定迭代分配方式
    • static:静态分配(默认),适合迭代耗时均匀的场景
    • dynamic:动态分配,适合迭代耗时不均的场景
    • guided:引导式分配,初始块大,逐渐减小
// 动态调度示例(每个线程处理100个迭代后再请求新任务)
#pragma omp parallel for schedule(dynamic, 100)
for (int i = 0; i < N; ++i) {process(data[i]);  // 假设process耗时不均
}
2.2.3 任务划分(sections)

sections用于将不同任务分配给不同线程(非循环类并行):

// 多任务并行示例
#include <iostream>
#include <omp.h>void task1() { std::cout << "任务1执行\n"; }
void task2() { std::cout << "任务2执行\n"; }
void task3() { std::cout << "任务3执行\n"; }int main() {#pragma omp parallel sections{#pragma omp sectiontask1();#pragma omp sectiontask2();#pragma omp sectiontask3();}return 0;
}
2.2.4 同步机制

并行执行中需通过同步机制避免数据竞争:

  1. 临界区(critical):保证代码块同一时间只有一个线程执行

    int counter = 0;
    #pragma omp parallel for
    for (int i = 0; i < 1000; ++i) {#pragma omp critical{counter++;  // 临界区保护共享变量}
    }
    
  2. 原子操作(atomic):比critical更轻量,仅保护单一操作

    #pragma omp atomic
    counter++;  // 原子自增,比critical高效
    
  3. 屏障(barrier):等待所有线程到达屏障点后再继续

    #pragma omp parallel
    {// 阶段1:各自计算local_compute();#pragma omp barrier  // 等待所有线程完成阶段1// 阶段2:使用阶段1的结果global_compute();
    }
    
  4. 线程私有变量(private):为每个线程创建独立变量副本

    int x = 0;  // 主线程变量
    #pragma omp parallel private(x)  // 每个线程有自己的x,初始值不确定
    {x = omp_get_thread_num();  // 线程私有x赋值std::cout << "线程" << omp_get_thread_num() << "的x:" << x << "\n";
    }
    std::cout << "主线程的x:" << x << "\n";  // 仍为0
    

    相关变量类型:

    • private:线程私有,初始值未定义
    • firstprivate:继承主线程初始值的私有变量
    • lastprivate:将最后一个迭代的私有变量值传回主线程
    • shared:线程间共享(默认)

2.3 OpenMP实战:并行矩阵乘法

矩阵乘法是典型的计算密集型任务,适合用OpenMP并行化:

#include <vector>
#include <iostream>
#include <omp.h>
#include <chrono>// 矩阵乘法:C = A * B
void matrix_multiply(const std::vector<std::vector<double>>& A,const std::vector<std::vector<double>>& B,std::vector<std::vector<double>>& C) {int n = A.size();int m = B[0].size();int k = B.size();// 并行化外层循环(行)#pragma omp parallel for collapse(2)  // collapse(2)将两层循环合并并行for (int i = 0; i < n; ++i) {for (int j = 0; j < m; ++j) {double sum = 0.0;for (int l = 0; l < k; ++l) {sum += A[i][l] * B[l][j];}C[i][j] = sum;}}
}int main() {const int size = 1000;  // 1000x1000矩阵std::vector<std::vector<double>> A(size, std::vector<double>(size, 1.0));std::vector<std::vector<double>> B(size, std::vector<double>(size, 1.0));std::vector<std::vector<double>> C(size, std::vector<double>(size, 0.0));// 并行计算并计时auto start = std::chrono::high_resolution_clock::now();matrix_multiply(A, B, C);auto end = std::chrono::high_resolution_clock::now();std::cout << "矩阵乘法耗时:"<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()<< " ms\n";std::cout << "验证结果(应为" << size << "):" << C[0][0] << "\n";return 0;
}

优化技巧

  • 利用collapse(2)合并两层循环,提高并行粒度
  • 调整矩阵存储顺序(如行主序转列主序),优化缓存利用率
  • 设置OMP_NUM_THREADS环境变量控制线程数(通常设为CPU核心数)
    export OMP_NUM_THREADS=8  # 限制为8线程
    

三、MPI:分布式内存集群并行

MPI(Message Passing Interface)是分布式内存系统的标准并行编程接口,通过显式消息传递实现跨节点通信,支持从多节点集群到超级计算机的大规模并行。

3.1 MPI核心原理与环境配置

核心原理

MPI采用分布式内存模型

  • 每个进程有独立内存空间,不能直接访问其他进程的内存
  • 进程间通过消息传递交换数据(发送MPI_Send/接收MPI_Recv
  • 进程通过唯一ID(rank)标识,通过通信子(communicator)组织
环境配置
  • MPI实现:主流实现包括OpenMPI、MPICH、Intel MPI
  • 安装(Ubuntu)
    sudo apt-get install openmpi-bin openmpi-common libopenmpi-dev
    
  • 编译命令
    mpic++ mpi_demo.cpp -o mpi_demo  # 自动链接MPI库
    
  • 运行命令
    mpirun -np 4 ./mpi_demo  # 启动4个进程运行程序
    

3.2 MPI基础函数与核心概念

3.2.1 初始化与结束

所有MPI程序必须以初始化开始,以结束函数收尾:

#include <mpi.h>
#include <iostream>int main(int argc, char**argv) {// 初始化MPI环境MPI_Init(&argc, &argv);// 获取进程总数int world_size;MPI_Comm_size(MPI_COMM_WORLD, &world_size);// 获取当前进程ID(rank)int world_rank;MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);std::cout << "Hello from rank " << world_rank << " of " << world_size << "\n";// 结束MPI环境MPI_Finalize();return 0;
}

核心概念:

  • MPI_COMM_WORLD:默认通信子,包含所有进程
  • rank:进程唯一标识(0到world_size-1
  • MPI_Init:解析命令行参数,初始化通信环境
  • MPI_Finalize:释放MPI资源,所有进程必须调用
3.2.2 点对点通信(Point-to-Point)

进程间直接发送/接收消息的基础通信方式:

// 简单消息传递示例:rank 0向rank 1发送数据
#include <mpi.h>
#include <iostream>int main(int argc, char**argv) {MPI_Init(&argc, &argv);int world_rank;MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);int world_size;MPI_Comm_size(MPI_COMM_WORLD, &world_size);// 确保至少有2个进程if (world_size < 2) {if (world_rank == 0) {std::cerr << "至少需要2个进程!\n";}MPI_Finalize();return 1;}const int data_size = 10;int data[data_size];if (world_rank == 0) {// 初始化数据for (int i = 0; i < data_size; ++i) {data[i] = i;}// 向rank 1发送数据:缓冲区、大小、类型、目标rank、标签、通信子MPI_Send(data, data_size, MPI_INT, 1, 0, MPI_COMM_WORLD);std::cout << "rank 0发送了数据\n";} else if (world_rank == 1) {// 接收rank 0的数据:缓冲区、大小、类型、源rank、标签、通信子、状态MPI_Status status;MPI_Recv(data, data_size, MPI_INT, 0, 0, MPI_COMM_WORLD, &status);// 从状态中获取实际接收的大小int recv_size;MPI_Get_count(&status, MPI_INT, &recv_size);std::cout << "rank 1接收了" << recv_size << "个数据:";for (int i = 0; i < recv_size; ++i) {std::cout << data[i] << " ";}std::cout << "\n";}MPI_Finalize();return 0;
}

关键参数

  • MPI_Comm:通信子(MPI_COMM_WORLD为默认全局通信子)
  • tag:消息标签(用于区分同一对进程的不同消息)
  • MPI_Status:接收状态,包含实际接收大小、源进程、标签等信息
3.2.3 集合通信(Collective Communication)

同时涉及通信子中所有进程的通信操作,比点对点通信更高效:

  1. 广播(Broadcast):从一个进程向所有其他进程发送数据

    // rank 0广播数据到所有进程
    if (world_rank == 0) {// 准备数据
    }
    MPI_Bcast(data, size, MPI_INT, 0, MPI_COMM_WORLD);  // 从rank 0广播
    
  2. 归约(Reduction):将所有进程的局部数据合并为全局结果

    // 每个进程计算局部和,最终在rank 0得到全局和
    int local_sum = ...;
    int global_sum;
    MPI_Reduce(&local_sum, &global_sum, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);
    

    常用归约操作:MPI_SUM(求和)、MPI_PROD(求积)、MPI_MAX(最大值)等。

  3. 散射(Scatter):将一个进程的数组分散到所有进程

    // rank 0有一个大数组,分散到每个进程
    int sendbuf[100];  // rank 0的数组
    int recvbuf[10];   // 每个进程接收10个元素
    MPI_Scatter(sendbuf, 10, MPI_INT, recvbuf, 10, MPI_INT, 0, MPI_COMM_WORLD);
    
  4. 聚集(Gather):将所有进程的数据收集到一个进程

    // 每个进程的局部结果聚集到rank 0
    int sendbuf[10];   // 每个进程的10个元素
    int recvbuf[100];  // rank 0接收的总数组
    MPI_Gather(sendbuf, 10, MPI_INT, recvbuf, 10, MPI_INT, 0, MPI_COMM_WORLD);
    

3.3 MPI实战:分布式素数筛选

用MPI实现并行埃拉托斯特尼筛法,筛选指定范围内的素数:

#include <mpi.h>
#include <vector>
#include <iostream>
#include <cmath>// 局部筛选函数:找出[start, end)中的素数
std::vector<int> local_sieve(int start, int end, const std::vector<int>& small_primes) {std::vector<bool> is_prime(end - start, true);int offset = start;// 0对应start,1对应start+1,依此类推for (int p : small_primes) {// 计算p的第一个倍数 >= startint first = std::max(p * p, (start + p - 1) / p * p);for (int i = first; i < end; i += p) {is_prime[i - offset] = false;}}// 收集素数std::vector<int> primes;for (int i = 0; i < is_prime.size(); ++i) {if (is_prime[i] && (offset + i) > 1) {primes.push_back(offset + i);}}return primes;
}int main(int argc, char**argv) {MPI_Init(&argc, &argv);int world_rank, world_size;MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);MPI_Comm_size(MPI_COMM_WORLD, &world_size);const int n = 1000000;  // 筛选1到n之间的素数const int sqrt_n = std::sqrt(n);std::vector<int> small_primes;std::vector<int> all_primes;if (world_rank == 0) {// 主进程计算小素数(<=sqrt(n))std::vector<bool> sieve(sqrt_n + 1, true);sieve[0] = sieve[1] = false;for (int i = 2; i <= sqrt(n); ++i) {if (sieve[i]) {small_primes.push_back(i);for (int j = i * i; j <= sqrt(n); j += i) {sieve[j] = false;}}}all_primes = small_primes;  // 先加入小素数}// 广播小素数到所有进程int small_size = small_primes.size();MPI_Bcast(&small_size, 1, MPI_INT, 0, MPI_COMM_WORLD);if (world_rank != 0) {small_primes.resize(small_size);}MPI_Bcast(small_primes.data(), small_size, MPI_INT, 0, MPI_COMM_WORLD);// 划分区间:每个进程处理一部分int chunk_size = (n - sqrt_n) / world_size;int start = sqrt_n + 1 + world_rank * chunk_size;int end = (world_rank == world_size - 1) ? n + 1 : start + chunk_size;// 局部筛选std::vector<int> local_primes = local_sieve(start, end, small_primes);// 收集各进程的素数到主进程if (world_rank == 0) {// 先加入主进程自己的局部素数all_primes.insert(all_primes.end(), local_primes.begin(), local_primes.end());// 接收其他进程的素数for (int rank = 1; rank < world_size; ++rank) {int size;MPI_Recv(&size, 1, MPI_INT, rank, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);std::vector<int> recv_primes(size);MPI_Recv(recv_primes.data(), size, MPI_INT, rank, 1, MPI_COMM_WORLD, MPI_STATUS_IGNORE);all_primes.insert(all_primes.end(), recv_primes.begin(), recv_primes.end());}std::cout << "1到" << n << "之间共有" << all_primes.size() << "个素数\n";// 可选:排序并输出部分素数} else {// 发送局部素数大小和数据到主进程int size = local_primes.size();MPI_Send(&size, 1, MPI_INT, 0, 0, MPI_COMM_WORLD);MPI_Send(local_primes.data(), size, MPI_INT, 0, 1, MPI_COMM_WORLD);}MPI_Finalize();return 0;
}

优化技巧

  • 合理划分数据区间,保证负载均衡
  • 先筛选小素数(<=sqrt(n)),再用小素数标记大区间的合数
  • 集合通信(如MPI_Bcast)比多次点对点通信更高效

四、OpenMP与MPI的混合编程

在大规模并行系统中,单一模型往往无法满足需求。混合编程(OpenMP+MPI)结合两者优势:节点内用OpenMP利用多核,节点间用MPI通信,是当前高性能计算的主流方案。

4.1 混合编程的核心架构

混合编程模型架构:

  1. 启动多个MPI进程(通常一个节点一个进程)
  2. 每个MPI进程内部通过OpenMP创建多个线程,利用节点内多核
  3. 节点间通过MPI消息传递通信,节点内通过共享内存通信

4.2 混合编程实战:并行积分计算

以数值积分(计算π值)为例,展示混合编程模式:

#include <mpi.h>
#include <omp.h>
#include <iostream>
#include <cmath>// 被积函数:f(x) = 4/(1+x²),积分从0到1的结果为π
double f(double x) {return 4.0 / (1.0 + x * x);
}// 并行积分计算
double parallel_integrate(int n, int world_rank, int world_size) {double h = 1.0 / n;  // 步长int local_n = n / world_size;  // 每个MPI进程的子区间数double local_sum = 0.0;// 每个MPI进程内部用OpenMP并行化#pragma omp parallel reduction(+:local_sum){// 获取线程ID和总线程数int thread_num = omp_get_thread_num();int num_threads = omp_get_num_threads();int thread_local_n = local_n / num_threads;// 计算线程负责的区间int start = world_rank * local_n + thread_num * thread_local_n;int end = (thread_num == num_threads - 1) ? (world_rank + 1) * local_n : start + thread_local_n;// 梯形法积分for (int i = start; i < end; ++i) {double x = h * (i + 0.5);  // 中点local_sum += f(x);}}// 所有MPI进程的局部和归约到全局和double global_sum;MPI_Reduce(&local_sum, &global_sum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);// 乘以步长得到积分结果return global_sum * h;
}int main(int argc, char**argv) {MPI_Init(&argc, &argv);int world_rank, world_size;MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);MPI_Comm_size(MPI_COMM_WORLD, &world_size);// 设置OpenMP线程数(每个MPI进程的线程数)omp_set_num_threads(4);  // 假设每个节点4核const int n = 100000000;  // 总步数(越大精度越高)double pi = parallel_integrate(n, world_rank, world_size);if (world_rank == 0) {std::cout << "积分结果:" << pi << "\n";std::cout << "误差:" << std::fabs(pi - M_PI) << "\n";}MPI_Finalize();return 0;
}

编译与运行

mpic++ -fopenmp hybrid_pi.cpp -o hybrid_pi  # 混合编译
mpirun -np 2 ./hybrid_pi  # 启动2个MPI进程,每个进程4线程(共8核)

优势

  • 节点内共享内存通信避免了消息传递开销
  • 节点间MPI通信支持大规模扩展
  • 资源利用率更高(同时利用多核与多节点)

五、两种模型的对比与最佳实践

5.1 技术选型指南

场景推荐技术理由
单节点多核CPUOpenMP编程简单,无通信开销
多节点集群MPI支持分布式内存,扩展性好
大规模集群(>100节点)MPI+OpenMP混合兼顾节点内效率与跨节点扩展性
快速原型开发OpenMP增量并行化,开发效率高
实时性要求高的应用OpenMP线程通信延迟低
数据密集型分布式应用MPI适合处理跨节点分布的大规模数据

5.2 性能优化通用原则

  1. 最小化通信:通信是并行程序的主要开销来源,尽量减少数据交互

    • OpenMP:减少共享变量访问(用私有变量)
    • MPI:批量传输数据(减少消息数量),利用集合通信
  2. 负载均衡:确保每个计算单元(线程/进程)的工作量相近

    • OpenMP:用dynamic调度处理不均任务
    • MPI:根据节点性能分配不同负载(如强节点多分配任务)
  3. 内存局部性:提高数据在缓存中的命中率

    • 按缓存行大小对齐数据
    • 优化访问模式(如行优先遍历)
  4. 避免过度并行:并行粒度太小会导致调度/通信开销超过收益

    • OpenMP:避免循环迭代次数过少的并行
    • MPI:进程数不宜远大于节点数×核心数

5.3 常见问题与解决方案

问题OpenMP解决方案MPI解决方案
数据竞争使用critical/atomic/reduction避免共享数据,通过消息同步
负载不均schedule(dynamic)动态调度动态任务分配(如主从模式)
通信开销大增加私有计算比例合并消息,使用非阻塞通信(MPI_Isend
扩展性受限增加节点数(需结合MPI)优化通信模式,减少全局同步
调试困难禁用并行(OMP_NUM_THREADS=1)单步调试单进程调试,逐步增加进程数

六、总结与未来展望

并行计算是高性能应用的核心技术,OpenMP与MPI分别在共享内存与分布式内存领域发挥着不可替代的作用:

  • OpenMP:以简单易用的编译制导语句实现多核并行,适合快速将串行程序并行化,是单节点性能优化的首选。
  • MPI:通过消息传递支持跨节点集群并行,扩展性强,是大规模科学计算与分布式应用的标准方案。
  • 混合模型:结合两者优势,在现代集群系统中提供最佳性能与扩展性。

未来,随着异构计算(CPU+GPU+FPGA)的普及,并行编程模型将进一步发展,如OpenMP对GPU的支持(target指令)、MPI与CUDA的混合编程等。掌握OpenMP与MPI的核心思想,将为适应未来并行计算架构奠定坚实基础。

并行计算的核心不仅是技术的应用,更是将问题分解为并行子任务的思维方式。通过不断实践与优化,开发者可以充分释放硬件潜力,构建高效的并行应用。

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

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

相关文章

TCP 动态选路协议全面研究:OSPF、BGP 与 IS-IS 的比较与应用分析

一、引言&#xff1a;动态选路协议概述 在现代计算机网络中&#xff0c;路由选择是数据传输的核心功能&#xff0c;它决定了数据包从源到目的地的路径选择。随着网络规模的不断扩大和复杂性的增加&#xff0c;静态路由已经无法满足网络动态变化的需求&#xff0c;动态路由协议…

OpenCV 图像哈希类cv::img_hash::AverageHash

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::img_hash::AverageHash是OpenCV中用于图像哈希&#xff08;Image Hashing&#xff09;的一个类&#xff0c;属于opencv_img_hash模块。它实现了…

【Python-网络爬虫】爬虫的基础概念介绍

目录 一、爬虫的介绍 1.1 爬虫的概念 1.2 爬虫的作用 1. 搜索引擎数据索引 2. 商业数据采集与分析 3. 舆情监控与社交分析 4. 学术研究与数据挖掘 5. 信息聚合与服务优化 二、爬虫的分类 三、爬虫的基本流程 3.1 基本流程 3.2 Robots协议 一、爬虫的介绍 1.1 爬虫的…

力扣-31.下一个排列

题目链接 31.下一个排列 class Solution {public void nextPermutation(int[] nums) {//1.从右往左找第一个非逆序的数aint left nums.length - 2; //这里是为了找不到顺序对的时候正好停在-1while (left > 0 && nums[left] > nums[left 1]) { //一定要取等号…

Python爬虫实战:研究python-nameparser库相关技术

1. 引言 在当今数字化时代,姓名作为个人身份的重要标识,在许多领域都有着广泛的应用需求。例如,在客户关系管理系统中,准确解析姓名可以帮助企业更好地了解客户背景;在学术研究中,分析作者姓名分布有助于发现研究团队的地域特征;在社交网络分析中,姓名信息可以辅助进行…

Android中MVI架构详解

博主前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住也分享一下给大家 👉点击跳转到教程 什么是 MVI 架构? MVI (Model-View-Intent) 是一种单向数据流的架构模式,它源于响应式编程思想。在 MVI 中: 架构图: 1、Model: 代表的是UI 状态,它包含了…

AutoGen-AgentChat-3-人机交互

import os from dotenv import load_dotenvload_dotenv()True人机交互 在上一节“团队”中&#xff0c;我们了解了如何创建、观察和控制代理团队。本节将重点介绍如何在应用程序中与团队进行交互&#xff0c;并向团队提供人工反馈。 您可以通过两种主要方式从您的应用程序与团队…

Flink Db2 CDC 环境配置与验证

一、DB2 数据库核心配置 1. 启用数据库日志记录与CDC支持 -- 以DB2管理员身份连接数据库 CONNECT TO mydb USER db2inst1 USING password;-- 启用数据库归档日志模式&#xff08;CDC依赖&#xff09; UPDATE DATABASE CONFIGURATION USING LOGARCHMETH1 DISK:/db2log/archive…

初识单例模式

文章目录场景通点定义实现思路六种 Java 实现饿汉式懒汉式synchronized 方法双重检查锁 Double Check Lock Volatile静态内部类 Singleton Holder枚举单例单例运用场景破解单例模式参考场景通点 资源昂贵&#xff1a;数据库连接池、线程池、日志组件&#xff0c;只需要一份全…

音乐抢单源码(连单卡单/叠加组规则/打针/多语言)

简介&#xff1a; 测试环境&#xff1a;Nginx、PHP7.2、MySQL5.6&#xff0c;运行目录设置为public&#xff0c;伪静态thinkphp&#xff0c;建议开启SSL 测试语言&#xff1a;11种 不知道谁给我的一套&#xff0c;说是买来的&#xff0c;我看了一下功能感觉也一般&#…

分类树查询性能优化:从 2 秒到 0.1 秒的技术蜕变之路

在电商系统中&#xff0c;分类树查询是一个基础且高频的功能&#xff0c;然而这个看似简单的功能背后却隐藏着不小的性能挑战。本文将分享我们在实际项目中对分类树查询功能进行五次优化的全过程&#xff0c;看如何将查询耗时从 2 秒缩短至 0.1 秒&#xff0c;为用户提供更流畅…

Ansible 介绍及安装

简介 Ansible 是一款开源的自动化工具&#xff0c;广泛应用于配置管理、应用部署、任务自动化以及多节点管理等领域。它由 Michael DeHaan 于 2012 年创建&#xff0c;ansible 目前已经已经被红帽官方收购&#xff0c;是自动化运维工具中大家认可度最高的&#xff0c;并且上手…

超光谱相机的原理和应用场景

超光谱相机是光谱成像技术的尖端形态&#xff0c;具备亚纳米级光谱分辨率与超千波段连续覆盖能力&#xff0c;通过“图谱合一”的三维数据立方体实现物质的精准识别与分析。其核心技术架构、应用场景及发展趋势如下&#xff1a;一、核心技术原理1、‌分光机制‌‌干涉分光‌&am…

掌握MySQL函数:高效数据处理指南

​ 在 MySQL 数据库管理系统中&#xff0c;函数扮演着极为重要的角色。它们就像是数据库操作的得力助手&#xff0c;能够帮助开发者高效地完成各种数据处理任务。本文将深入探讨 MySQL 函数的方方面面&#xff0c;从其基本概念到实际应用&#xff0c;帮助读者全面掌握这一强大的…

10.SpringBoot的统一异常处理详解

文章目录1. 异常处理基础概念1.1 什么是异常处理1.2 为什么需要统一异常处理1.3 Spring异常处理机制2. SpringBoot默认异常处理2.1 默认错误页面2.2 自定义错误页面3. 全局异常处理器3.1 基础全局异常处理器3.2 统一响应格式3.3 使用统一响应格式的异常处理器4. 自定义异常4.1 …

No Hack No CTF 2025Web部分个人WP

No Hack No CTF 2025 Next Song is 春日影 hint&#xff1a;NextJS Vulnerability at /adminCVE-2025-29927Next.js 中间件权限绕过漏洞 访问admin路由发现跳转利用CVE&#xff1a; curl -i \-H "x-middleware-subrequest: middleware:middleware:middleware:middleware:m…

STM32第十八天 ESP8266-01S和电脑实现串口通信

一&#xff1a; ESP和电脑实现串口通信1. 配置 WiFi 模式 ATCWMODE3 // softAPstation mode 响应 : OK 2. 连接路路由器器 ATCWJAP"SSID","password" // SSID and password of router 响应 : OK 3. 查询 ESP8266 设备的 IP 地址 ATCIFSR 响应 : CIFSR:APIP…

STM32第十七天ESP8266-01Swifi模块

ESP8266-01S wifi模块1&#xff1a;ESP8266是实现wifi通讯的一个模块种类&#xff0c;有很多分类包含esp8266-12、esp8266-12E、ESP8266-01S、esp32等等。esp8266-01S由一颗esp8266作为主控再由一块flash作为存储芯片组成&#xff0c;带有板载芯片供电采用3.3V电压使用串口进行…

ProCCD复古相机:捕捉复古瞬间

在数字摄影盛行的今天&#xff0c;复古胶片相机的独特质感和怀旧风格依然吸引着众多摄影爱好者。ProCCD复古相机APP正是这样一款能够满足用户对复古摄影需求的应用程序。它通过模拟复古CCD数码相机的效果&#xff0c;让用户在手机上也能轻松拍出具有千禧年风格的照片和视频。无…