openmp学习记录

1. OpenMP

OpenMP API 用户指南: https://math.ecnu.edu.cn/~jypan/Teaching/ParaComp/books/OpenMP_sun10.pdf

超算习堂 - OpenMP 编程实训: https://easyhpc.net/problem/programming_lab/2

推荐学习视频: https://www.bilibili.com/video/BV18M41187ZU/?spm_id_from=333.999.0.0

https://www.bilibili.com/video/BV1Ea411X78R/?spm_id_from=333.999.0.0&vd_source=d4d8725a1a30e189fa2cd9218fa9842a

open MP入门

(1)什么是并行?
  • 多个任务同时进行

    • 交错并发(interleaved)
    • 并行(parallel)

请添加图片描述

  • 多任务实现方式

    • 并发(concurrent):

      • 多个进程或线程“同时“进行

      • 交错并发:可通过系统调度由单核完成。

      • 并行:需要多个运算核心同时完成

      • 多处理器vs单处理器多核

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

      请添加图片描述

    • 进程:一个执行中的程序即为一个进程

      • 每个进程有独立的程序计数器(program counter)、堆(heap)、栈(stack)、数据段(data section)、代码段等。

        从上到下:

        stack(局部变量等)—> (相遇则内存不足) <—heap(由malloc、new等动态分配的空间)、data、text/code

      • 程序运行状态由进程控制块(process control block)记录

    请添加图片描述

    CPU在进程之间切换需要进行上下文切换

    • 线程:常被称为轻量级进程(lightweight process)

      • 与进程相似:每个线程有线程ID、程序计数器、寄存器(registers)、栈等。

      • 与进程不同:所有线程共享代码段、数据段及其他系统资源(如文件等)

      请添加图片描述

      • CPU在线程之间切换开销较小

        线程的硬件调度

        硬件和操作系统会自行将线程调度到CPU核心运行。

        当线程数目超过核心数,会出现多个线程抢占一个CPU核心,导致性能下降。

        超线程(hyper-threading)将单个CPU物理核心抽象为多个(目前通 常为2个)逻辑核心,共享物理核心的计算资源。

(2)什么是OpenMP?

一种面向共享内存的多CPU多线程并行编程接口。

API,英文全称Application Programming Interface,翻译为“应用程序编程接口”

(OpenMP是一个应用程序接口(API) ,可以简单理解为它是一个库,支持多种指令集和操作系统)

  • OpenMPOpen Multi-Processing

    • 多线程编程API

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

      • 与CUDA、MPI所支持的硬件比较

    请添加图片描述

  • #pragma

    • 预处理指令

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

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

        #pragma once

        • 指定头文件只被编译一次

        #pragma once

        需要编译器支持、针对物理文件、需要用户保证头文件没有多份拷贝

        (#pragma once一般由编译器提供保证:同一个文件不会被包含多次。这里所说的”同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。)

        #ifndef

        不需要特定编译器

        不针对物理文件

        需要用户保证不同文件的宏名不重复

        请添加图片描述

    • 其他#pragma指令

      • #pragma GCC poison printf(收集不安全的c函数,使用预编译属性禁止这些函数在项目源码中使用,这里是printf)

      • #pragma warning (disable : 4996)

        在程序的最前面加上 #pragma warning (disable:4996) 表示为忽略改警告 。 1. #pragma warning只对当前文件有效(对于.h,对包含它的cpp也是有效的),而不是对整个工程的所有文件有效。 当该文件编译结束,设置也就失去作用。 2. #pragma warning (push) 存储当前报警设置。

    • OpenMp中的并行化声明由#pragma完成

      • 格式:#pragma omp construct(指令) [clause(子句) [clause]...]

        • #pragma omp parallel for
        • 编译器如果不支持该指令则将直接忽略
      • 其作用范围通常为一个代码区块

        #pragma omp parallel for
        for (int i=0; i<10; ++i){ std::cout << i << std::endl; }
        

        #pragma omp parallel关键字创建多线程必须在编译时加上-fopenmp选项,否则起不到并行的效果,

        g++ a.cc -fopenmp
        

        请添加图片描述

        如何使一段代码并行处理呢?omp中使用parallel制导指令标识代码中的并行段,形式为:

        #pragma omp parallel

        {

        每个线程都会执行大括号里的代码

        }

        如果想将for循环用多个线程去执行,可以用for制导语句

        for制导语句是将for循环分配给各个线程执行,这里要求数据不存在依赖

        使用形式为:

        1)#pragma omp parallel for

        for()

        (2)#pragma omp parallel

        {//注意:大括号必须要另起一行

        #pragma omp for

        for()

        }

  • OpenMP环境配置

    • Windows

      • 项目属性 -> C/C++ -> Language -> Open MP Support
    • macOS/Linux

      • 对于支持OpenMP的编译器(gcc 在编译时增加-fopenmp标记)
    • 使用库函数

      • #include<omp.h>
    • 查看OpenMP版本

      • 使用_OPENMP宏定义

        _OPENMP宏可以用来判断OpenMP是否被支持,通过它可以写出任何C语言编译器(即使不支持OpenMP)都可以编译的代码。

        #include<unordered_map>
        #include<string>
        #include<cstdio>
        #include<omp.h>
        int main(int argc,char *argv[])
        {
                std::unordered_map<unsigned,std::string>map{
                        {200205,"2.5"},{200805,"3.0"},
                        {201107,"3.1"},{201307,"4.0"},
                        {201511,"4.5"}};
                printf("OpenMP version:%s.\n",map.at(_OPENMP).c_str());
                return 0;
        }
        
        yang@yang-virtual-machine:~/桌面/openmp$ vim openmp.cpp
        yang@yang-virtual-machine:~/桌面/openmp$ g++ -fopenmp openmp.cpp -o openmp.out
        yang@yang-virtual-machine:~/桌面/openmp$ ./openmp.out
        OpenMP version:4.5.
        

        查看版本方法二:

        yang@yang-virtual-machine:~/桌面$ echo |cpp -fopenmp -dM |grep -i open
        #define _OPENMP 201511
        
  • OpenMP Hello world

    • 通过#pragma omp parallel指明并行部分

    • 无需改变串行代码

      #include <stdio.h>
      #include <omp.h>
      int main()
      {
      #pragma omp parallel
      {
      printf("Hello World\n");
      }
      return 0;
      }
      
      yang@yang-virtual-machine:~/桌面/openmp$ vim helloworld.cpp
      yang@yang-virtual-machine:~/桌面/openmp$ g++ -fopenmp helloworld.cpp -o hello.out
      yang@yang-virtual-machine:~/桌面/openmp$ ./hello.out
      Hello World
      Hello World
      
    • 在输出中添加线程编号omp_get_thread_num()

      #include <stdio.h>
      #include <omp.h>
      int main()
      {
      #pragma omp parallel
      {
      int thread = omp_get_thread_num();
      int max_threads = omp_get_max_threads();
      printf("Hello World (Thread %d of %d)\n", thread, max_threads);
      }
      return 0;
      }
      
      yang@yang-virtual-machine:~/桌面/openmp$ vim hello2.cpp
      yang@yang-virtual-machine:~/桌面/openmp$ g++ -fopenmp hello2.cpp -o hello2.out
      yang@yang-virtual-machine:~/桌面/openmp$ ./hello2.out
      Hello World (Thread 0 of 2)
      Hello World (Thread 1 of 2)
      

      omp_get_max_threads 该函数可以用于获得最大的线程数量,根据OpenMP文档中的规定,这个最大数量是指在不使用num_threads的情况下,OpenMP 可以创建的最大线程数量。

    • 同一线程的多个语句是否连续执行?

      #include <stdio.h>
      #include <omp.h>
      int main()
      {
      #pragma omp parallel
      {
      int thread = omp_get_thread_num();
      printf("hello(%d) ", thread);
      printf("world(%d) ", thread);
      }
      return 0;
      }
      
      yang@yang-virtual-machine:~/桌面/openmp$ vim hello3.cpp
      yang@yang-virtual-machine:~/桌面/openmp$ g++ -fopenmp hello3.cpp -o hello3.out
      yang@yang-virtual-machine:~/桌面/openmp$ ./hello3.out
      hello(1) world(1) hello(0) world(0)
      

      有些人能输出:hello(0) world(0) hello(4) hello(1) world(1) hello(7) hello(3) world(7) world(3) hello(6) world(6) hello(5) world(5) hello(2) world(4) world(2)

  • OpenMP运行机制

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

      • Fork:由主线程(master thread)创建一组从线程(slave threads)
        • 主线程编号永远为0(thread 0)
        • 不保证执行顺序
      • Join:同步终止所有线程并将控制权转移回至主线程

      请添加图片描述

(3)语法

编译器指令

#pragma omp construct [clause [clause]...]{structured block}

  • 指明并行区域及并行方式

  • clause子句

    • 指明详细的并行参数

      – 控制变量在线程间的作用域

      – 显式指明线程数目

      – 条件并行

#pragma omp parallel num_threads(16)
{
int thread = omp_get_thread_num();
int max_threads = omp_get_max_threads();
printf(“Hello World (Thread %d of %d)\n”, thread, max_threads);
}
  • if(scalar_expression):决定是否以并行的方式执行并行区

    • 表达式为真 (非零):按照并行方式执行并行区
    • 否则:主线程串行执行并行区
  • num_threads(int)

    • 用于指明线程数目
    • 当没有指明时,将默认使用OMP_NUM_THREADS环境变量
      • 环境变量的值为系统运算核心数目(或超线程数目)
      • 可以使用omp_set_num_threads(int)修改全局默认线程数
      • 可使用omp_get_num_threads()获取当前设定的默认线程数
      • num_threads(int)优先级高于环境变量
    • num_threads(int)不保证创建指定数目的线程 (系统资源限制)

    设置线程数

    • 优先级由低到高
    • 什么也不做,系统选择运行线程数
    • 设置环境变量export OMP_NUM_THREADS=4
    • 代码中使用库函数void omp_set_num_threads(int)
    • 通过制导语句从句num_threads(integer-expression)
    • if从句判断串行还是并行执行
  • 并行for循环

    • 将循环中的迭代分配到多个线程并行。

      在并行区内对for循环进行线程划分,且for循环满足格式要求

      • init-expr:需要是var=lb形式,类型也有限制
      • test-expr:限制为var relational-opb或者b relational-op var
      • incr-expr:仅限加减法
      #pragma omp parallel
      {
      	int n;
      	for (n = 0; n < 4; n++){
      	int thread = omp_get_thread_num();
      	printf("thread %d \n", thread);
      	}
      }
      
      yang@yang-virtual-machine:~/桌面/openmp$ vim test1.cpp
      yang@yang-virtual-machine:~/桌面/openmp$ g++ -fopenmp test1.cpp -o test1.out
      yang@yang-virtual-machine:~/桌面/openmp$ ./test1.out
      thread 1 
      thread 1 
      thread 1 
      thread 1 
      thread 0 
      thread 0 
      thread 0 
      thread 0 
      
      • 风格1:在并行区域加入#pragma omp for

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

        #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);
        	}
        }
        
      • 风格2:合并为#pragma omp parallel for

        (写法更简洁)

        int n;
        #pragma omp parallel for
        for (n = 0; n < 4; n++) {
        	int thread = omp_get_thread_num();
        	printf("thread %d \n", thread);
        }
        
        yang@yang-virtual-machine:~/桌面/openmp$ vim test2.cpp
        yang@yang-virtual-machine:~/桌面/openmp$ g++ -fopenmp test2.cpp -o test2.out
        yang@yang-virtual-machine:~/桌面/openmp$ ./test2.out
        thread 0 
        thread 0 
        thread 1 
        thread 1 
        #两种风格结果一样
        
        vim test3.cpp
        yang@yang-virtual-machine:~/桌面/openmp$ g++ -fopenmp test3.cpp -o test3.out
        yang@yang-virtual-machine:~/桌面/openmp$ ./test3.out
        thread 1(n=2)
        thread 1(n=3)
        thread 0(n=0)
        thread 0(n=1)
        yang@yang-virtual-machine:~/桌面/openmp$ cat test3.cpp
        #include<stdio.h>
        #include<omp.h>
        int main()
        {
        	#pragma omp parallel
            	{
        		int n;
        		#pragma omp for
        		for(n=0; n<4;n++){
        			int thread= omp_get_thread_num();
        			printf("thread %d(n=%d)\n", thread,n);
        		}
        	}
        	return 0;
        }
        
  • 嵌套并行

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

      • OpenMP默认关闭嵌套,需要使用omp_set_nested(1)打开

        #include<stdio.h>
        #include<omp.h>
        int main()
        {
            omp_set_nested(1);
        	#pragma omp parallel for
        	for (int i = 0; i < 2; i++){
        		int outer_thread = omp_get_thread_num();
        		#pragma omp parallel for
        		for (int j = 0; j < 4; j++){
        			int inner_thread = omp_get_thread_num();
        			printf("Hello World (i = %d j = %d)\n",
                           outer_thread, inner_thread);
        		}
        	}
        }
        
        yang@yang-virtual-machine:~/桌面/openmp$ vim test4.c
        yang@yang-virtual-machine:~/桌面/openmp$ gcc -fopenmp test4.c -o test4.out
        yang@yang-virtual-machine:~/桌面/openmp$ ./test4.out
        Hello World (i = 0 j = 0)
        Hello World (i = 0 j = 0)
        Hello World (i = 0 j = 1)
        Hello World (i = 0 j = 1)
        Hello World (i = 1 j = 0)
        Hello World (i = 1 j = 0)
        Hello World (i = 1 j = 1)
        Hello World (i = 1 j = 1)
        
      • 仍然可以使用fork and join

    请添加图片描述

  • 不能并行的循环

    • 语法限制

      • 不能使用!=作为判断条件

        ​ for (int i = 0; i**!=**8; ++i){

        ​ • error: condition of OpenMP for loop must be a relational comparison (‘<’, ‘<=’, ‘>’, or ‘>=’) of loop variable ‘i’

      • 循环必须为单入口单出口

        • 不能使用break、goto等跳转语句
        • error: ‘break’ statement cannot be used in OpenMP for loop
      • (以上错误提示来自OpenMP 3.1)

    • 数据依赖性

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

      请添加图片描述

(4)同步机制

线程互动与同步

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

竞争条件

  • 语句执行顺序造成结果不一致

    • int a[3]={3,4,5};
      thread 1:a[1]=a[0]+a[1];
      thread 2:a[2]=a[1]+a[2];
      
    • 先执行 thread 1 再执行 thread 2

      • a[1]=a[0]+a[1]=3+4=7; a[2]=a[1]+a[2]=7+5=12;
      • a = { 3, 7, 12}
    • 先执行 thread 2 再执行 thread 1

      • a[2]=a[1]+a[2]=4+5=9; a[1]=a[0]+a[1]=3+4=7;
      • a = { 3, 7, 9 }
  • 高级语言的语句并非原子操作

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

如何保证执行结果正确

  • 临界区(critical section)

    • #pragma omp critical

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

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

    • 比照操作系统中信号量(semaphore)与P、V操作

    #pragma omp critical
    {
    	...
    	critical section;
    	...
    }
    
    Semaphore a;
    P(a);
    ...
    critical section;
    ...
    V(a);
    

    信号量:信号量(Semaphores)的数据结构由一个值value和一个进程链表指针L组成,信号量的代表了资源的数目,链表指针链接了所有等待访问该资源的进程。

    PV操作:通过对信号量S进行两个标准的原子操作(不可中断的操作)wait(S)signa(S),可以实现进程的同步和互斥。这两个操作又常被称为P、V操作,其定义如下:

    **P(S):**①将信号量S的值减1,即S.value=S.value-1;
      ②如果S.value≥0,则该进程继续执行;否则该进程置为等待状态,排入等待队列。
      **V(S):**①将信号量S的值加1,即S.value=S.value+1;
      ②如果S.value>0,则该进程继续执行;否则释放S.L中第一个的等待进程。

    说明:

    • S.value代表可用的资源数目,当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。
    • 一次P操作意味着请求分配一个单位资源,因此S.value减1,当S.value<0时,表示已经没有可用资源,请求者必须等待别的进程释放该类资源,它才能运行下去。
    • 而执行一次V操作意味着释放一个单位资源,因此S.value加1;若S≤0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。
    进程P1
    ...
    P(S);
    临界区;
    V(S);
    ...
    
    • 例子:统计随机数分步(随机产生1000个[0-20)之间的整数,统计每个数字出现的频率)

      /*无临界区*/
      #include<iostream>
      #include<cstdlib>
      #include<omp.h>
      using namespace std;
      int main()
      {
          int histogram[10000];
      	#pragma omp parallel 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]<<" ";
      	}
      	cout<<endl<<"total: "<<total<<endl;
      	return 0;
      }
      
      /*有临界区*/
      #include<iostream>
      #include<cstdlib>
      #include<omp.h>
      using namespace std;
      int main()
      {
          int histogram[20]={0};
              #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]<<" ";
              }
              cout<<endl<<"total: "<<total<<endl;
              return 0;
      }
      
      yang@yang-virtual-machine:~/桌面/openmp$ vim sjswljq.cpp
      yang@yang-virtual-machine:~/桌面/openmp$ g++ -fopenmp sjswljq.cpp -o sjsw.out
      yang@yang-virtual-machine:~/桌面/openmp$ ./sjsw.out
      43 50 53 48 52 45 48 48 56 50 64 45 46 49 45 44 52 58 48 56 
      total: 1000
      yang@yang-virtual-machine:~/桌面/openmp$ vim sjswljq.cpp
      yang@yang-virtual-machine:~/桌面/openmp$ vim sjsljq.cpp
      yang@yang-virtual-machine:~/桌面/openmp$ g++ -fopenmp sjsljq.cpp -o sjsy.out
      yang@yang-virtual-machine:~/桌面/openmp$ ./sjsy.out
      43 50 53 48 52 45 48 48 56 50 64 45 46 49 45 44 52 58 48 56 
      total: 1000
      

    线程多的时候可能出现:

    • 无临界区输出:

      25 31 26 34 40 47 24 29 44 44 31 26 41 38 
      32 45 26 54 45 27
      total: 709
      
    • 有临界区输出:

      60 47 28 54 52 50 33 56 44 53 61 58 43 47 52 
      54 50 52 53 53
      total: 1000
      
  • 原子(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

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

      • 先运行至栅障点处的线程必须等待其他线程
      • 常用来等待某部分任务完成再开始下一部分任务
      • 每个并行区域的结束点默认自动同步线程
      #pragma omp parallel
      {
      	function_A()
      	#pragma omp barrier 
      	function_B();
      }
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3OJOjU0H-1676586121403)(D:\学习学习活动活动\超算第三次考核\图片\栅障.png)]

    • 并行随机数统计及并行求和

      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]++;
      }
      int thread = omp_get_thread_num();
      #pragma omp atomic/*求和时可能其他线程还没完成统计*/
      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];
      }
      

      结果:

      yang@yang-virtual-machine:~/桌面/openmp$ vim test5.c
      yang@yang-virtual-machine:~/桌面/openmp$ gcc -fopenmp test5.c -o test5.out
      yang@yang-virtual-machine:~/桌面/openmp$ ./test5.out
      total=520
      yang@yang-virtual-machine:~/桌面/openmp$ vim test6.c
      yang@yang-virtual-machine:~/桌面/openmp$ gcc -fopenmp test6.c -o test6.out
      yang@yang-virtual-machine:~/桌面/openmp$ ./test6.out
      total=1000
      
    • 下列两段代码结果是否相同?相同

      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];
      }
      /*结果由上面可知:total=1000*/
      
      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];
      }
      /*结果是total=1000*/
      

singal&mater

  • #pragma omp single{}

    • 用于保证{}内的代码由一个线程完成

    • 常用于输入输出或初始化

    • 由第一个执行至此处的线程执行

    • 同样会产生一个隐式栅障

      • 可由#pragma omp single nowait去除

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y6YWU8dg-1676586121403)(D:\学习学习活动活动\超算第三次考核\图片\single.png)]

  • #pragma omp master{}

    • 与single相似,但指明由主线程执行

    • 与使用IF的条件并行等价

      • #pragma omp parallel IF(omp_get_thread_num() == 0) nowait
      • 默认不产生隐式栅障

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YcGOA5hM-1676586121403)(D:\学习学习活动活动\超算第三次考核\图片\master.png)]

    • 在下面代码中与atomic结果相同

      int total = 0;
      #pragma omp parallel
      {
      	#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];
      		}
      	}*/
          //两段结果相同
          /*int thread = omp_get_thread_num();
      	#pragma omp atomic
      	total += histogram[thread];*/
      }
      

并行Reduction

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

    • #pragma omp for reduction(+: total)

    • 支持的操作: +, -, *, & , |, && and ||

    • int total = 0;
      #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];
      	}
      }
      

      会造成冲突的数据是total,我们要对total执行的操作是+,所以在使用Reduction时,我们声明类似为:reduction(+: total)

      reduction执行过程:

      1.fork线程并分配任务

      2.每一个线程定义一个私有变量omp_priv(同private)

      3.各个线程执行计算

      4.所有omp_privomp_in一起顺序进行 reduction,写回原变量

      在这里插入图片描述

(5)变量作用域

变量作用域

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

    串行是最一般的情况,程序会按顺序执行每个任务,效率往往十分低下。

    • OpenMP中必须指明变量为shared或private

      • Shared:变量为所有线程所共享
        • 并行区域外定义的变量默认为shared
      • Private:变量为线程私有,其他线程无法访问
        • 并行区域内定义的变量默认为private
        • 循环计数器默认为private
      int histogram[20];/*shared*/
      init_histogram(histogram);
      int total = 0;/*shared*/
      int i, j;
      #pragma omp parallel for
      for(i=0; i<1000; ++i){/*循环计数器i为private!*/
      	int value = rand()%20;/*private*/
      	#pragma omp atomic
      	histogram[value]++;
      	for(j=0; j<1000; ++j){/*循环计数器j为private!*/}
      }
      
  • 显式作用域定义

    • 显式指明变量的作用域

    • shared(var):指明变量var为shared

    • default(none/shared/private)

      • 指明变量的默认作用域
      • 如果为none则必须指明并行区域内每一变量的作用域
      int a, b = 0, c;
      #pragma omp parallel default(none) shared(b)
      {
      	b += a;
      }
      //error: variable 'a' must have explicitly specified data sharing attributes
      
    • private(var):指明变量var为private

      • int i = 10;
        #pragma omp parallel for private(i)
        for (int j=0; j<4; ++j) {
        	printf("Thread %d: i = %d\n", omp_get_thread_num(), i);
        }
        printf("i = %d\n", i);
        
      • 输出:

        Thread 0: i = 1
        Thread 1: i = 0
        Thread 3: i = 0
        Thread 2: i = 0
        i = 10
        
    • 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);
        
      • 输出:

        Thread 0: i = 10
        Thread 3: i = 10
        Thread 2: i = 10
        Thread 1: i = 10
        i = 10
        
    • lastprivate(var):指明变量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);
        
      • 输出:

        Thread 0: i = 1
        Thread 3: i = 0
        Thread 1: i = 0
        Thread 2: i = 0
        i = 0
        

数据并行与任务并行

  • 数据并行:同样指令作用在不同数据上,前述例子均为数据并行。

  • 任务并行:线程可能执行不同任务

    • #pragma omp sections

    • 每个section由一个线程完成

    • 同样有隐式栅障(可使用nowait去除)

    • #pragma omp parallel
      #pragma omp sections
      {
      	#pragma omp section
      	task_A();
      	#pragma omp section
      	task_B();
      	#pragma omp section
      	task_C();
      }
      //or
      #pragma omp parallel sections
      {
        #pragma omp section
        task_A();
        #pragma omp section
        task_B();
        #pragma omp section
        task_C();
      }
      
    • 在这里插入图片描述

      sections构造:

      将并行区内的代码块划分为多个section分配执行

      可以搭配parallel合成为parallel sections构造

      每个section由一个线程执行

      • 线程数大于section数目:部分线程空闲
      • 线程数小于section数目:部分线程分配多个section
(6)线程调度

线程调度

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

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

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

        • type包括static、dynamic、guided、auto、runtime
        • 默认为static
        #include<stdio.h>
        #include<omp.h>
        int main()
        {
        	#pragma omp parallel for num_threads(4)
        	for (int i=0; i<6; ++i)
        	{
        		int thread = omp_get_thread_num();
        		printf("thread %d\n", thread);
        	}
        
        	return 0;
         } 
        
        thread 0
        thread 0
        thread 2
        thread 3
        thread 1
        thread 1
        
  • Static调度:调度由编译器静态决定

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

    • 每个线程轮流获取 chunk size 个迭代任务

    • 默认chunk size 为 n/threads(没有指定时)在这里插入图片描述

    • 线程的负载可能不均匀

    在这里插入图片描述

  • Dynamic调度

    • 在运行中动态分配任务
    • 迭代任务依然根据chunk size划分成块
    • 线程完成一个chunk后向系统请求下一个chunk
    • 默认chunk size 为1
  • Guided调度

    • 与dynamic类似
    • 但分配的chunk大小在运行中递减
    • 最小不能小于chunk size参数
  • Auto(编译器决定策略)与 runtime(由环境变量指定策略OMP_SCHEDULE

    • – “Note that keywords auto and runtime aren’t adequate.”
  • 总的来说autoruntime其实最后都是在static,dynamic,guided中挑选一个策略。

OpenMP陷阱

  • 当#pragma指令无法为编译器理解时 – 不会报错!
  • #pragma omg parallel不会报错
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/atomic/barrier、nowait
    • 变量作用域:default(none/shared/private), shared(), private(), firstprivate(), last private()
    • 调度:schedule(static/dynamic/guided, [chunk_size])
  • 常用库函数:

    // 设置并行区运行的线程数
    void omp_set_num_threads(int)
    // 获得并行区运行的线程数
    int omp_get_num_threads(void)
    // 获得线程编号
    int omp_get_thread_num(void)
    // 获得openmp wall clock时间(单位秒)
    double omp_get_wtime(void)
    // 获得omp_get_wtime时间精度
    double omp_get_wtick(void)
    
  • collapse(n):应用于n重循环,合并(展开)循环

    • 将多重循环展开到第n重
    • 待展开的循环之间必须没有依赖关系
    • 展开后的顺序与串行顺序一致
    • 相当于增大外层循环的次数,有助于schedule
  • ordered:声明有潜在的顺序执行部分

    • 使用#pragma omp ordered标记顺序执行代码(搭配使用)

    • ordered区内的语句任意时刻仅由最多一个线程执行

    • #pragma omp parallel for [clauses…] ordered//说明并行区中有顺序执行的部分
      {//并行执行1
      #pragma omp ordered
      {} //顺序执行//并行执行2
      }
      
    • orderd加的位置不对,可能导致并行失效

硬件的内存模型

在这里插入图片描述

一些例子
#pragma omp parallel num_threads(8)
{
	int tid = omp_get_thread_num();
	int num_threads = omp_get_num_threads();
	#pragma omp for ordered
	for (int i = 0; i < num_threads; i++) {
		// do something
		// #pragma omp ordered
		// #pragma omp critical
		std::cout << "Hello from thread " << tid << std::endl;
	}
}
#no synchronization
Hello from thread 0Hello from thread 
Hello from thread 4
Hello from thread Hello from thread Hello from thread 7
Hello from thread 1
2
Hello from thread 5
6
3
# ordered
Hello from thread 0
Hello from thread 1
Hello from thread 2
Hello from thread 3
Hello from thread 4
Hello from thread 5
Hello from thread 6
Hello from thread 7
# critical
Hello from thread 5
Hello from thread 4
Hello from thread 1
Hello from thread 7
Hello from thread 6
Hello from thread 3
Hello from thread 2
Hello from thread 0
#include <iostream>
#include <omp.h>
int main() {
	#pragma omp parallel num_threads(8)
	{
		int tid = omp_get_thread_num();
		int num_threads = omp_get_num_threads();
		int max_threads = omp_get_max_threads();
		printf("Hello from thread %d of %d. max= %d\n", tid, num_threads,max_threads);
	}
	return 0;
}
/*输出:
Hello from thread 1 of 8. max= 2
Hello from thread 7 of 8. max= 2
Hello from thread 6 of 8. max= 2
Hello from thread 5 of 8. max= 2
Hello from thread 4 of 8. max= 2
Hello from thread 3 of 8. max= 2
Hello from thread 2 of 8. max= 2
Hello from thread 0 of 8. max= 2
*/
for (int i = 0; i < N; i++) {
for (int j = i; j < N; j++) {
double sum = 0;
for (int k = 0; k < N; k++) {
sum += A[i * N + k] * A[j * N + k];
}
B[i * N + j] = sum;
B[j * N + i] = sum;
}
}
#pragma omp parallel for schedule(runtime)
for (int i = 0; i < N; i++) {
for (int j = i; j < N; j++) {
double sum = 0;
for (int k = 0; k < N; k++) {
sum += A[i * N + k] * A[j * N + k];
}
B[i * N + j] = sum;
B[j * N + i] = sum;
}
}
# export OMP_SCHEDULE="dynamic"
size: 1024
sequence time: 1.46233
omp time: 0.133192
# export OMP_SCHEDULE="static"
size: 1024
sequence time: 1.47874
omp time: 0.219114
//串行
for(int i=0;i<N;i++)
{
	for(int j =0; j <N;j++)
	{
		x_seq[i] +=A[i* N +j]*b[j];
    }
}
//正确并行
#pragma omp parallel for
for(int i;i<N;i++)
{
    double tmp=0;
    for(int j=0;j<N;j++)
    {
        tmp+=A[i*N+j]*b[j];
    }
    x_omp[i]=tmp;
}
//错误并行
//不同核心对同一cache line的同时读写会造成严重的冲突,导致该级缓存失效
#pragma omp parallel for
for(int i=0;i<N;i++)
{
    for(int j=0;j<N;j++)
    {
        x_omp_fs[i]+=A[i*N+j]*b[j];
    }
}
yang@yang-virtual-machine:~/桌面/openmp$ ./fs.out
size: 16384
sequence time: 0.797457
simple omp time: 0.384088
false sharing time: 0.532769
yang@yang-virtual-machine:~/桌面/openmp$ ./fs.out
size: 16384
sequence time: 1.00921
simple omp time: 0.407889
false sharing time: 0.502701
for(int i=0;i<N;i++)
{
	ans_seq +=b[i]*b[i];
}
ans_seq= sqrt(ans_seq);
#pragma omp parallel for reduction(+ : ans_omp)
for(int i=0;i<N;i++)
{
	ans_omp +=b[i]*b[i];
}
ans_omp= sqrt(ans_omp);
#pragma omp parallel for
for(int i=0;i<N;i++)
{
	#pragma omp atomic
	// #pragma omp critical
	ans_omp_sync +=b[i]*b[i];
}
ans_omp_sync= sqrt(ans_omp_sync);
yang@yang-virtual-machine:~/桌面$ ./vector.out
size: 33554432
sequence result: 3344.45
omp result: 3344.45
omp sync result: 3344.45
sequence time: 0.128534
omp time: 0.0634244
omp sync time: 1.71575
yang@yang-virtual-machine:~/桌面$ ./vector.out
size: 33554432
sequence result: 3344.56
omp result: 3344.56
omp sync result: 3344.56
sequence time: 0.126461
omp time: 0.0629478
omp sync time: 2.34646
yang@yang-virtual-machine:~/桌面$ ./vector.out
size: 33554432
sequence result: 3344.52
omp result: 3344.52
omp sync result: 3344.52
sequence time: 0.126658
omp time: 0.0629643
omp sync time: 1.59414
//下面的循环无法正确执行
#pragma omp parallel for
for(k=0;k<100;k++)
{ x=array[k];
array[k]=do_work(x);
}
//正确的方式
//1.直接声明为私有变量
#pragma omp parallel for private(x)
for(k=0;k<100;k++)
{ x=array[k];
array[k]=do_work(x);
}
//2.在parallel 结构中声明变量,这样的变量是++++++++私有的
#pragma omp parallel for
for(k=0;k<100;k++)
{
int x;
x=array[k];
array[k]=do_work(x);
}

超算习堂-OpenMP编程实训

OpenMP介绍
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
    int i;
	#pragma omp parallel for
	for (i = 0; i < 10; i++) {
		printf("i = %d\n", i);
	}
	return 0;
}
fork/join并行执行模式的概念
#include <stdio.h>
#include <time.h>

void foo()
{
	int cnt = 0;
	clock_t t1 = clock();
	int i;
	for (i = 0; i < 1e8; i++) {
		cnt++;
	}
	clock_t t2 = clock();
	printf("Time = %ld\n", t2 - t1);
}

int main(int argc, char* argv[])
{
	clock_t t1 = clock();
	int i;
	#pragma omp parallel for
	for (i = 0; i < 2; i++) {
		foo();
	}
	clock_t t2 = clock();
	printf("Total time = %ld\n", t2 - t1);
	return 0;
}
/*warning: format '%d' expects argument of type 'int', but argument 2 has type 'clock_t' {aka 'long int'}*/
#没有并行
Time = 321759
Time = 320960
Total time = 642780
#并行
Time = 717205
Time = 719696
Total time = 761560
parallel指令的用法
#pragma omp parallel [for | sections] [子句[子句]] { /*并行部分*/ }
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	#pragma omp parallel num_threads(6)
	{
		printf("Thread: %d\n", omp_get_thread_num());
	}
	return 0;
}
for 指令的使用方法
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	#pragma omp parallel 
	{
	    int i, j;
		#pragma omp for
		for (i = 0; i < 5; i++)
			printf("i = %d\n", i);
		#pragma omp for
		for (j = 0; j < 5; j++)
			printf("j = %d\n", j);
	}
	return 0;
}
sections和section指令的用法
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	#pragma omp parallel sections
	{
		#pragma omp section 
		printf("Section 1 ThreadId = %d\n", omp_get_thread_num());
		#pragma omp section
		printf("Section 2 ThreadId = %d\n", omp_get_thread_num());
		#pragma omp section
		printf("Section 3 ThreadId = %d\n", omp_get_thread_num());
		#pragma omp section
		printf("Section 4 ThreadId = %d\n", omp_get_thread_num());
	}
	return 0;
}
private 子句的用法
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	int i = 20;
	#pragma omp parallel for private(i)
	for (i = 0; i < 10; i++)
	{
		printf("i = %d\n", i);
	}
	printf("outside i = %d\n", i);
	return 0;
}
/*private()在for的后面*/
firstprivate子句的用法
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	int t = 20, i;
	#pragma omp parallel for firstprivate(t)
	for (i = 0; i < 5; i++)
	{
		t += i;
		printf("t = %d\n", t);
	}
	printf("outside t = %d\n", t);
	return 0;
}
lastprivate子句的用法
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	int t = 20, i;
	#pragma omp parallel for firstprivate(t), lastprivate(t)
	for (i = 0; i < 5; i++)
	{
		t += i;
		printf("t = %d\n", t);
	}
	printf("outside t = %d\n", t);
	return 0;
}
threadprivate子句的用法

threadprivate子句可以将一个变量复制一个私有的拷贝给各个线程,即各个线程具有各自私有的全局对象。

#include <stdio.h>
#include <omp.h>

int g = 0;
#pragma omp threadprivate(g)

int main(int argc, char* argv[])
{
	int t = 20, i;
	#pragma omp parallel
	{
		g = omp_get_thread_num();
	}
	#pragma omp parallel
	{
		printf("thread id: %d g: %d\n", omp_get_thread_num(), g);
	}
	return 0;
}
thread id: 1 g: 1
thread id: 2 g: 2
thread id: 3 g: 3
thread id: 0 g: 0
shared子句的用法

在并行部分进行写操作时,要求共享变量进行保护,否则不要随便使用共享变量,尽量将共享变量转换为私有变量使用。

#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	int t = 20, i;
	#pragma omp parallel for shared(t)
	for (i = 0; i < 10; i++)
	{
		if (i % 2 == 0)
			t++;
		printf("i = %d, t = %d\n", i, t);
	}
    printf("\nt = %d\n", t);
	return 0;
}
/*并行完t=23、24、25*/
reduction子句的用法

reduction(operator:list)

支持的操作: +, -, *, & , |, && and ||

reduction子句可以对一个或者多个参数指定一个操作符,然后每一个线程都会创建这个参数的私有拷贝,在并行区域结束后,迭代运行指定的运算符,并更新原参数的值。私有拷贝变量的初始值依赖于redtution的运算类型。

#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	
	int i, sum = 10;
	#pragma omp parallel for reduction(+: sum)
	for (i = 0; i < 10; i++)
	{
		sum += i;
		printf("%d\n", sum);
	}
	printf("sum = %d\n", sum);
	return 0;
}
copyin子句的用法

copyin子句可以将主线程中变量的值拷贝到各个线程的私有变量中,让各个线程可以访问主线程中的变量。
copyin的参数必须要被声明称threadprivate,对于类的话则并且带有明确的拷贝赋值操作符。#pragma omp parallel for copyin(g)

#include <stdio.h>
#include <omp.h>

int g = 0;
#pragma omp threadprivate(g) /*copyin的参数必须要被声明称threadprivate*/
int main(int argc, char* argv[])
{
	int i;
	#pragma omp parallel for   
	for (i = 0; i < 4; i++)
	{
		g = omp_get_thread_num();
		printf("thread %d, g = %d\n", omp_get_thread_num(), g);
	}
	printf("global g: %d\n", g);
	#pragma omp parallel for copyin(g)
	for (i = 0; i < 4; i++)
		printf("thread %d, g = %d\n", omp_get_thread_num(), g);
	return 0;
}
static

当parallel for没有带schedule时,大部分情况下系统都会默认采用static调度方式。假设有n次循环迭代,t个线程,那么每个线程大约分到n/t次迭代。这种调度方式会将循环迭代均匀的分布给各个线程,各个线程迭代次数可能相差1次。

#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	int i;
	#pragma omp parallel for schedule(static)
	for (i = 0; i < 10; i++)
	{
		printf("i = %d, thread %d\n", i, omp_get_thread_num());
	}
	return 0;
}
size参数的用法

最小迭代次数。

#pragma omp parallel for schedule(static, 3)
dynamic子句指令的用法
#pragma omp parallel for schedule(dynamic)
omp_get_num_procs

返回调用函数时可用的处理器数目。

omp_get_num_threads

返回当前并行区域中的活动线程个数,如果在并行区域外部调用,返回1。

omp_get_thread_nun

返回当前的线程号。

omp_set_num_threads

设置进入并行区域时,将要创建的线程个数。

omp_in_parallel

可以判断当前是否处于并行状态
函数原型int omp_in_parallel();

是1,否0

omp_get_max_threads

该函数可以用于获得最大的线程数量,根据OpenMP文档中的规定,这个最大数量是指在不使用num_threads的情况下,OpenMP可以创建的最大线程数量。需要注意的是这个值是确定的,与它是否在并行区域调用没有关系。

OpenMP中互斥锁的用法

Openmp中有提供一系列函数来进行锁的操作,一般来说常用的函数的下面4个
void omp_init_lock(omp_lock*) 初始化互斥锁
void omp_destroy_lock(omp_lock*) 销毁互斥锁
void omp_set_lock(omp_lock*) 获得互斥锁
void omp_unset_lock(omp_lock*) 释放互斥锁

#include <stdio.h>
#include <omp.h>
static omp_lock_t lock;
int main(int argc, char* argv[])
{
    int i;
	omp_init_lock(&lock); 
	#pragma omp parallel for   
	for (i = 0; i < 5; ++i)
	{
		omp_set_lock(&lock);
		printf("%d+\n", omp_get_thread_num());
		printf("%d-\n", omp_get_thread_num());
		omp_unset_lock(&lock); 
	}
	omp_destroy_lock(&lock);
	return 0;
}
/*初始化->获得->释放->销毁*/
omp_test_lock

与互斥锁的相关的函数,用来尝试获得锁。可以看作是omp_set_lock的非阻塞版本。

#include <stdio.h>
#include <omp.h>

static omp_lock_t lock;

int main(int argc, char* argv[])
{
    int i;
	omp_init_lock(&lock); 
	#pragma omp parallel for   
	for (i = 0; i < 5; ++i)
	{
		if (omp_test_lock(&lock))
		{
			printf("%d+\n", omp_get_thread_num());
			printf("%d-\n", omp_get_thread_num());
			omp_unset_lock(&lock);
		}
		else
		{
			printf("fail to get lock\n");
		}
	}
	omp_destroy_lock(&lock);
	return 0;
}
omp_set_dynamic

该函数可以设置是否允许在运行时动态调整并行区域的线程数。

void omp_set_dynamic(int)

参数为0时,动态调整被禁用。
当参数为非0值时,系统会自动调整线程以最佳利用系统资源。

#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
    int i;
	omp_set_dynamic(1);
	#pragma omp parallel for
	for (i = 0; i < 4; i++)
	{
		printf("%d\n", omp_get_thread_num());
	}
	return 0;
}
omp_get_dynamic

该函数可以返回当前程序是否允许在运行时动态调整并行区域的线程数。
函数原型:int omp_get_dynamic()
当返回值为非0时表示允许系统动态调整线程。
当返回值为0时表示不允许。

#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
    int i;
	printf("%d\n", omp_get_dynamic());
	omp_set_dynamic(1);
	#pragma omp parallel for
	for (i = 0; i < 4; i++)
	{
		printf("%d\n", omp_get_dynamic());
	}
	return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值