当前位置: 首页 > news >正文

C/Fortran多核并行计算

文章目录

  • OpenMP基础
    • 1、OpenMP(Open Multi-Processing)
    • 2、 MPl (Message Passing Interface)
    • 3、CUDA (Compute Unified Device Architecture) /OpenACC
  • C/Fortran中OMP标记的说明
  • OpenMP构造并行域
    • OMP代码执行模式
    • 文本范围(lexical extent)和动态范围(dynamic extent)
    • 设置线程数
      • 常用函数
      • OMP线程数说明
      • 嵌套并行域
      • 子句
        • OMP的数据属性
        • if(b)
        • numthreads (nT)
        • default(private | firstprivate 丨 shared l(none)
        • private(a,b, c... )
        • firstprivate (a,b,c...)
        • reduction(operator:list)
        • copyin
        • proc_bind
        • allocate
    • 指令
      • sections
        • 子句
      • workshare
      • single/master
      • atomic/critical
      • flush
      • barrier
      • for/do
        • schedule子句
        • collapse子句
        • linear子句
  • 参考

OpenMP基础

新的0penMP标准(目前是5.2)支持GPU设备(显卡或专业计算卡)加速,但现有的OMP库对该功能支持有限(intel oneAPl套件支持intel的显卡),因此Devices相关的指令不做介绍。

此外,锁操作、SIMD指令等也不作介绍,读者可参阅最新的官方文档:

  • 《OpenMP Application Programming Interface》
  • 网址

并行计算是一种同时执行多任务或处理多数据的技术:
1、多任务:将任务划分为多个片段(或块),然后使用多个
计算设备(或核心)分别、同时执行这些任务片段,以此达到减少计算耗时的目的。例如:OMP,MPI;
2、多数据:同时对多个数据执行相同操作,例如:CUDA,SIMD.

前提:
1、设备条件:拥有多个计算设备或核心
2、任务条件:任务或数据可划分为多个独立片段

注意:

1、并非所有的任务都适合并行;
2、并行计算代码中包含了额外的管理开销(任务分配与调度、数据同步等),总计算量多于串行代码;
3、并行可以减少计算耗时,但并行代码的耗时也可能多于串行代码(任务并行度太低或代码糟糕);
4、结合实际选择合适的并行方式。

数值计算领域,主要的并行计算方式有三种:

  • 1、单机多核并行:OpenMP
  • 2、多节点并行:MPI
  • 3、GPU设备加速:CUDA/OpenACC
    并行方案很多,此处所列条目并不完整,三种方案可混合使用。MPI也可用于单机,OMP也支持设备加速。
    为了方便并行、减少错误,建议Fortran用户尽量使用纯函数或逐元函数

1、OpenMP(Open Multi-Processing)

现今的处理器一般均包含多个计算核心(例如4核),单台机器(节点)更是可能安装有多个处理器(例如2颗)。串行程序仅能够使用一个处理核心进行计算,最多可使用12.5%的CPU资源。0penMP通过启用多个线程,利用该计算机上的多个核心进行并行,已达到加速计算的目的。

编译器自带OMP库,无需额外安装。

注意:
1、核心数量指CPU的物理核心数(4核8线程的CPU具有4个物理核
心),超线性技术对计算密集型代码没有帮助;
2、OMP线程数一般不大于CPU核数,否则计算速度可能降低;
3、区分CPU线程(客观存在—完全不用关心)与OMP线程(程序设定)。

OpenMP是一种用于共享内存系统(单机)的多线程并行方案,支持C/C++和Fortran语言。它通过在串行程序的适当位置添加指令(或指令对),采用Fork-Join模式,启用多个线程并行执行特定部分的代码。

在这里插入图片描述

优点
1、简单易学,代码改动小(针对规范的代码);
2、可自由切换并行/串行模式;
3、1可在线程间共享内存(变量)节省内存开销,便于线程间数据传递,减少数据同步耗时。

缺点
1、代码不规范容易造成数据竞争,可能需要大幅度修改原有代码;
2、不适合具有复杂的线程间同步和互斥操作的情形(影响效率);
3、不能用在非共享内存系统(如计算机集群),但支持使用主机以外的计算设备;

OMP/MPI适用于粗粒度任务并行(任务并行),CUDA适用于细粒度任务(数据并行)

参考文献

  • 《多核并行高性能计算OpenMP》雷洪,胡许冰
  • 《多核异构并行计算OpenMP4.5 C/C++篇》雷洪
  • 《Parallel Programming in Fortran 95 using OpenMP》----很经典
    Migue丨Hermanns著,李兴旺译(资料较老,内容不多,入门读物)
    中文译本下载地址:
    https: //download.csdn.net/download/lixingwang0913/86545163
    http: //fcode.cn/resource_ebook-24-1.html
  • 官网文档: www. openmp.org/specifications

启用OMP并行
命令行编译。根据编译器不同,添加相应的编译参数:

  • gcc/gfortran-fopenmp(Windows/Linux)
  • ifort /Qopenmp(Windows系统)
  • ifort -fopenmp(Linux系统)
    在这里插入图片描述

2、 MPl (Message Passing Interface)

OpenMP只能在单机上使用,假设你有3台这样的机器(或集群),此时可用MPI实现跨节点并行。MPI在每个节点(机器)启用一个或多个进程来并行执行任务。同样,各节点上进程数一般不
大于该节点的CPU核数。一个进程可以含有多个线程,有时也将MPI和OMP结合使用。

常用MPI库:MS MPI,intel MPI,Open MPI,MPICH等。
MPI函数详情: https://www.open-mpi.org/doc/

3、CUDA (Compute Unified Device Architecture) /OpenACC

CUDA是NVIDIA开发的一个并行计算平台和编程模型,而OpenACC则由多个组织(包括NVIDIA)共同维护,二者均是旨在利用GPU实现并行计算

CUDA适用于NVIDIA卡,目前仅有NVIDIA HPC SDK(包含C和Fortran编译器)支持cuda fortran;

OpenACC同时适用于NVIDIA卡和AMD卡,NVIDIA HPC SDK和GCC(开源免费)均支持。

C/Fortran中OMP标记的说明

为了在并行和串行代码间相互切换(修改编译命令,不修改代码),OMP引入特殊的标记:
在这里插入图片描述

注:标记和语句(或指令)之间必须有空格;Fortran也可使用预处理(大小写敏感)。

C语言

  • OMP库中包含众多函数,无论是否开启OMP,仅需在代码中#incIude “omp. h",即可使用OMP库函数;
  • 2、当关闭OMP选项,#pragma omp所在行被忽略,没有任何作用,生成的程序为串行。

Fortran语言

  • 1、如要使用OMP库函数,须开启OMP并在代码中use omp_lib;
  • 2、当关闭OMP选项,!$和!$omp将被识别为注释,没有任何作用,也不能使用OMP库函数;
    3、选择生成串行代码时,!$和!$omp将被识别为注释,但可以使用OMP库中的部分函数
    (需在代码中use omp_lib)

在这里插入图片描述

OpenMP构造并行域

OMP以Fork-Join模式并行执行特定部分的代码,这些特定代码所组成的动态范围(dynamic extent)称并行域。
以下指令对用于构造一个并行域,包含于其中的代码或任务可被并行执行:
在这里插入图片描述
注:

  • 1、C大小写敏感,Fortran不区分大小写(预处理区分);
  • 2、并行域中的代码不一定并行执行。

OMP代码执行模式

1、启动程序,主线程(线程0)执行代码块1;
2、遇到!$omp parallel后,主线程分裂(fork)为多个线程(此处
线程数为2),每个线程均尝试执行代码块2;
3、各线程遇到!$omp end parallel后,合并(join)为一个线程(主线程),并由主线程执行代码块3;
4、主线程结束程序。

!

文本范围(lexical extent)和动态范围(dynamic extent)

文本范围(lexical extent)

  • parallel指令对之间的语句。

动态范围(dynamic extent)

  • 并行域中所有被执行的语句。

在这里插入图片描述

一个完整的OMP程序,其中可以包含若干个串行区域和若干个并行区域,各个并行域内的线程数可以不同。线程数可由环境变量(或内部控制变量ICV,可认为是默认值)、OMP函数(子程序),并行域的子句等确定。
在这里插入图片描述

设置线程数

  • 1、环境变量OMP_NUM_THREADS
    缺省(默认)线程数一般等于CPU核数

  • 2、子程序omp_set_num_threads
    影响后续所有并行域的线程数

3、子句num_threads

  • 设置当前并行域的线程数

优先级:子句>子程序>环境变量

在这里插入图片描述

常用函数

omp_set_max_active_levels:最大嵌套并行层数;
omp_get_num_procs:获取CPU核心数;
omp_set_num_threads:设置后续所有并行域的线程数;
omp_get_num_threads: 获取函数调用处线程数;
omp_get_thread_num:获取当前线程的线程号。

注:需要 #include"omp.h"或use omp_lib。一般 set 为子程序(无返回值),get 为函数。

在这里插入图片描述
在这里插入图片描述

OMP线程数说明

OMP的线程数可为任意正整数(实际受限,可由omp_get_thread_limit函数查询),但线程数不宜超过CPU物理核心数(CPU的超线程对计算密集型任务无益),
主要原因:

  • 开辟、释放线程比较耗时;
  • 单个核心处理多个线程时会降低效率(线程切换);
  • 线程数过多造成内存和堆栈负担。

!

嵌套并行域

在并行域中再次开辟新的并行域,称嵌套并行域。子程序omp_set_max_active_Ievels用于设置执行时的最大嵌套并行层数n(默认n=1)。当设置n=0时,并行域失效,代码串行执行;n>1
时,可进行嵌套并行。

如果代码中并行域的嵌套层数为m,则实际嵌套并行层数为min (m, n)

注:老版本用omp_set_nested函数开启/关闭嵌套并行;
vs2019/GCC 9.4.0 不支持 omp set max active levels.

不同嵌套层数的执行结果

  • n=1只有最外层的并行域起作用
  • n=2或者3的结果都是一样的,因为嵌套并行域都是2个,所以’regin 3’和‘regin 2’都执行了3*2=6次
    在这里插入图片描述

子句

OMP指令之后常常可以添加一个或多个子句(clause),用以进行补充说明。子句用于控制具体行为、数据属性、或二者兼之。

并行域处的子句有:

  • 并行域开头(#pragma omp parallel/!$omp parallel)可用
    子句有:if, num_threads, default,private, firstprivate, shared, reduction, copyin, proc_bind,
    allocate.
    并行域结尾处(!$omp endparallel)无可用子句,隐含线程同步。
OMP的数据属性

OMP的数据属性可归结为三类:shared、private、threadprivate。
进入并行域时,如果变量具有私有属性(private, firstprivate, lastprivate, reduction, linear),
则会额外开辟nT份(线程数)临时空间来存储数据,并在退出该结构时销毁临时数据。

在这里插入图片描述

if(b)

根据参数b的值判断并行域是否有效,进而决定构建并行或串行代码。
在这里插入图片描述

numthreads (nT)

参数nT为整型变量或表达式(正整数),设置并行域内的线程数。

default(private | firstprivate 丨 shared l(none)

设置并行域内的变量的默认属性,即如果没有显式给定变量的属性,则其属性为默认属性。

参数为上述四种(私有、承前私有、共享、无)中的其中一种(C语言只可选shared | none)。

注意:C语言中,并行域内变量也可有private丨firstprivate属性,只是default子句中仅有两种可选。C可在并行域定义局部变量(私有)。

private(a,b, c… )

变量a,b,c等为私有变量,每个线程都生成变量的私有备份,同一变量在各个线程中可以有不同值。
初始值未知。

firstprivate (a,b,c…)

变量a,b,c等为私有变量,每个线程都生成变量的私有备份,同一变量在各个线程中可以有不同值。
初始值为主线程的值。

6、 shared(a,b, c… )
变量a,b,c等为共享变量,所有线程共享同一片内存(一荣俱荣)。
初始值为主线程的值。

不同数据属性的效果
在这里插入图片描述

reduction(operator:list)

规约:退出并行域时,按照约定的操作将同名变量的多个值处理为一个值。
列表中的变量自动含有private属性。

可用的操作符或函数:

C语言

  • 操作符:+, -, *,&, l,^,&&, ll

Fortran

  • 操作符:++, *, -, .AND., .OR., .EQV., .NEQV.
  • 内置函数:N MAX, Min,IAND,,IOR,IEOR

在这里插入图片描述

操作符对应的初始值
| 操作符 | + | - | * | & | | | ^ | && | || |
|--------|----|----|----|----|-----|----|-----|-----|
| 初始值 | 0 | 0 | 1 | 所有位均为1 | 所有位均为0 | 所有位均为0 | 1 | 所有位均为0 |
| 函数 | | | | .and. | .or. | .eqv. | .true. | .false. |

操作符+-*.and..or..eqv..neqv.
初始值001.true..false..true..false.
函数maxminiandiorieor
初始值该数据类型能够表示的最小值该数据类型能够表示的最大值所有位均为1所有位均为0所有位均为0
copyin

仅用于线程私有(threadprivate)变量,进入并行域时将主线程的值赋值给其他线程。

proc_bind

设置线程与CPU核心之间的绑定关系,极少用。

allocate

指针或动态数组可在并行域分配,将增加代码的复杂度,不
建议使用,可用子过程规避。

指令

指令
在这里插入图片描述

在这里插入图片描述

前期的所有例子,并行域(parallel结构)中的语句会被该并行域中所有线程执行,即相同的代码被多次执行(各线程的执行结果可以不同),未达到并行计算的目的。

为使不同线程执行不同的任务(或同一任务的不同部分),需要在并行域中使用其他OMP指令(结构)将任务分派给各个线程。根据实际情况,任务分派可采用sections、workshare、do、task、taskloop等指令,其中sections结构是最简单、最直接的一种分配方式。

sections

sections结构将代码分割成多个独立代码块(section),每块代码仅被一个线程执行一次。
在这里插入图片描述

特点

  • 1、该结构适用于处理多个完全不同的任务(一边….一边…);
  • 2、section的数量是恒定的(写代码时就确定了),程序运行过程中不会增减(局限性);
  • 3、每个代码块(section)组成一个单独的任务,仅被一个线程执行;
  • 4、一个线程可能执行0,1,n个代码块(由线程和代码块数量决定);
  • 5、紧挨着sections的section可以省略;
  • 6、结构末尾处隐含有线程同步(可用nowait取消同步);
子句

private, firstprivate, lastprivate, reduction, allocate,nowait

  • lastprivate(a,b,c...):变量a,b,c等为私有变量,初始值未知,退出结构时,源变量的值为最后一次更新的结果(可以修改源变量的值)。
  • nowait:置于结构开头©或结尾(Fortran)处,用于取消隐式线程同步,即线程到达该结构的结尾处后无需等待“集合”,继续执行后续任务。

在这里插入图片描述

在这里插入图片描述

workshare

该结构仅适用于fortran语言,将数组相关的整体操作划分为多个工作单元,分派给各个线程并行执行。

!$omp workshare
·..代码块
!$omp end workshare [nowait]

其内操作包括:数值整体赋值、forall和where语句,内置函数MATMUL,DOT_PRODUCT, SUM, PRODUCT, MAXVAL, MINVAL,COUNT, ANY, ALL,SPREAD, PACK, NPACK, RESHAPE,TRANSPOSE,EOSHIFT, CSHIFT,minloc,MAXLOC。

Fortran语言自身具备数组向量化操作能力,编译器会对此类代码进行优化,对于元素较少的数组,或者单元素计算并不十分复杂的情况,没必要使用workshare结构。

在这里插入图片描述

语句操作描述多线程执行方式
b = [1, 0, -2, 5, 3]初始化小数组可能按元素分配给线程执行
forall(i=1:size(a))并行填充 a(i) = i每线程处理若干个 i
sum(a) + maxval(b)并行归约(求和+最大值)多线程局部计算 + 汇总
Thread 0: b(1), a(1), a(6), sum=7
Thread 1: b(2), a(2), a(7), sum=9
Thread 2: b(3), a(3), a(8), sum=11
Thread 3: b(4), a(4), a(9), sum=13
Thread 4: b(5), a(5), a(10), sum=15maxval(b) = 5total s = 55 + 5 = 60

在 !$omp workshare 块中,OpenMP 会自动分析数组操作,并将这些工作平均分配给所有线程。以下是每条语句的线程划分策略。

b = [1, 0, -2, 5, 3]实际行为:
OpenMP 实现可能按元素划分给线程,比如:线程编号	操作
Thread 0	b(1) = 1
Thread 1	b(2) = 0
Thread 2	b(3) = -2
Thread 3	b(4) = 5
Thread 4	b(5) = 3

但是由于开销太小,有些实现会由主线程直接执行全部赋值。

forall(i=1:size(a)) a(i) = i每个线程处理2个元素的赋值
| 线程编号     | 处理 `i` 范围 |
| -------- | --------- |
| Thread 0 | i = 1, 6  |
| Thread 1 | i = 2, 7  |
| Thread 2 | i = 3, 8  |
| Thread 3 | i = 4, 9  |
| Thread 4 | i = 5, 10 |
s = sum(a) + maxval(b)
sum(a) 和 maxval(b) 都是数组归约操作(reduction)
OpenMP 会用多线程并行归约:sum(a):
线程先对它们负责的 a(i) 部分求和,再归并出总和。
| 线程编号     | 求部分和                       |
| -------- | -------------------------- |
| Thread 0 | a(1) + a(6) = 1 + 6 = 7    |
| Thread 1 | a(2) + a(7) = 2 + 7 = 9    |
| Thread 2 | a(3) + a(8) = 3 + 8 = 11   |
| Thread 3 | a(4) + a(9) = 4 + 9 = 13   |
| Thread 4 | a(5) + a(10) = 5 + 10 = 15 |最终汇总:7+9+11+13+15 = 55
maxval(b):
线程分别查看自己负责的 b(i) 值,最后归约出最大值 5

总结:5 个线程如何操作

语句操作描述多线程执行方式
b = [1, 0, -2, 5, 3]初始化小数组可能按元素分配给线程执行
forall(i=1:size(a))并行填充 a(i) = i每线程处理若干个 i
sum(a) + maxval(b)并行归约(求和+最大值)多线程局部计算 + 汇总

single/master

如果并行域中某部分代码仅需执行一次,例如输出进度提示信息、读取模型参数等,有以下几种方案:

  • 构建“并行–串行–并行”模式的代码。并行域的构建和销毁较为耗时,此方案的效率较低;
  • 并行域中构建只有一个代码块的sections结构。不能广播数据(难以将变量在某一线程中的值传递给其他线程);
  • 并行域中使用判断语句,仅让一个线程(一般是主线程)参与运算。不能广播数据;
  • 使用single或master结构。一般,single多用于读取并广播数据,master多用于输出提示。

single结构仅被并行域中的一个线程(任意)执行,末尾处含有隐式线程同步。

在这里插入图片描述

clause: private, firstprivate, allocate
end_clause:copyprivate或者nowait

copyprivate子句写在结构开头(C)或结尾(Fortran)处,作用是将一个私有变量的值传递给其他进程(广播)。

在这里插入图片描述

master结构仅被并行域中的主线程执行,末尾处没有线程同步,无任何子句。主线程执行代码块的同时,其余线程执行后续任务。一般用于输出提示。
在这里插入图片描述

atomic/critical

如果并行域中某部分代码需要被所有线程执行,但不能被多个线程同时执行,例如更新共享变量、读取数据等,可使用

atomic 或critical结构。atomic确保同时只有一个线程修改变量(一般为共享变量),应用于简单的赋值语句:
在这里插入图片描述
atomic-clause: read、 write、 update、 capture
read:仅读取变量x的值v=x;
write:覆盖原有值,x=v;
update:对原有值进行更新, x+=1
capture:更新原有值,并传递给其他变量,v=x++

注意:
v为私有变量,x为共享变量
atomic-clause可以不写,默认update
老版本支持单!$omp atomic指令(Fortran),新版不支持

在这里插入图片描述

atomic主要用于控制共享变量的更新,应用场景较为简单。
对于更加复杂的代码或场景,可使用critical结构。临界区里面可以是任意合法代码。
在这里插入图片描述

临界区特点

  • 1、所有同名的多个临界区被看做同一个临界区;
  • 2、所有未命名的临界区看作同一个临界区;
  • 3、同一时刻,临界区A、B、C...(A、B、C...是多个不同名字的临界区,其中一个可以是无名临界区)里面分别最多只有一个线程;
  • 4、当某个线程到达临界区A入口时,它会在此等待,检索所有同名临界区里是否有其他线程执行;等待前一个线程执行完毕后再进入临界区。末尾处没有线程同步。
    在这里插入图片描述

在这里插入图片描述

flush

flush用于强制刷新变量,使缓存与内存一致,
在这里插入图片描述
例如:线程A对一个共享数组进行操作时,可能需要使用到受线程B所影响那一部分的信息。线程B对相关信息进行操作时使用了缓存,即操作结果在缓存中,并未更新内存中的信息。这种情况下,必须保证要在A读取数组之前执行完内存更新,可采用flush指令。如果指令后不跟变量列表,则刷新所有变量。

barrier

barrier指令在指定位置插入一个阻塞(同步点),所有线程到达此处后,再继续执行后续语句。
在这里插入图片描述

下列结构的结尾处隐含有同步:parallel、 sections、 single、workshare
同步点处隐含有flush。

限制条件:
1、不能用于 sections、single、master、atomic、critical等结构中;
2、其他可引起死锁的情形,如

if(omp_get_thread_num==0)!$OMP BARRIER
end if

for/do

前面介绍的sections结构仅能处理固定的任务片段数(写代码时就给定),workshare主要应对数组整体操作(Fortran)。如果可并行执行的任务数必须在运行时才能确定,且多个任务可通过循环处理,则可采用for/do结构来并行。

指令和循环之间不能有其他语句,尾部隐含同步,可用 nowait 取消。
在这里插入图片描述

该指令的作用是将最近的for/do循环(计数循环)中的有限次迭代按照一定方式分派给并行域中的所有线程,各个线程同时执行分配到的迭代任务。适用于每次迭代计算量差异不大的情形。

可用子句:
private, firstprivate, lastprivate, reduction, schedule,collapse, linear, ordered, allocate, order(concurrent)

注意:任务之间无依赖;只能用于计数循环;执行过程中不能跳出循环。

schedule子句

schedule(kind [, chunk])子句
该子句用于设置循环迭代的分配方式(kind),可选值有:static,dynamic,guided,auto,runtime。chunk为整数,对于不同kind的意义不同。

1、

C: #pragma omp for schedule(static, chunk )
Fortran: !$omp do schedule(static, chunk )

首先将循环迭代划分为多个块,按照线程号顺序,依次将各个块分派给每个线程。分派完成后,再开始执行。
适用于每次迭代的任务量相当的情况。

如未指定可选参数chunk,默认把循环迭代平均分配(不能整除怎么办?),块数等于线程数;如指定了可选参数chunk,则每个块均包含chunk次迭代(其中一个可以小于chunk)。静态方案可能存在分配不均的问题,例如将600次迭代分给3线程:
在这里插入图片描述

2、 schedule(dynamic, chunk
动态分配每个工作块:按照chunk值(缺省值为1)将循环划分为若干个块,并为每个线程分配一个块。当线程完成一个工作块后,为其分配新的工作块,直至所有块分派完毕。此方案适合各块计算耗时差异较大的情形。

动态方法提高了执行能力和效率,但在处理和分配工作块的时候产生了更多的计算冗余。每个工作块越大,冗余越少,但也会扩大各线程的工作量的差异。

3、 schedule(guided, chunk )
动态分配每个工作块:随着计算进行,逐步减少每个块的工作量(通常是将剩余工作量的一半进行分配),但每个块最少包含chunk次迭代(最后一个块除外)。如果未指定chunk,默认为1。

在这里插入图片描述

4、 schedule(auto)
程序自行决定分派方式。

5、 schedule(runtime)
前面的几种方案在编译源代码时就固定了,如果需要在运行时修改分配方式,则可指定为RUNTIME方式,并由环境变量OMP_SCHEDULE决定。

大多数情况(尤其是每次迭代任务相当时)无需使用schedule子句,默认就行。

collapse子句

默认情况下,omp for/do仅将最近的循环并行化,collapse可将n层循环合并为一个大循环进行并行。遵守以下限制条件:

  • 1、n层循环之间不能有其他语句;
  • 2、循环域规则。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

linear子句

表明变量为firstprivate属性,且其值与关联的循环的迭代次序线性相关。

参考

  • C/Fortran多核并行计算
http://www.xdnf.cn/news/1059301.html

相关文章:

  • wireshark过滤器的使用
  • tomcat 配置规范
  • stack 和 queue练习
  • 【面试题001】生产环境中如何排查MySQL CPU占用率高达100%?
  • linux kernel优化之rootfs
  • CANFD加速是什么?和CANFD有什么区别?
  • linux 下 jenkins 构建 uniapp node-sass 报错
  • 使用@SpringJUnitConfig注解开发遇到的空指针问题
  • spring-webmvc @InitBinder 典型用法
  • 《挑战你的控制力!开源项目小游戏学习“保持平衡”开发解析:用HTML+JS+CSS实现物理平衡挑战》​
  • 【51单片机】8. 矩阵LED显示自定义图案、动画
  • 用idea操作git缓存区回退、本地库回退、远程库回退
  • singlefligt使用方法和源码解读
  • 无需公网IP:Termux+手机+内网穿透实现Minecraft远程多人联机
  • Uniapp 中根据不同离开页面方式处理 `onHide` 的方法
  • python3:线程管理进程
  • 前端打断点
  • python校园服务交流系统
  • 第十八天:初级数据库学习笔记2
  • easyexcel基于模板生成报表
  • RabbitMQ七种工作模式
  • 21.加密系统函数
  • macOS版的节点小宝上架苹果APP Store了
  • git的使用——初步认识git和基础操作
  • DeepForest开源程序是用于 Airborne RGB 机器学习的 Python 软件包
  • 使用 Elasticsearch 提升 Copilot 能力
  • [计算机网络] 网络的诞生:协议的认知建立
  • 2025年暑期在线实习项目分享
  • 理解 create 指向的箭头函数
  • 从零Gazebo中实现Cartographer算法建图