openmp

并行开发

标准并行模式执行代码的基本思想是,

  • 程序开始时只有一个主线程,
  • 程序中的串行部分都由主线程执行,
  • 并行的部分是通过派生其他线程来执行,
  • 但是如果并行部分没有结束时是不会执行串行部分的

使用

1.头文件

#include <omp.h>

2.cmake

add_link_options(-fopenmp)
add_executable(test main.cpp)
FIND_PACKAGE(OpenMP REQUIRED)
if (OPENMP_FOUND)
    message("OPENMP FOUND")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
endif ()

指令格式

在C++中,OpenMP的指令格式为:

pragma omp 指令 [子句[子句]]

**例如: **

#pragma omp parallel private(i, j)

parallel 就是指令, private是子句

1. OpenMP的指令

OpenMP的指令有以下一些:(常用的已标黑)

  • parallel,用在一个代码段之前,表示这段代码将被多个线程并行执行
  • for,用于for循环之前,将循环分配到多个线程中并行执行,必须保证每次循环之间无相关性。
  • parallel for, parallel 和 for语句的结合,也是用在一个for循环之前,表示for循环的代码将被多个线程并行执行。
  • sections,用在可能会被并行执行的代码段之前
  • parallel sections,parallel和sections两个语句的结合
  • critical,用在一段代码临界区之前
  • single,用在一段只被单个线程执行的代码段之前,表示后面的代码段将被单线程执行。
  • flush,
  • barrier,用于并行区内代码的线程同步,所有线程执行到barrier时要停止,直到所有线程都执行到barrier时才继续往下执行。
  • atomic,用于指定一块内存区域被制动更新
  • master,用于指定一段代码块由主线程执行
  • ordered, 用于指定并行区域的循环按顺序执行
  • threadprivate, 用于指定一个变量是线程私有的。

例子1:

#include <iostream>
#include "omp.h"
using namespace std;
int main(int argc, char **argv) {
	//设置线程数,一般设置的线程数不超过CPU核心数,这里开4个线程执行并行代码段
	omp_set_num_threads(4);
#pragma omp parallel
	{
		cout << "Hello" << ", I am Thread " << omp_get_thread_num() << endl;
	}
}

结果1:

Hello, I am Thread 1
Hello, I am Thread 0
Hello, I am Thread 2
Hello, I am Thread 3

例子2:(带for的指令)

#include <iostream>
#include "omp.h"

using namespace std;

int main() {
	omp_set_num_threads(4);
#pragma omp parallel
        for (int i = 0; i < 3; i++)
		printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
	getchar();
}

结果2:

i = 0, I am Thread 0
i = 1, I am Thread 0
i = 2, I am Thread 0
i = 0, I am Thread 0
i = 0, I am Thread 0
i = 0, I am Thread 0
i = 1, I am Thread 0
i = 2, I am Thread 0
i = 1, I am Thread 0
i = 2, I am Thread 0
i = 1, I am Thread 0
i = 2, I am Thread 0

例子3:

#include <iostream>
#include "omp.h"

using namespace std;

int main() {
	omp_set_num_threads(4);
#pragma omp parallel for
	for (int i = 0; i < 3; i++)
		printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
	getchar();
}

结果3:

i = 0, I am Thread 0
i = 1, I am Thread 1
i = 2, I am Thread 2

注意注意:例子三和例子二的区别和不同

2. OpenMP的常用库函数

*omp_get_num_procs()// 返回运行本线程的多处理机的处理器个数。

*omp_get_num_threads()// 返回当前并行区域中的活动线程个数。

*omp_get_thread_num()// 返回线程号

omp_set_num_threads()// 设置并行执行代码时的线程个数

*omp_init_lock()// 初始化一个简单锁

*omp_set_lock()// 上锁操作

*omp_unset_lock()// 解锁操作,要和omp_set_lock函数配对使用。

omp_destroy_lock()// omp_init_lock()函数的配对操作函数,关闭一个锁

3. OpenMP的子句

*private	# 指定每个线程都有它自己的变量私有副本。

firstprivate	#指定每个线程都有它自己的变量私有副本,并且变量要被继承主线程中的初值。

lastprivate	#主要是用来指定将线程中的私有变量的值在并行处理结束后复制回主线程中的对应变量。

*reduce	#用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的运算。

nowait	#忽略指定中暗含的等待

num_threads	#指定线程的个数

*schedule	#指定如何调度for循环迭代

shared	#指定一个或多个变量为多个线程间的共享变量

ordered	#用来指定for循环的执行要按顺序执行

copyprivate	#用于single指令中的指定变量为多个线程的共享变量

copyin	#用来指定一个threadprivate的变量的值要用主线程的值进行初始化。

*default	#用来指定并行处理区域内的变量的使用方式,缺省是shared

例子

1.使用 num_threads 控制线程的数量

#include <iostream>
int main()
{
  #pragma omp parallel num_threads(5) 
  {
    std::cout << "Hello World!\n";
  }
}

2.在 for 循环中使用 parallel for 指令进行并行处理

int main( )
{
int a[1000000], b[1000000]; 
// ... some initialization code for populating arrays a and b; 
int c[1000000];
#pragma omp parallel for 
for (int i = 0; i < 1000000; ++i)
  c[i] = a[i] * b[i] + a[i-1] * b[i+1];
// ... now do some processing with array c
 }

3.理解 omp_get_wtime

#include <omp.h>
#include <math.h>
#include <time.h>
#include <iostream>
 
int main(int argc, char *argv[]) {
    int i, nthreads;
    clock_t clock_timer;
    double wall_timer;
    double c[1000000]; 
    for (nthreads = 1; nthreads <=8; ++nthreads) {
        clock_timer = clock();
        wall_timer = omp_get_wtime();
#pragma omp parallel for private(i) num_threads(nthreads)
        for (i = 0; i < 1000000; i++) 
          c[i] = sqrt(i * 4 + i * 2 + i); 
        std::cout << "threads: " << nthreads <<  " time on clock(): "
            << (double) (clock() - clock_timer) / CLOCKS_PER_SEC
           << " time on wall: " <<  omp_get_wtime() - wall_timer << "\n";
    }
}
//可以通过不断增加线程的数量来计算运行内部 for 循环的时间。
//omp_get_wtime API 从一些任意的但是一致的点返回已用去的时间,以秒为单位。
//因此,omp_get_wtime() - wall_timer 将返回观察到的所用时间并运行 for 循环。
//clock() 系统调用用于预估整个程序的处理器使用时间,也就是说,将各个特定于线程的处理器使用时间相加,然后报告最终的结果。

4.临界区

  • optional section name 是一个全局标识符,

  • 在同一时间,两个线程不能使用相同的全局标识符名称运行临界区段。

#pragma omp critical (section1)
{
	myhashtable.insert("key1", "value1");
} 
// ... other code follows
#pragma omp critical (section1)
{
	myhashtable.insert("key2", "value2");
}

5.锁

  • omp_init_lock:此 API 必须是第一个访问 omp_lock_t 的 API,并且要使用它来完成初始化。注意,在完成初始化之后,锁被认为处于未设置状态。
  • omp_destroy_lock:此 API 会破坏锁。在调用该 API 时,锁必须处于未设置状态,这意味着您无法调用 omp_set_lock 并随后发出调用来破坏这个锁。
  • omp_set_lock:此 API 设置 omp_lock_t,也就是说,将会获得互斥。如果一个线程无法设置锁,那么它将继续等待,直到能够执行锁操作。
  • omp_test_lock:此 API 将在锁可用时尝试执行锁操作,并在获得成功后返回 1,否则返回 0。这是一个非阻塞 API, 也就是说,该函数不需要线程等待就可以设置锁。
  • omp_unset_lock:此 API 将会释放锁。
#include <openmp.h> 
#include "myqueue.h"
 
class omp_q : public myqueue<int> { 
public: 
   typedef myqueue<int> base; 
   omp_q( ) { 
      omp_init_lock(&lock);
   }
   ~omp_q() { 
       omp_destroy_lock(&lock);
   }
   bool push(const int& value) { 
      omp_set_lock(&lock);
      bool result = this->base::push(value);
      omp_unset_lock(&lock);
      return result;
   }
   bool trypush(const int& value) 
   { 
       bool result = omp_test_lock(&lock);
       if (result) {
          result = result && this->base::push(value);
          omp_unset_lock(&lock);
      } 
      return result;
   }
   // likewise for pop 
private: 
   omp_lock_t lock;
};

6.嵌套锁

OpenMP 提供的其他类型的锁为 omp_nest_lock_t 锁的变体。它们与 omp_lock_t 类似,但是有一个额外的优势:已经持有锁的线程可以多次锁定这些锁。每当持有锁的线程使用 omp_set_nest_lock 重新获得嵌套锁时,内部计数器将会加一。当一个或多个对 omp_unset_nest_lock 的调用最终将这个内部锁计数器重置为 0 时,就会释放该锁。下面显示了用于 omp_nest_lock_t 的 API:

  • omp_init_nest_lock(omp_nest_lock_t\* ):此 API 将内部嵌套计数初始化为 0
  • omp_destroy_nest_lock(omp_nest_lock_t\* ):此 API 将破坏锁。使用非零内部嵌套计数对某个锁调用此 API 将会导致出现未定义的行为。
  • omp_set_nest_lock(omp_nest_lock_t\* ):此 API 类似于 omp_set_lock,不同之处是线程可以在已持有锁的情况下多次调用这个函数。
  • omp_test_nest_lock(omp_nest_lock_t\* ):此 API 是 omp_set_nest_lock 的非阻塞版本。
  • omp_unset_nest_lock(omp_nest_lock_t\* ):此 API 将在内部计数器为 0 时释放锁。否则,计数器将在每次调用该方法时递减。

一开始就创建 8 个线程。对于这 8 个线程,只需使用三个线程执行 pragma omp sections 代码块中的工作。在第二个区段中,您指定了输出语句的运行顺序。这就是使用 sections 编译指示的全部意义。如果需要的话,您可以指定代码块的顺序。

int main( )
{
  #pragma omp parallel
  {
    cout << "All threads run this\n";
    #pragma omp sections
    {
      #pragma omp section
      {
        cout << "This executes in parallel\n";
      }
      #pragma omp section
      {
        cout << "Sequential statement 1\n";
        cout << "This always executes after statement 1\n";
      }
      #pragma omp section
      {
        cout << "This also executes in parallel\n";
      }
    }
  }
}
//All threads run this
//All threads run this
//All threads run this
//All threads run this
//All threads run this
//All threads run this
//All threads run this
//All threads run this
//This executes in parallel
//Sequential statement 1
//This also executes in parallel
//This always executes after statement 1

7.firstprivate 指令

可以将线程中的变量初始化为它在主线程中的任意值。

#include <stdio.h>
#include <omp.h>
 
int main()
{
  int idx = 100;
  #pragma omp parallel firstprivate(idx)
  {
    printf("In thread %d idx = %d\n", omp_get_thread_num(), idx);
  }
}

8.lastprivate 指令

现在将使用最后一次循环计数生成的数据同步主线程的数据

#include <stdio.h>
#include <omp.h>
 
int main()
{
  int idx = 100;
  int main_var = 2120;
 
  #pragma omp parallel for private(idx) lastprivate(main_var)
  for (idx = 0; idx < 12; ++idx)
  {
    main_var = idx * idx;
    printf("In thread %d idx = %d main_var = %d\n",
      omp_get_thread_num(), idx, main_var);
  }
  printf("Back in main thread with main_var = %d\n", main_var);
}