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. UDP协议,多道技术,进程,同步与异步,阻塞与非阻塞

    UDP协议 简介 UDP叫做用户数据报协议,是OSI七层参考模型中传输层使用的协议,他提供的是不可靠传输,既它在传输过程 中不保证数据的完整性! 端口号 UDP使用IP地址和端口号进行标识,以此将数据 ...

  2. 论文解读《Bilinear Graph Neural Network with Neighbor Interactions》

    论文信息 论文标题:Bilinear Graph Neural Network with Neighbor Interactions论文作者:Hongmin Zhu, Fuli Feng, Xiang ...

  3. Centos7上安装python3.7

    Centos7安装python3.7 由于python2和python3在很大程度上有些不同,因为需要,就以决定安装python3.x的版本,但由于Centos上自安装的就是pyhton2.6.所以在 ...

  4. 好客租房39-react组件基础总结

    1组件的两种创建方式:函数组件和类组件 2无状态函数组件 负责静态结构展示 3有状态组件 负责更新ui 让页面动起来 4绑定事件注意this指向问题 5使用受控组件创建表单 6完全利用js语言的能够力 ...

  5. 抽象数据类型(ADT)

    抽象数据类型(Abstract Data Type,ADT)是指一个数学模型以及定义在这个模型上的一组操作.抽象数据类型的定义仅仅取决于它的一组逻辑特性,而与它在计算机中的表示和实现无关. 例如,in ...

  6. 【Azure 存储服务】Java Azure Storage SDK V12使用Endpoint连接Blob Service遇见 The Azure Storage endpoint url is malformed

    问题描述 使用Azure Storage Account的共享访问签名(Share Access Signature) 生成的终结点,连接时遇见  The Azure Storage endpoint ...

  7. 对于vjudge在有些网络下无法打开的问题

    因为有些网络会屏蔽vjudge,所以打开 镜像网址 不行再试试这个:最新镜像网址

  8. html5手册语义化标签

    html5手册语义化标签: article section aside hgroup header footer nav time mark figure figcaption contextmenu ...

  9. 关闭StackExchange等平台的privacy收集窗口

    技术背景 当我们打开一个StackExchange页面的时候,经常会出现一个很大的privacy收集窗口,而且不管怎么点都关闭不了,比如像下图这样: 如果屏幕足够大,影响可能也不是很大,但是关键是对于 ...

  10. 基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传

    在基于SqlSugar的开发框架的服务层中处理文件上传的时候,我们一般有两种处理方式,一种是常规的把文件存储在本地文件系统中,一种是通过FTP方式存储到指定的FTP服务器上.这种处理应该由程序进行配置 ...