以下是有关如何使用线程分析器检测和修复数据争用的详细教程。本教程由以下部分组成:
本教程依赖两个包含数据争用的程序:
第一个程序查找素数。它是用 C 编写的,是使用 OpenMP 指令并行化的。源文件名为 omp_prime.c。
第二个程序也查找素数,并且也是用 C 编写的。但是, 它是使用 POSIX 线程而不是 OpenMP 指令并行化的。源文件名为 pthr_prime.c。
1 #include <stdio.h> 2 #include <math.h> 3 #include <omp.h> 4 5 #define THREADS 4 6 #define N 3000 7 8 int primes[N]; 9 int pflag[N]; 10 11 int is_prime(int v) 12 { 13 int i; 14 int bound = floor(sqrt ((double)v)) + 1; 15 16 for (i = 2; i < bound; i++) { 17 /* No need to check against known composites */ 18 if (!pflag[i]) 19 continue; 20 if (v % i == 0) { 21 pflag[v] = 0; 22 return 0; 23 } 24 } 25 return (v > 1); 26 } 27 28 int main(int argn, char **argv) 29 { 30 int i; 31 int total = 0; 32 33 #ifdef _OPENMP 34 omp_set_num_threads(THREADS); 35 omp_set_dynamic(0); 36 #endif 37 38 for (i = 0; i < N; i++) { 39 pflag[i] = 1; 40 } 41 42 #pragma omp parallel for 43 for (i = 2; i < N; i++) { 44 if ( is_prime(i) ) { 45 primes[total] = i; 46 total++; 47 } 48 } 49 printf("Number of prime numbers between 2 and %d: %d\n", 50 N, total); 51 for (i = 0; i < total; i++) { 52 printf("%d\n", primes[i]); 53 } 54 55 return 0; 56 }
1 #include <stdio.h> 2 #include <math.h> 3 #include <pthread.h> 4 5 #define THREADS 4 6 #define N 3000 7 8 int primes[N]; 9 int pflag[N]; 10 int total = 0; 11 12 int is_prime(int v) 13 { 14 int i; 15 int bound = floor(sqrt ((double)v)) + 1; 16 17 for (i = 2; i < bound; i++) { 18 /* No need to check against known composites */ 19 if (!pflag[i]) 20 continue; 21 if (v % i == 0) { 22 pflag[v] = 0; 23 return 0; 24 } 25 } 26 return (v > 1); 27 } 28 29 void *work(void *arg) 30 { 31 int start; 32 int end; 33 int i; 34 35 start = (N/THREADS) * (*(int *)arg) ; 36 end = start + N/THREADS; 37 for (i = start; i < end; i++) { 38 if ( is_prime(i) ) { 39 primes[total] = i; 40 total++; 41 } 42 } 43 return NULL; 44 } 45 46 int main(int argn, char **argv) 47 { 48 int i; 49 pthread_t tids[THREADS-1]; 50 51 for (i = 0; i < N; i++) { 52 pflag[i] = 1; 53 } 54 55 for (i = 0; i < THREADS-1; i++) { 56 pthread_create(&tids[i], NULL, work, (void *)&i); 57 } 58 59 i = THREADS-1; 60 work((void *)&i); 61 62 printf("Number of prime numbers between 2 and %d: %d\n", 63 N, total); 64 for (i = 0; i < total; i++) { 65 printf("%d\n", primes[i]); 66 } 67 68 return 0; 69 }
如 2.1.1 omp_prime.c 的完整列表所述,当代码包含争用情况且不同的运行提供不同的计算结果时,内存访问顺序是不确定的。由于代码中存在数据争用,所以每次执行 omp_prime.c 都会生成不正确且不一致的结果。下面显示了一个输出示例:
% cc -xopenmp=noopt omp_prime.c -lm % a.out | sort -n 0 0 0 0 0 0 0 Number of prime numbers between 2 and 3000: 336 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 ... 2971 2999 % a.out | sort -n 0 0 0 0 0 0 0 0 0 Number of prime numbers between 2 and 3000: 325 3 5 7 13 17 19 23 29 31 41 43 47 61 67 71 73 79 83 89 101 ... 2971 2999
同样,由于 pthr_prime.c 中存在数据争用,所以程序的不同运行可能生成不正确且不一致的结果,如下所示。
% cc pthr_prime.c -lm -mt . % a.out | sort -n Number of prime numbers between 2 and 3000: 304 751 757 761 769 773 787 797 809 811 821 823 827 829 839 853 857 859 863 877 881 ... 2999 2999 % a.out | sort -n Number of prime numbers between 2 and 3000: 314 751 757 761 769 773 787 797 809 811 821 823 827 839 853 859 877 881 883 907 911 ... 2999 2999
线程分析器沿用与 Sun Studio 性能分析器相同的“收集分析”模型。使用线程分析器包括以下三个步骤:
为了在程序中启用数据争用检测,必须首先使用特殊的编译器选项编译源文件。对 C、C++ 和 Fortran 语言来说,此特殊选项是:-xinstrument=datarace
将 -xinstrument=datarace 编译器选项添加到现有的用来编译程序的一组选项。只能将该选项应用于您怀疑有数据争用的源文件。
确保在编译程序时指定 -g。为检测争用而编译程序时,不要指定高优化级别。使用 -xopenmp=noopt 编译 OpenMP 程序。使用高优化级别时,报告的信息(如行号和调用栈)可能是不正确的。
以下是对源代码进行校验的示例命令:
cc -xinstrument=datarace -g -mt pthr_prime.c
cc -xinstrument=datarace -g -xopenmp=noopt omp_prime.c
将 collect 命令与 -r on 标志一起使用,以运行程序并在执行过程中创建数据争用检测实验。对于 OpenMP 程序,请确保所用的线程超过一个。以下是创建数据争用实验的示例命令:
collect -r race./a.out
为增大检测到数据争用的可能性,建议将 collect 与 r race 标志一起使用,以创建若干个数据争用检测实验。在不同的实验中使用不同的线程数和不同的输入数据。
可以使用线程分析器、性能分析器或 er_print 实用程序检查数据争用检测实验。线程分析器和性能分析器都提供 GUI 界面;前者提供的是一组简化的缺省选项卡,但在其他方面与性能分析器完全相同。
线程分析器 GUI 具有菜单栏、工具栏和包含各种选项卡的拆分窗格(不同选项卡对应不同的显示)。在左窗格上,缺省情况下显示以下三个选项卡:
Races(争用)选项卡显示在程序中检测到的数据争用的列表。缺省情况下此选项卡处于选中状态。
“双重数据源”选项卡显示对应于所选数据争用的两次访问的两个源位置。突出显示其中发生数据争用访问的源代码行。
“试验”选项卡显示实验中的装入对象,并列出错误和警告消息。
在线程分析器显示屏的右窗格上,显示以下两个选项卡:
“摘要”选项卡显示有关在 Races(争用)选项卡中选择的数据争用访问的摘要信息。
Race Details(争用详细信息)选项卡显示有关在 Races(争用)选项卡中选择的数据争用跟踪的详细信息。
另一方面,er_print 实用程序提供命令行界面。在使用 er_print 实用程序检查争用时,以下子命令很有用:
-races:它报告实验所显示的任何数据争用。
-rdetail race_id:它显示有关具有指定 race_id 的数据争用的详细信息。如果指定的 race_id 为 "all",将显示有关所有数据争用的详细信息。
-header:它显示有关实验的描述性信息,并报告所有错误或警告。
有关更多信息,请参阅 collect.1、tha.1、analyzer.1 和 er_print.1 手册页。
本部分说明如何使用 er_print 命令行和线程分析器 GUI 显示有关检测到的每个数据争用的以下信息:
数据争用的唯一 ID。
与数据争用关联的虚拟地址 Vaddr
。如果存在多个虚拟地址,则在圆括号中显示标签“多个地址”。
两个不同线程对虚拟地址 Vaddr
的内存访问。显示访问类型(读取或写入),以及源代码中发生访问处的函数、偏移量和行号。
与数据争用关联的跟踪总数。每个跟踪都引用发生两个数据争用访问时的线程调用栈对。如果使用 GUI,则选择单个跟踪时 Race Details(争用详细信息)选项卡中将显示这两个调用栈。如果使用 er_print 实用程序,则 rdetail 命令将显示这两个调用栈。
% cc -xopenmp=noopt omp_prime.c -lm -xinstrument=datarace % collect -r race a.out | sort -n 0 0 0 0 0 0 0 0 0 0 ... 0 0 Creating experiment database test.1.er ... Number of prime numbers between 2 and 3000: 429 2 3 5 7 11 13 17 19 23 29 31 37 41 47 53 59 61 67 71 73 ... 2971 2999 % er_print test.1.er (er_print) races Total Races: 4 Experiment: test.1.er Race #1, Vaddr: 0xffbfeec4 Access 1: Read, main -- MP doall from line 42 [_$d1A42.main] + 0x00000060, line 45 in "omp_prime.c" Access 2: Write, main -- MP doall from line 42 [_$d1A42.main] + 0x0000008C, line 46 in "omp_prime.c" Total Traces: 2 Race #2, Vaddr: 0xffbfeec4 Access 1: Write, main -- MP doall from line 42 [_$d1A42.main] + 0x0000008C, line 46 in "omp_prime.c" Access 2: Write, main -- MP doall from line 42 [_$d1A42.main] + 0x0000008C, line 46 in "omp_prime.c" Total Traces: 1 Race #3, Vaddr: (Multiple Addresses) Access 1: Write, main -- MP doall from line 42 [_$d1A42.main] + 0x0000007C, line 45 in "omp_prime.c" Access 2: Write, main -- MP doall from line 42 [_$d1A42.main] + 0x0000007C, line 45 in "omp_prime.c" Total Traces: 1 Race #4, Vaddr: 0x21418 Access 1: Read, is_prime + 0x00000074, line 18 in "omp_prime.c" Access 2: Write, is_prime + 0x00000114, line 21 in "omp_prime.c" Total Traces: 1 (er_print)
以下屏幕快照显示在 omp_primes.c 中检测到的争用,与线程分析器 GUI 显示的相同。调用 GUI 并装入实验数据的命令是 tha test.1.er。
在 omp_primes.c 中有以下四个数据争用:
一号争用:第 45 行上 total 的读取和第 46 行上 total 的写入之间的数据争用。
二号争用:第 46 行上 total 的写入和同一行上 total 的另一写入之间的数据争用。
三号争用:第 45 行上 primes[]
的写入和同一行上 primes[]
的另一写入之间的数据争用。
四号争用:第 18 行上 pflag[]
的读取和第 21 行上 pflag[]
的写入之间的数据争用。
% cc pthr_prime.c -lm -mt -xinstrument=datarace . % collect -r on a.out | sort -n Creating experiment database test.2.er ... of type "nfs", which may distort the measured performance. 0 0 0 0 0 0 0 0 0 0 ... 0 0 Creating experiment database test.2.er ... Number of prime numbers between 2 and 3000: 328 751 757 761 773 797 809 811 821 823 827 829 839 853 857 859 877 881 883 887 907 ... 2999 2999 % er_print test.2.er (er_print) races Total Races: 6 Experiment: test.2.er Race #1, Vaddr: 0x218d0 Access 1: Write, work + 0x00000154, line 40 in "pthr_prime.c" Access 2: Write, work + 0x00000154, line 40 in "pthr_prime.c" Total Traces: 3 Race #2, Vaddr: 0x218d0 Access 1: Read, work + 0x000000CC, line 39 in "pthr_prime.c" Access 2: Write, work + 0x00000154, line 40 in "pthr_prime.c" Total Traces: 3 Race #3, Vaddr: 0xffbfeec4 Access 1: Write, main + 0x00000204, line 55 in "pthr_prime.c" Access 2: Read, work + 0x00000024, line 35 in "pthr_prime.c" Total Traces: 2 Race #4, Vaddr: (Multiple Addresses) Access 1: Write, work + 0x00000108, line 39 in "pthr_prime.c" Access 2: Write, work + 0x00000108, line 39 in "pthr_prime.c" Total Traces: 1 Race #5, Vaddr: 0x23bfc Access 1: Write, is_prime + 0x00000210, line 22 in "pthr_prime.c" Access 2: Write, is_prime + 0x00000210, line 22 in "pthr_prime.c" Total Traces: 1 Race #6, Vaddr: 0x247bc Access 1: Write, work + 0x00000108, line 39 in "pthr_prime.c" Access 2: Read, main + 0x00000394, line 65 in "pthr_prime.c" Total Traces: 1 (er_print)
以下屏幕快照显示在 pthr_primes.c 中检测到的争用,与线程分析器 GUI 显示的相同。调用 GUI 和装入实验数据的命令是 tha test.2.er。
在 pthr_prime.c 中有以下六个数据争用:
一号争用:第 40 行上 total 的写入和同一行上 total 的另一写入之间的数据争用。
二号争用:第 39 行上 total 的读取和第 40 行上 total 的写入之间的数据争用。
三号争用:第 55 行上 i 的写入和第 35 行上 i 的读取之间的数据争用。
四号争用:第 39 行上primes[]
的写入和同一行上primes[]
的另一写入之间的数据争用。
五号争用:第 22 行上 pflag[]
的写入和同一行上 pflag[]
的另一写入之间的数据争用
六号争用:第 39 行上 primes[]
的写入和第 65 行上 primes[]
的读取之间的数据争用。
GUI 的一个优势在于,它允许您并排查看与数据争用关联的两个源位置。例如,在 Races(争用)选项卡中选择 pthr_prime.c 的六号争用,然后单击“双重数据源”选项卡。您将看到以下内容:
在顶部 "Race Source" 窗格中显示六号争用(第 39 行)的第一次访问,在底部窗格中则显示该数据争用的第二次访问。突出显示其中发生数据争用访问的源代码(第 39 行和第 65 行)。在每个源代码行的左侧显示缺省度量(互斥争用访问度量)。该度量显示在该行上报告的数据争用访问次数。
此部分提供诊断数据争用原因的基本策略。
误报数据争用是线程分析器报告了实际上未发生的数据争用。线程分析器尝试减少误报数。但是,存在该工具无法执行准确的作业并可能误报数据争用的情况。
可以忽略误报的数据争用,因为它不是真正的数据争用,因此不会影响程序的行为。
有关误报数据争用的一些示例,请参见 2.5 误报。有关如何避免误报数据争用的信息,请参见A.1 线程分析器的用户 API。
良性数据争用是指其存在不会影响程序正确性的有意数据争用。
有些多线程应用程序会有意使用可能导致数据争用的代码。由于那里的数据争用是设计使然,因此无需进行修复。但是,在某些情况下,使这样的代码正确运行是相当棘手的。应仔细检查这些数据争用。
有关良性争用的更多详细信息,请参见 2.5 误报。
线程分析器可以帮助查找程序中的数据争用,但是它无法自动查找程序中的错误,也无法建议如何修复所找到的数据争用。数据争用也可能是由错误引入的。找到并修复错误是很重要的。仅仅消除数据争用并不是正确的方法,这样做可能会使进一步调试变得更加困难。修复错误而不是修复数据争用。
以下说明如何修复 omp_prime.c 中的错误。有关完整的文件列表,请参见 2.1.1 omp_prime.c 的完整列表。
将第 45 行和第 46 行移动到临界段中,以便消除第 45 行上 total 的读取和第 46 行上 total 的写入之间的数据争用。临界段保护这两行并防止数据争用。以下是更正后的代码:
42 #pragma omp parallel for . 43 for (i = 2; i < N; i++) { 44 if ( is_prime(i) ) { #pragma omp critical { 45 primes[total] = i; 46 total++; } 47 } 48 }
请注意,添加单个临界段还修复了 omp_prime.c 中的两个其他数据争用。它修复了第 45 行上 prime[]
的数据争用,以及第 46 行上 total 的数据争用。第 18 行上 pflag[]
的读取和第 21 行上 pflag[]
的写入之间的第四个数据争用实际上是良性争用,因为它不会导致不正确的结果。修复良性数据争用不是必需的。
还可以将第 45 行和第 46 行移动到临界段中,如下所示,但是此更改将无法更正程序:
42 #pragma omp parallel for . 43 for (i = 2; i < N; i++) { 44 if ( is_prime(i) ) { #pragma omp critical { 45 primes[total] = i; } #pragma omp critical { 46 total++; } 47 } 48 }
第 45 行和第 46 行周围的临界段消除了数据争用,因为线程不使用任何互斥锁控制其对 total 的访问。第 46 行周围的临界段确保 total 的计算值是正确的。但是,程序仍是不正确的。两个线程可能使用 total 的同一值更新 primes[]
的同一元素。此外,primes[]
中的某些元素可能根本未赋值。
以下说明如何修复 pthr_prime.c 中的错误。有关完整的文件列表,请参见 2.1.2 pthr_prime.c 的完整列表。
使用单个互斥锁消除 pthr_prime.c 中第 39 行上 total 的读取和第 40 行上 total 的写入之间的数据争用。此添加还修复了 pthr_prime.c 中的两个其他数据争用:第 39 行上 prime[]
的数据争用以及第 40 行上 total 的数据争用。
第 55 行上 i 的写入和第 35 行上 i 的读取之间的数据争用以及第 22 行上 pflag[]
的数据争用显示了不同线程对变量 i 进行共享访问的问题。pthr_prime.c 中的初始线程在循环中创建子线程(源代码的第 55-57 行),并调度它们处理函数 work()。循环索引 i 按地址传递到 work()。由于所有线程都访问 i 的同一内存位置,因此每个线程的 i 值不会保持唯一,而是将随初始线程递增循环索引而改变。由于不同线程使用 i 的同一值,因此发生了数据争用。
修复该问题的一种方法是将 i 按值传递到 work()。这确保每个线程都具有自己的带有唯一值的专用 i 副本。要消除第 39 行 上写入访问和第 65 行上读取访问之间的 primes[]
数据争用,可以使用与前面第 39 行和第 40 行所用相同的互斥锁保护第 65 行。但是,这不是正确的修复方法。真正的问题是,主线程可能会在子线程仍在函数 work() 中更新 total 和 primes[]
的同时报告结果(第 50 行到第 53 行)。使用互斥锁并不提供线程之间的正确排序同步。一种正确的修复方法是在打印出结果之前让主线程等待所有子线程加入。
以下是更正后的 pthr_prime.c 版本:
1 #include <stdio.h> 2 #include <math.h> 3 #include <pthread.h> 4 5 #define THREADS 4 6 #define N 3000 7 8 int primes[N]; 9 int pflag[N]; 10 int total = 0; 11 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 12 13 int is_prime(int v) 14 { 15 int i; 16 int bound = floor(sqrt(v)) + 1; 17 18 for (i = 2; i < bound; i++) { 19 /* no need to check against known composites */ 20 if (!pflag[i]) 21 continue; 22 if (v % i == 0) { 23 pflag[v] = 0; 24 return 0; 25 } 26 } 27 return (v > 1); 28 } 29 30 void *work(void *arg) 31 { 32 int start; 33 int end; 34 int i; 35 36 start = (N/THREADS) * ((int)arg) ; 37 end = start + N/THREADS; 38 for (i = start; i < end; i++) { 39 if ( is_prime(i) ) { 40 pthread_mutex_lock(&mutex); 41 primes[total] = i; 42 total++; 43 pthread_mutex_unlock(&mutex); 44 } 45 } 46 return NULL; 47 } 48 49 int main(int argn, char **argv) 50 { 51 int i; 52 pthread_t tids[THREADS-1]; 53 54 for (i = 0; i < N; i++) { 55 pflag[i] = 1; 56 } 57 58 for (i = 0; i < THREADS-1; i++) { 59 pthread_create(&tids[i], NULL, work, (void *)i); 60 } 61 62 i = THREADS-1; 63 work((void *)i); 64 65 for (i = 0; i < THREADS-1; i++) { 66 pthread_join(tids[i], NULL); 67 } 68 69 printf("Number of prime numbers between 2 and %d: %d\n", 70 N, total); 71 for (i = 0; i < total; i++) { 72 printf("%d\n", primes[i]); 73 } 74 }
有些情况下,线程分析器可能会报告程序中未真正发生的假数据争用。这些情况称为误报。大多数情况下,误报是由2.5.1 用户定义的同步或2.5.2 由不同线程再循环的内存导致的。
线程分析器可以识别由 OpenMP、POSIX 线程和 Solaris 线程提供的大多数标准同步 API 和构造。但是,该工具无法识别用户定义的同步,而且在代码包含这样的同步时可能会报告假的数据争用。例如,该工具无法使用 CAS 指令识别锁的实现,无法使用忙等待识别张贴和等待操作,等等。以下是某类误报的典型示例,其中程序利用的是 POSIX 线程条件变量的常见用法:
/* Initially ready_flag is 0 */ /* Thread 1: Producer */ 100 data = ... 101 pthread_mutex_lock (&mutex); 102 ready_flag = 1; 103 pthread_cond_signal (&cond); 103 pthread_mutex_unlock (&mutex); ... /* Thread 2: Consumer */ 200 pthread_mutex_lock (&mutex); 201 while (!ready_flag) { 202 pthread_cond_wait (&cond, &mutex); 203 } 204 pthread_mutex_unlock (&mutex); 205 ... = data;
pthread_cond_wait() 调用通常在测试谓词的循环中进行,以防止程序错误和虚假唤醒。谓词的测试和设置通常由互斥锁进行保护。在上面的代码中,线程 1 在第 100 行针对变量 data 生成值,在第 102 行将 ready_flag 的值设置为一以指示已生成数据,然后调用 pthread_cond_signal() 以唤醒使用方线程(即线程 2)。线程 2 在循环中测试谓词 (!ready_flag)。它在发现已设置标志时,将使用第 205 行上的数据。
第 102 行上 ready_flag 的写入和第 201 行上 ready_flag 的读取是由同一互斥锁保护的,因此在这两个访问之间不存在数据争用,而且该工具可正确地对此进行识别。
第 100 行上 data 的写入和第 205 行上 data 的读取不受互斥锁的保护。但是,在程序逻辑中,第 205 行上的读取始终发生在第 100 行上的写入之后,原因是存在标志变量 ready_flag。因此,在这两个访问数据之间不存在数据争用。但是,如果在运行时实际上未调用对 pthread_cond_wait()(第 202 行)的调用,该工具将报告在两个访问之间存在数据争用。如果曾在执行第 201 行之前执行第 102 行,则执行第 201 行时,循环项测试失败并跳过第 202 行。该工具监视 pthread_cond_signal() 调用和 pthread_cond_wait() 调用, 并且可以使它们成对以派生同步。如果未调用第 202 行上的 pthread_cond_wait(),则该工具不知道第 100 行上的写入始终是在第 205 行上的读取之前执行的。因此,它认为它们是并发执行的,并报告它们之间的数据争用。
为了避免误报此类数据争用,线程分析器提供了一组 API,可以用来在执行用户定义的同步时通知该工具。有关更多信息,请参见A.1 线程分析器的用户 API。
一些内存管理例程再循环线程释放的内存以供另一线程使用。线程分析器有时无法识别由不同线程使用的同一内存位置的使用期限不重叠。如果出现此情况,则该工具可能误报数据争用。以下示例说明此类误报。
/*----------*/ /*----------*/ /* Thread 1 */ /* Thread 2 */ /*----------*/ /*----------*/ ptr1 = mymalloc(sizeof(data_t)); ptr1->data = ... ... myfree(ptr1); ptr2 = mymalloc(sizeof(data_t)); ptr2->data = ... ... myfree(ptr2);
线程 1和线程 2 并发执行。每个线程都分配一个用作其专用内存的内存块。例程 mymalloc() 可能会将前一调用释放的内存提供给 myfree()。如果线程 2 在线程 1 调用 myfree() 之前调用 mymalloc(),则 ptr1 和 ptr2 将获取不同的值,因此这两个线程之间没有数据争用。但是,如果线程 2 在线程 1 调用 myfree() 之后调用 mymalloc(),则 ptr1 和 ptr2 可能具有相同值。由于线程 1 不再访问该内存,因此不存在数据争用。但是,如果该工具不知道 mymalloc() 正在再循环内存,则它将报告 ptr1 数据的写入和 ptr2 数据的写入之间存在数据争用。当 C++ 运行时库为临时变量再循环内存时,此类误报通常发生在 C++ 应用程序中。它通常还发生在实现自己的内存管理例程的用户应用程序中。当前,线程分析器能够识别通过标准 malloc()、calloc() 和 realloc() 接口执行的内存分配和释放操作。
一些多线程应用程序有意允许数据争用,以便获得更佳性能。良性数据争用是指其存在不会影响程序正确性的有意数据争用。以下示例说明良性数据争用。
除了良性数据争用外,大量应用程序允许数据争用,因为它们依赖于很难正确设计的锁释放和等待释放算法。线程分析器可以帮助确定这些应用程序中存在数据争用的位置。
文件 omp_prime.c 中的线程通过执行函数 is_prime() 来检查一个整数是否为素数。
11 int is_prime(int v) 12 { 13 int i; 14 int bound = floor(sqrt ((double)v)) + 1; 15 16 for (i = 2; i < bound; i++) { 17 /* No need to check against known composites */ 18 if (!pflag[i]) 19 continue; 20 if (v % i == 0) { 21 pflag[v] = 0; 22 return 0; 23 } 24 } 25 return (v > 1); 26 }
线程分析器报告在第 21 行上 pflag[]
的写入和第 18 行上 pflag[]
的读取之间存在数据争用。但是,此数据争用是良性的,因为它不会影响最终结果的正确性。在第 18 行上,线程检查对于 i 的给定值 pflag[i]
是否等于零。如果 pflag[i]
不等于零,则表明 i 是已知的合数(换句话说,知道 i 不是素数)。因此,无需检查 v 是否可被 i 整除;我们只需检查 v 是否可被某个素数整除。因此,如果 pflag[i]
等于零,则线程将继续执行 i 的下一个值。如果 pflag[i]
不等于零且 v 可被 i 整除,则线程将为 pflag[v]
分配零,以指示 v 不是素数。
从正确性方面看,多个线程检查同一 pflag[]
元素并同时向其写入是可以的。pflag[]
元素的初始值为一。当线程更新该元素时,它们为该元素分配零值。即,线程在该元素的同一内存字节的同一位中存储零。在当前的体系结构中,可以假定那些存储是原子的。这意味着,线程读取该元素时,读取的值要么为一,要么为零。如果线程在为其分配零值之前检查给定 pflag[]
元素(第 18 行),则它执行第 20-23 行。如果在此期间,另一线程为该同一 pflag[]
元素(第 21 行)分配零值,则最终结果不变。在本质上,这意味着第一个线程不必要地执行了第 20-23 行。
一组线程并发调用 check_bad_array() 以检查数组 data_array 是否有元素已损坏。每个线程检查数组的不同部分。如果线程发现某元素已损坏,则它会将全局共享变量 is_bad 的值设置为 true。
20 volatile int is_bad = 0; ... 100 /* 102 * Each thread checks its assigned portion of data_array, and sets 102 * the global flag is_bad to 1 once it finds a bad data element. 103 */ 104 void check_bad_array(volatile data_t *data_array, unsigned int thread_id) 105 { 106 int i; 107 for (i=my_start(thread_id); i<my_end(thread_id); i++) { 108 if (is_bad) 109 return; 110 else { 111 if (is_bad_element(data_array[i])) { 112 is_bad = 1; 113 return; 114 } 115 } 116 } 117 }
第 108 行上 is_bad 的读取和第 112 行上 is_bad 的写入之间存在数据争用。但是,该数据争用不会影响最终结果的正确性。
is_bad 的初始值为零。当线程更新 is_bad 时,它们为其分配值一。即,线程在 is_bad 的同一内存字节的同一位中存储一。在当前的体系结构中,可以假定那些存储是原子的。因此,当线程读取 is_bad 时,读取的值要么是零,要么是一。如果线程在为其分配值一之前检查 is_bad(第 108 行),则它继续执行 for 循环。如果在此期间,另一个线程为 is_bad(第 112 行)分配值一,则不会更改最终结果。这仅仅意味着,线程执行 for 循环的时间超过了所需时间。
单件可确保在整个程序中只有一个特定类型的对象。双检锁是一种常用的有效方法,用于在多线程应用程序中初始化单件。以下代码说明这样的实现。
100 class Singleton { 101 public: 102 static Singleton* instance(); 103 ... 104 private: 105 static Singleton* ptr_instance; 106 }; ... 200 Singleton* Singleton::ptr_instance = 0; ... 300 Singleton* Singleton::instance() { 301 Singleton *tmp = ptr_instance; 302 memory_barrier(); 303 if (tmp == NULL) { 304 Lock(); 305 if (ptr_instance == NULL) { 306 tmp = new Singleton; 307 memory_barrier(); 308 ptr_instance = tmp; 309 } 310 Unlock(); 311 } 312 return tmp; 313 }
ptr_instance 的读取(第 301 行)有意不受锁的保护。这使检查可以确定在多线程环境中是否已有效地实例化单件。请注意,在第 301 行上的读取和第 308 行上的写入之间存在对 ptr_instance 变量的数据争用,但是程序可正常工作。不过,编写允许数据争用的正确程序是一项艰难的任务。例如,在上面的双检锁代码中,第 302 行和第 307 行上对 memory_barrier() 的调用用于确保按正确的顺序设置和读取单件和 ptr_instance。因此,所有线程将一致地读取它们。如果未使用内存屏障,则此编程方法将失效。