OpenMP入门

前情提要:并行(parallel):需要多个运算核心同时完成

其中有多处理器和单处理器多核两种实现方式,其中差异如下:

  • 同一芯片上的多核通信速度更快
  • 同一芯片上的多核能耗更低

OpenMP初见

OpenMP环境配置

笔者当初刚进入的时候,走了许多弯路,这里给出Dev-Cpp版本的OpenMP的环境配置

点击Tools,再点击里面的Complier Options,在里面的General下的Add the following commands when calling the compiler:加入这段控制语句-fopenmp就可以在Dev-Cpp中编写omp程序

OpenMP作用

  • 编译器指令(#pragma)、库函数、环境变量
  • 极大地简化了C/C++/Fortran多线程编程
  • 并不是全自动并行编程语言(其并行行为仍需由用户定义及控制)

#pragma

预处理指令

  • 设定编译器状态或指定编译器完成特定动作

    • 需要编译器支持相应功能,否则将被忽略

#pragma once

该段代码实现的功能是头文件只被编译一次

#ifdef HEADER_H

#define HEADER_H

...

#endif

上述代码实现的功能也是头文件只被编译一次

#pragma once #ifndef
需要编译器支持 不需要特定编译器
针对物理文件 不针对物理文件
需要用户保证头文件没有多份拷贝 需要用户保证不同文件的宏名不重复

#pragma GCC poison printf

禁止使用printf函数,该段语句本质上就是申明printf被污染了,紧跟在该条语句后面的代码段不能使用printf。

注意:对于形如#pragma的语句,其作用域一般只包含于该语句的下一个代码段,也就是一对大括号之内。

对于#pragma还有许多语法,类似

#pragma startup

#pragma exit

#pragma warning

这些语句对于gcc编译器来说,不能进行正常的编译,直接会被忽视,并不会执行,因此这里不表

总之,对于编译器来说如果不支持#pragma指令则会直接将其忽视,并不会执行

查看OpenMP版本

  • 使用 _OPENMP 宏定义

使用OpenMP编写第一个程序

#include<stdio.h>
#include<omp.h> int main() {
#pragma omp parallel
{
printf("Hello World\n");
}
return 0;
}

我们一般通过 #pragma omp parallel 指明并行部分

线程的编号获取是通过下列语句

omp_get_thread_num();

总共的线程总数获取是通过下列语句

omp_get_max_threads();

同一线程的多个语句一般情况下并不是连续执行,其中可能有其他线程加入,导致该线程阻塞。

OpenMP运行机制

  • 使用分叉(fork)与交汇(join)模型

fork : 由主线程(master thread)创建一组从线程(slave threads)

  • 主线程编号永远为0(thread 0)
  • 不保证执行顺序

join : 同步终止所有线程并将控制权转移回至主线程

fork是各奔东西,join是百川入海

OpenMP语法

编译器指令

#pragma omp parallel [clause[clause]...]{stuctured block}

  • 指明并行区域及并行方式
  • 这里的clause是指明详细的并行参数,其大致分为
    • 控制变量在线程间的作用域
    • 显式指明线程数目
    • 条件并行

num_threads(int)

  • 用于指明线程数目
  • 当没有指明时,将默认使用OMP_NUM_THREADS环境变量
    • 环境变量的值为系统运算核心数目(或超线程数目)
    • 可以使用 omp_set_num_threads(int) 修改全局默认线程数,注意这需要在外面修改,在线程中修改笔者尝试后没有作用
    • 可以使用omp_get_num_threads()获取当前设定的默认线程数
    • num_threads(int)优先级高于环境变量,不保证创建指定数目的线程,会后到系统资源的限制

这里做个区分

并行控制指令 功能
omp_get_num_threads 返回当前并行区域中的活动线程个数,如果在并行区域外部调用,返回1
omp_get_thread_num 返回当前的线程号,注意不要和之前的omp_get_num_threads混肴
omp_set_num_threads 设置进入并行区域时,将要创建的线程个数
omp_get_max_threads 用于获得最大的线程数量,这个最大数量是指在不使用num_threads的情况下,OpenMP可以创建的最大线程数量。需要注意这个值是确定的,与它是否在并行区域调用没有关系

并行for循环

#pragma omp parallel
{
int n;
for(n = 0; n < 4; n++) {
int thread = omp_get_thread_num();
printf("thread %d\n", thread);
}
}

该段语句是每个线程都执行了一次该循环体中的内容,也就是最后的结果会发现每个线程都执行了四次printf("thread %d\n", thread)语句。

如果想要将循环中的迭代分配到多个线程并行,有两种方式,

方式1

#pragma omp parallel
{
int n;
#pragma omp for
for(n = 0; n < 4; n++) {
int thread = omp_get_thread_num();
printf("thread %d]n", thread);
}
}

在并行区域内加入#pragma omp for

注意:在并行区域内,for循环外还可以加入其他并行代码

方式2

int n;
#pragma omp parallel for
for(n = 0; n < 4; n++) {
int thread = omp_get_thread_num();
printf("thread %d\n", thread);
}

合并为#pragma omp parallel for,这样子写法更加简洁

嵌套并行

OpenMP中的每个线程同样可以被并行化为一组线程

  • OpenMP默认关闭嵌套,即默认情况下一个线程不能并行化为一组线程,需要使用omp_set_nested(1)打开
  • 嵌套并行仍然是fork-join模型,注意无论什么时候,主线程的编号一定是0

语法限制

当循环分配并行的时候,有下列限制:

  • 不能使用!=作为判断条件(接受<,<=,>,>=)
  • 循环必须为单入口单出口(也就是不能使用break,goto等跳转语句)

数据依赖性

  • 循环迭代相关(loop-carried dependence)

    • 依赖性与循环相关,去除循环则依赖性不存在
  • 非循环迭代相关(loop-independent dependence)

    • 依赖性与循环无关,去除循环依赖性仍然存在

    如果数据与循环无关,那么普通的循环并行是可以的,但是如果与循环有关,那么并行就会出现问题

    #include<stdio.h>
    #include<omp.h> int main() {
    int a[4] = {1, 2, 3, 4};
    #pragma omp parallel for
    for(int i = 1; i < 4; i++) {
    a[i] = a[i-1] + 2;
    }//这里运行出来的结果不一定是1,3,5,7,这就是循环迭代相关
    return 0;
    }

线程互动与同步

  • OpenMP是多线程共享地址架构

    • 线程可通过共享变量通信
  • 线程及其语句执行具有不确定性
    • 共享数据可能造成竞争条件(race condition)
    • 竞争条件:程序运行的结果依赖于不可控的执行顺序(此时结果是不可测的,即不同的线程执行顺序会有不同的结果)
  • 必须使用同步机制避免竞争条件的出现
    • 同步机制将带来巨大开销
    • 尽可能改变数据的访问方式减少必须的同步次数

高级语言的语句并非原子操作

比如count++并不是只有一个子操作,而是返回count值后再进行++操作。

对于count++其所包含的操作如下

LOAD Reg, count
ADD #1
STORE Reg, count

包含三个子操作,如果一个线程负责count++,另一个线程负责count--,那么就有可能出现下面的情况(竞争条件)

        thread 1        thread 2
Reg count Reg
LOAD 10 10
ADD 11 10
11 10 10 LOAD
11 10 9 SUB
11 9 9 STORE
STORE 11 11

同样如果上述的模板中thread 1执行的是count--,而thread 2执行的是count++,那么最终count存储的是9,这样子就会因为执行顺序不同而造成不同的结果

临界区(critical section)

#pragma omp critical

  • 指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性

    • 同一时间内只有一个线程能执行临界区内代码
    • 其他线程必须等待临界区内线程执行完毕后才能进入临界区
    • 常用来保证对共享数据的访问之间互斥

    如果具有竞争条件,那么就可以使用临界区来保证结果的正确,此时被#pragma omp critical修饰的代码段对于每个线程来说此时里面的资源是它独占的,其他线程无权访问修改,因此可以消除竞争条件带来的不确定性

    #pragma omp parrallel for
    for(int i = 0; i < 1000; i++) {
    int value = rand()%20;
    histogram[value]++;
    }
    int total = 0;
    for(int i = 0; i < 20; i++) {
    total +== histogram[i];
    cout << histogram[i] << endl;
    }
    cout << "total:" << total << endl;

    本段代码的输出未必是1000,原因可以参考前面的count++,++并非原子操作,因此多线程使用的时候会形成竞争条件,导致最后的存储除了问题


解决方案(添加临界区):

#pragma omp parallel for
for(int i = 0; i < 1000; i++) {
int value = rand()%20;
#pragma omp critical
{
histogram[value]++;
}
}
int total = 0;
for(int i = 0; i < 20; i++) {
total += histogram[i];
cout << histogram[i] << endl;
}
cout << "total:" << total << endl;

原子(atomic)操作

#pragma omp atomic

  • 保证对内存的读写更新等操作在同一时间只能被一个线程执行

    • 常用来做计数器、求和等
  • 原子操作通常比临界区执行更快
    • 不需要阻塞其他线程
  • 临界区的作用范围更广,能够实现的功能更复杂

代码演示如下:

#pragma omp parallel for
for(int i = 0; i < 1000; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
}

关于原子操作和临界区的一些总结

指令 功能
临界区 访问临界资源的代码,一个线程进入了临界区,如果第二个线程不会进入临界区那么第二个线程也可以运行
原子操作 原子操作独占处理器,其他线程必须等原子操作完了才可以运行

原子操作是山大王,临界区是此路是我开,此树是我栽

总的来说就是,原子操作时不允许其他线程抢占资,也就是不能中断,但是临界区没有这个硬性规定,只是相关的代码区段此时只能有一个线程执行。

栅障(barrier)

#pragma omp barrier

  • 在栅障点处同步所有线程

    • 先运行至栅障点处的线程必须等待其他线程
    • 常用来等待某部分任务完成再开始下一部分任务
    • 每个并行区域的结束点默认自动同步线程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
#include<omp.h> int main() {
srand(time(NULL));
int total = 0;
int histogram[20];
memset(histogram, 0, sizeof(histogram));
#pragma omp parallel num_threads(20)
{
for(int i = 0; i < 50; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
} int thread = omp_get_thread_num();
#pragma omp atomic
total += histogram[thread];
}
return 0;
}

这里如果没有加入栅障指令,那么最后统计的total一般不是1000,造成数据丢失,因为不能保证执行到total += histogram[thread]对应的数字已经全部统计完毕,其他线程可能仍然在统计

修改方案如下:

int total = 0;

#pragma omp parallel num_threads(20)
{
for(int i = 0; i < 50; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
}
#pragma omp barrier//保证执行到下一个语句之前,所有的线程都已经统计完毕了随机数的次数
int thread = omp_get_thread_num();
#pragma omp atomic
total += histogram[thread];
}

这里给出#pragma omp for的一些相似之处

int total = 0;

#pragma omp parallel num_threads(20)
{
#pragma omp for
for(int i = 0; i < 1000; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
}
int thread = omp_get_thread_num();
#pragma omp atomic
total += histogram[thread];
}

这里的for分配出来的结果仍然是1000,也就是说#pragma omp for也是等到最后的线程执行完循环才进行下一步任务,前面先完成的线程会进入等待状态。注意如果修改成#pragma omp for nowait,那么结果就会不确定,因为此时先完成的线程不会再等待了(产生了一个隐式栅障,但是nowait可以去除)。

single & master

#pragma omp single{}

  • 用于保证{}内的代码由一个线程完成
  • 常用于输入输出或初始化
  • 由第一个执行至此处的线程执行
  • 同样会产生一个隐式栅障
    • 可由#pragma omp single nowait去除

#pragma omp master{}

  • 与single相似,但指明由主线程执行
  • 与使用if的条件并行等价
    • #pragma omp parallel IF(omp_get_thread_num() == 0) nowait,这边的话,笔者暂时没有找到相关的资料,不过可以理解为只有主线程执行该代码块,而其他线程直接跳过,可以执行后面的任务。
    • 默认不产生隐式栅障

#pragma omp master使用如下

int total = 0;
#pragma omp parrallel
{
#pragma omp for
for(int i = 0; i < 1000; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
}
#pragma omp master
{
for(int i = 0; i < 20; i++) {
total += histogram[i];
}
}
}

注意single会产生栅障,但是master不会

single独乐乐,master众乐乐

并行Reduction

指明如何将线程局部结果汇总

  • #pragma omp for reduction(+:total)
  • 支持的操作,八大金刚
    1. +
    2. -
    3. *
    4. &
    5. |
    6. &&
    7. ||
    8. ^
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>
#include<time.h> int main() {
int total = 0;
int histogram[20];
memset(histogram, 0, sizeof(histogram));
#pragma omp parallel
{
#pragma omp for
for(int i = 0; i < 1000; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
}
#pragma omp for reduction(+:total)
for(int i = 0; i < 20; i++) {
total += histogram[i];
}
}
printf("%d\n", total);
return 0;
}

reduction指令就是指明如何将线程局部结果汇总,原先没有reduction限制的时候,total是每个线程共享的,也就是所有的线程都可以访问修改total的值。但是加入reduction限制后,每个线程的total都是独属于该线程的全新total,线程1和线程2的total没有关系,也就是线程1的total的值改变了,不会影响到线程2的total的值。这样每个线程都有自己独特的total,等到所有线程结束完成之后,再根据reduction后面所给的操作符,将所有线程的total经过该运算储存到原来的total中。

变量作用域

OpenMP与串行程序的作用域不同

  • OpenMP中必须指明变量为sharedprivate
Shared Private
变量为所有线程所共享 变量为线程私有,其他线程无法访问
并行区域外定义的变量默认为shared 并行区域内定义的变量默认为private
循环计数器默认为private
int histogram[20];//histogram是shared
memset(histogram, 0, sizeof(histogram));
int total = 0;//total是shared int i,j;
#pragma omp parallel for
for(i = 0; i < 1000; i++) {//循环计数器i是private,虽然定义在并行区域外部
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
for(j = 0; j < 1000; j++) {//循环计数器j也是private
...
}
}

显式作用域定义

  • 显式指明变量的作用域
  • shared(var)
    • 指明变量varshared
  • default(none/shared/private)
    • 指明变量的默认作用域
    • 如果为none则必须指明并行区域内每一变量的作用域
    int a, b, c;
    ...//init a, b, c
    #pragma omp parallel default(shared) shared(a)
    {
    b += a;
    }

    这里会报错,因为b并没有指定是private还是shared,注意只要default中选择的是none,那么并行体中出现的变量就都要说明是shared还是private类型。

    • 注意在C/C++中只存在none和shared两种选择,对于Fortran,可以多一个private参数选择
    int sum = 0;
    #pragma omp parallel for
    for(int i = 0; i < 20; i++) {
    sum += i;
    }

    这里连续和的答案不确定,也是因为sum是shared,shared本质上就是说所有线程的sum指向的都是同一块内存空间

    • 注意:循环计数器变量无论前面是shared还是private都是private,否则会造成逻辑上的错误
  • private(var)
    • 指明变量var为private
    int i = 10;
    #pragma omp parallel for
    for(int j = 0; j < 4; j++) {
    printf("Thread %d: i = %d\n", omp_get_thread_num(), i);
    }
    printf("i = %d\n", i);

    这里线程中输出的i的值都是未定义的,因为此时i是private,也就是说,对于每个线程来说,他们的i都是相互独立的,拥有自己的空间。所以对于每个线程,他们的i都需要重新初始化。

  • firstprivate(var)
    • 指明变量var为private,同时表明该变量使用master thread中变量值初始化
    int i = 10;
    #pragma omp parallel for firstprivate(i)
    for(int j = 0; j < 4; j++) {
    printf("Thread %d: i = %d\n", omp_get_thread_num(), i);
    }
    printf("i = %d\n", i);

    这里输出的结果i都是10,也就是说firstprivate(var)的作用就是仍然申明i是private,不过此时不需要我们显式初始化,而是隐式的初始化,初始化的值是主线程中的i的值。

  • lastprivate(var)
    • 指明变量为private,同时表明结束将最后一层结果赋予该变量
    int i = 10;
    #pragma omp parallel for lastprivate(i)
    for(int j = 0; j < 4; j++) {
    printf("Thread %d: i = %d\n", omp_get_thread_num(), i);
    }
    printf("i = %d\n", i);

    这边会发现程序运行的结果,i的结果和最后一个线程,在这边是线程3中的i一致。所以我们可以得出lastprivate(var)的功能就是将最后一个线程中的变量的值在join后赋值给原先主线程的。

数据并行和任务并行

数据并行

  • 同样指令作用在不同数据上
  • 之前所举的例子都是数据并行

任务并行

  • 线程可能执行不同任务

    • #pragma omp section
  • 每个section由一个线程完成
  • 同样有隐式栅障(可使用nowait去除)

方式1:

#paragma omp parallel

#pragma omp sections
{
#pragma omp section
task_A();
#pragma omp section
task_B();
#pragma omp section
task_C();
}

方式2:

#pragma omp parallel sections
{
#pragma omp section
task_A();
#pragma omp section
task_B();
#pragma omp section
task_C();
}

#pragma omp sections的语法如上,此时里面的每个#pragma omp section都只会由一个线程来执行,如果没有加nowait,那么直到最后一个任务完成,所有的线程才能继续往下,否则将在这里等待,换句话说sections指令有隐式栅障。自测程序如下,加上nowait和不加输出结果是不同的。

#include<stdio.h>
#include<omp.h> int main() {
#pragma omp parallel
{
#pragma omp sections //nowait//这里证明了sections自带隐式栅障
{
#pragma omp section
{
int thread = omp_get_thread_num();
printf("Thread %d : tastk 1\n", thread);
}
#pragma omp section
{
int thread = omp_get_thread_num();
printf("Thread %d : task 2\n", thread);
}
#pragma omp section
{
int thread = omp_get_thread_num();
printf("Thread %d : task 3\n", thread);
}
}
int thread = omp_get_thread_num();
printf("Thread %d : task 4\n", thread);
}
return 0;
}

线程调度

当迭代数多于线程数时,需要调度线程

  • 某些线程将执行多个迭代

  • #pragma omp parallel for schedule(type,[chunk size])

    • type包括static,dynamic,guided,auto,runtime
    • 默认为static
    调度方式 作用&缺陷
    static 线程的负载可能不均匀
    dynamic 在运行中动态分配任务;迭代任务依然根据chunk size划分成块;线程完成一个chunk后向系统请求下一个chunk
    guided 与dynamic类似;但分配的chunk大小在运行中递减,最小不能小于chunk size参数
    auto&runtime 编译器决定策略与环境变量指定策略

总的来说autoruntime其实最后都是在static,dynamic,guided中挑选一个策略。所以理解这三种调度方式最为重要。

static

  • #pragma omp parallel for schedule(static)

    • 没有指定chunk size的参数,那么默认就是迭代数除以线程总数作为size,然后给每个线程分配这么多的任务
  • #pragma omp parallel for schedule(static, 5)
    • 指定了size的大小,那么就是每次给线程的任务大小就是size,注意这边每个线程执行的总任务数仍然是相等的,只不过一次执行的块的大小变化了(size小的时候),如果size过大,那么就会产生某个线程执行了非常多的任务,而其他线程执行很少或者不执行的现象。

dynamic

  • #pragma omp parallel for schedule(dynamic)

    • 没有指定size,size默认为1,也就是此时每个任务块蕴含的任务数量为1,然后逐个分配给所有线程,运行快的线程可以先接收下一个任务,实现任务的动态分配。
  • #pragma omp parallel for schedule(dynamic, 5)
    • 指定了size的大小,与不指定的区别只在于此时一个任务块含有的任务数量为5,每次调度都是5,而不是1

guided

  • #pragma omp parallel for schedule(guided)

    • 没有指定size的情况下,默认为1,与 dynamic类似,不过此时分配的任务数量不是固定不变,而是一开始很多,然后逐渐减小,但是不会小于chunk size的数量
  • #pragma omp parallel for schedule(guided, 5)
    • 指定了chunk size的数量,也就是说最后分配的任务数量不能小于这个值,其他的与guided是一样的

OpenMP小结

软硬件环境

  • CPU多线程并行库

    • 编译器指令、库函数、环境变量
  • 共享内存的多核系统

基础语法

老祖宗:#pragma omp construct [clause[clause]...]{structured block}

分类 语法
指明并行区域 #pragma omp parallel
循环 #pragma omp (parallel) for
嵌套 omp_set_nested(1)
常用函数 omp_get_thread_num(),num_threads(int)
同步 #pragma omp critical,#pragma omp atomic,#pragma omp barrier,nowait
变量作用域 default(none/shared/private),shared(),private(),firstprivate(),lastprivate()
调度 schedule(static/dynamic/guided,[chunk_size])

文末资源分享:

OpenMP常见的32个陷阱

OpenMP Reference Guide

OpenMP入门的更多相关文章

  1. OpenMP入门教程(三)

    承接前面两篇,这里直接逐一介绍和使用有关OpenMP的指令和函数 Directives 1.for 作用:for指令指定紧随其后的程序的循环的迭代必须由团队并行执行,只是假设已经建立了并行区域,否则它 ...

  2. OpenMP 入门

    OpenMP 入门 简介 OpenMP 一个非常易用的共享内存的并行编程框架,它提供了一些非常简单易用的API,让编程人员从复杂的并发编程当中释放出来,专注于具体功能的实现.openmp 主要是通过编 ...

  3. OpenMP 入门教程

    前两天(其实是几个月以前了)看到了代码中有 #pragma omp parallel for 一段,感觉好像是 OpenMP,以前看到并行化的东西都是直接躲开,既然躲不开了,不妨研究一下: OpenM ...

  4. 并行计算之OpenMP入门简介

    在上一篇文章中介绍了并行计算的基础概念,也顺便介绍了OpenMP. OpenMp提供了对于并行描述的高层抽象,降低了并行编程的难度和复杂度,这样程序员可以把更多的精力投入到并行算法本身,而非其具体实现 ...

  5. openmp入门总结

    Ref: https://wdxtub.com/2016/03/20/openmp-guide/ 简介 这门课作为 ECE 中少有的跟计算机科学相关的课,自然是必上不可.不过无论是 OpenMP 还是 ...

  6. [转]OpenMP 入门指南

    简介 这门课作为 ECE 中少有的跟计算机科学相关的课,自然是必上不可.不过无论是 OpenMP 还是 CUDA,对于平时极少接触并行编程的我来说,都是十分吃力的,第一次作业的 OpenMP 编程已经 ...

  7. OpenMP入门教程(二)

    OpenMP API概述 OpenMP由三部分组成: 编译指令(19) 运行时库程序(32) 环境变量(9) 后来的API包含同样的三个组件,只是三者的数量都有所增加. 编译器指令 OpenMP编译器 ...

  8. OpenMP入门教程(一)

    什么是OpenMP Open Multi-Processing的缩写,是一个应用程序接口(API),可用于显式指导多线程.共享内存的并行性. 在项目程序已经完成好的情况下不需要大幅度的修改源代码,只需 ...

  9. openMP多线程编程

    OpenMP(Open Muti-Processing) OpenMP缺点: 1:作为高层抽象,OpenMp并不适合需要复杂的线程间同步和互斥的场合: 2:另一个缺点是不能在非共享内存系统(如计算机集 ...

随机推荐

  1. 为什么 Redis 要有哨兵机制?

    作者:小林coding 计算机八股文刷题网站:https://xiaolincoding.com 大家好,我是小林. 这次聊聊,Redis 的哨兵机制. 提纲 为什么要有哨兵机制? 在 Redis 的 ...

  2. 用更云原生的方式做诊断|大规模 K8s 集群诊断利器深度解析

    背景 通常而言,集群的稳定性决定了一个平台的服务质量以及对外口碑,当一个平台管理了相当规模数量的 Kubernetes 集群之后,在稳定性这件事上也许会"稍显被动". 我们可能经常 ...

  3. Spring Security开发安全的REST服务

    第1章 课程导学 项目介绍 Java实战:Spring Security开发安全的REST服务,来自慕客网的视频 ,主要讲认证和授权. 企业级的认证和授权 从0开始实现一个可重用的,企业级的,认证和授 ...

  4. CentOS7软件环境

    一.软件环境 1.1 centos7 [root@centos7 ~]# cat /etc/redhat-release CentOS Linux release 7.4.1708 (Core) [r ...

  5. 【系统】Windows相关软件下载

    Windows相关软件下载 Visual Studio Windows SQL Server https://my.visualstudio.com/Downloads/Featured?mkt=zh ...

  6. 以点类 Point 及平面图形类 Plane 为基础设计圆类 Circle

    学习内容:以点类 Point 及平面图形类 Plane 为基础设计圆类 Circle 代码示例: import java.util.Scanner; class Point2{ private dou ...

  7. IIS7 网站发布常见报错问题解决方案汇总

    本文实例为大家分享了IIS7 网站发布常见问题,以及五种问题的解决方法,供大家参考,具体内容如下: 1.不是有效的Win32位应用程序 : 解决方案: 1).进入应用程序池=>选中网站=> ...

  8. LC T668笔记 & 有关二分查找、第K小数、BFPRT算法

    LC T668笔记 [涉及知识:二分查找.第K小数.BFPRT算法] [以下内容仅为本人在做题学习中的所感所想,本人水平有限目前尚处学习阶段,如有错误及不妥之处还请各位大佬指正,请谅解,谢谢!] !! ...

  9. 获得MySQL数据库存放位置

    更新记录 2022年6月13日 发布. 2022年6月11日 开始. 通过查看MySQL与存储目录相关的参数 show variables like '%dir%'; 通过查询后datadir参数的值 ...

  10. ansible环境安装及数据恢复

    配置免密登录服务器及下载备份文件#!/bin/bash BACKUP=192.168.30.233 #一行写一个IP BACKUP_PASSWD="lxzl_root*#2021" ...