OpenMP模式下多线程文件操作(二)
1. 引言
在并行区域内,若多个线程共同访问同一个存储单元,并且至少会有一个线程更新数据单元中的内容时,会发生数据竞争。本节的数据共享和私有化对数据竞争做一个初步探讨,后续会涉及同步、互斥的内容。
2. 并行区域内的变量的共享和私有
除了以下三种情况外,并行区域中的所有变量都是共享的:
> 并行区域中定义的变量
> 多个线程用来完成循环的循环变量
> private、firstprivate、lastprivate、reduction修饰的变量
例如,
- #include <iostream>
- #include <omp.h>
- int main()
- {
- int share_a = 0; // 共享变量
- int share_to_private_b = 1;
- #pragma omp parallel
- {
- int private_c = 2;
- //通过private修饰后在并行区域内变为私有变量
- #pragma omp for private(share_to_private_b)
- for(int i = 0; i < 10; ++i)
- {//该循环变量是私有的,若为两个线程,则一个执行0<=i<5,另一个执行5<=i<10
- std::cout << i << std::endl;
- }
- }
- return 0;
- }
3. 共享与私有变量声明的方法
private(val1, val2, ...) 并行区域中变量val是私有的,即每个线程拥有该变量的一个copy
firstprivate(val1, val2, ...) 与private不同,每个线程在开始的时候都会对该变量进行一次初始化
lastprivate(val1, val2, ...) 与private不同,并发执行的最后一次循环的私有变量将会copy到val
shared(val1, val2, ...) 声明val是共享的
4. private示例
如果使用private,无论该变量在并行区域外是否初始化,在进入并行区域后,该变量均不会初始化。
在VS2010下,会因为private所导致的私有变量未初始化而出现错误。例如:
- #include <iostream>
- #include <omp.h>
- int main()
- {
- //通过private修饰该变量之后在并行区域内变为私有变量,进入并行
- //区域后每个线程拥有该变量的拷贝,并且都不会初始化
- int shared_to_private = 1;
- #pragma omp parallel for private(shared_to_private)
- for(int i = 0; i < 10; ++i)
- {
- std::cout << shared_to_private << std::endl;
- }
- return 0;
- }
- F5调试由于变量shared_to_rivate未初始化而崩掉。
5. firstprivate示例
- #include <iostream>
- #include <omp.h>
- int main()
- {
- //通过firstprivate修饰该变量之后在并行区域内变为私有变量,
- //进入并行区域后每个线程拥有该变量的拷贝,并且会初始化
- int share_to_first_private = 1;
- #pragma omp parallel for firstprivate(share_to_first_private)
- for(int i = 0; i < 10; ++i)
- {
- std::cout << ++share_to_first_private << std::endl;
- }
- return 0;
- }
运行程序,可以看到每个线程对应的私有变量share_to_first_private都初始化为1,并且每次循环各自增加1.
6. lastprivate示例
- #include <iostream>
- #include <omp.h>
- int main()
- {
- //通过lastprivate修饰后在并行区域内变为私有变量,进入并行区域
- //后变为私有变量,进入并行区域后每个线程拥有该变量的拷贝,并且会初始化
- int share_to_last_private = 1;
- std::cout << "Before: " << share_to_last_private << std::endl;
- #pragma omp parallel for lastprivate(share_to_last_private)firstprivate(share_to_last_private)
- for(int i = 0; i < 11; ++i)
- {
- std::cout << ++share_to_last_private << std::endl;
- }
- std::cout << "After: " << share_to_last_private << std::endl;
- return 0;
- }
同样,仍然需要通过firstprivate来初始化并行区域中的变量,否则运行会出错。
在运行前后,share_to_last_private变量的值变了,其值最后变成最后一次循环的值,即多个线程最后一次修改的share_to_last_private(是share_to_last_private的copy)值会赋给share_to_last_private.
7. shared示例
- #include <iostream>
- #include <omp.h>
- int main()
- {
- int sum = 0;
- std::cout << "Before: " << sum << std::endl;
- #pragma omp parallel for shared(sum)
- for(int i = 0; i < 10; ++i)
- {
- sum += i;
- std::cout << sum << std::endl;
- }
- std::cout << "After: " << sum << std::endl;
- return 0;
- }
上面的代码中,sum本身就是共享的,这里的shared的声明作为演示用。上面的代码因为sum是共享的,多个线程对sum的操作会引起数据竞争,后续在做介绍。
8. reduction的用法
- #include <iostream>
- #include <omp.h>
- int main()
- {
- int sum = 0;
- std::cout << "Before: " << sum << std::endl;
- #pragma omp parallel for reduction(+:sum)
- for(int i = 0; i < 10; ++i)
- {
- sum = sum + i;
- std::cout << sum << std::endl;
- }
- std::cout << "After: " << sum << std::endl;
- return 0;
- }
其中sum是共享的,采用reduction之后,每个线程根据reduction(+:sum)的声明算出自己的sum,然后再将每个线程的sum加起来。
运行程序,发现第一个线程sum的值依次为0、1、3、6、10;第二个线程sum的值依次为5、11、18、26、35;最后10+35=45。
若果将其中reduction声明去掉,则会输出:
计算步骤如下:
第一个线程sum=0,第二个线程sum=5
第一个线程sum=2+12=14;第二个线程sum=7+14=21
第一个线程sum=3+21=24;第二个线程sum=8+24=32
第一个线程sum=4+32=36;第二个线程sum=9+36=45
尽管结果是对的,但是两个线程对共享的sum的操作时不确定的,会引发数据竞争,例如计算步骤可能如下:
第一个线程sum=0,第二个线程sum=5
第一个线程sum=1+5=6;第二个线程sum=6+6=12
第一个线程sum=2+12=14;第二个线程sum=7+14=21
第一个线程sum=3+21=24;第二个线程sum=8+21=29 //在第一个线程没有将sum更改为24时,第二个线程读取了sum的值
第一个线程sum=4+29=33;第二个线程sum=9+33=42 //导致结果错误。
9. reduction声明可以看作:
1. 保证了对sum的原则操作
2. 多个线程的执行结果通过reduction中声明的操作符进行计算,以加法操作符为例:
假设sum的初始化为10,reduction(+:sum)声明的并行区域中每个线程的sum初始化为0(规定),并行处理结束之后,会将sum的初始化值10以及每个线程所计算的sum值相加。
10. reduction的声明形式
其具体如下:
reduction(operator: val1, val2, ...)
其中operator以及约定变量的初始值如下:
运算符 数据类型 默认初始值
+ 整数、浮点 0
- 整数、浮点 0
* 整数、浮点 1
& 整数 所有位均为1
| 整数 0
^ 整数 0
&& 整数 1
|| 整数 0