本教程详细介绍如何使用线程分析器检测和修复数据争用。
本教程分为以下几节:
本教程所依赖的两个程序都存在数据争用:
第一个程序查找质数。该程序是用 C 编写的,并使用 OpenMP 指令进行了并行化。该源文件称为 prime_omp.c。
第二个程序也查找质数,也是用 C 编写的。但它使用 POSIX 线程(而不是 OpenMP 指令)进行了并行化。该源文件称为 prime_pthr.c。
本教程中使用的源文件位于 /opt/solstudio12.2/prod/examples/tha(Oracle Solaris 系统)或 /opt/oracle/solstudio12.2/prod/examples/tha(Linux 或 OpenSolaris 系统)下。这些示例位于 prime_omp 和 prime_pthr 子目录中。每个示例目录都包含 Makefile 和 DEMO 说明文件,但是本教程并不遵循这些说明,也不使用 Makefile。本教程将逐步指导您执行命令。
为了学习本教程,可以将 prime_omp.c 和 prime_pthr.c 文件从示例目录复制到其他目录,也可以创建自己的文件并从下面列出的代码内容中复制代码。
prime_omp.c 的源代码如下所示:
1 /* 2 * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All Rights Reserved. 3 * @(#)prime_omp.c 1.3 (Oracle) 10/03/26 4 */ 5 6 #include <stdio.h> 7 #include <math.h> 8 #include <omp.h> 9 10 #define THREADS 4 11 #define N 10000 12 13 int primes[N]; 14 int pflag[N]; 15 16 int is_prime(int v) 17 { 18 int i; 19 int bound = floor(sqrt(v)) + 1; 20 21 for (i = 2; i < bound; i++) { 22 /* no need to check against known composites */ 23 if (!pflag[i]) 24 continue; 25 if (v % i == 0) { 26 pflag[v] = 0; 27 return 0; 28 } 29 } 30 return (v > 1); 31 } 32 33 int main(int argn, char **argv) 34 { 35 int i; 36 int total = 0; 37 38 #ifdef _OPENMP 39 omp_set_dynamic(0); 40 omp_set_num_threads(THREADS); 41 #endif 42 43 for (i = 0; i < N; i++) { 44 pflag[i] = 1; 45 } 46 47 #pragma omp parallel for 48 for (i = 2; i < N; i++) { 49 if ( is_prime(i) ) { 50 primes[total] = i; 51 total++; 52 } 53 } 54 55 printf("Number of prime numbers between 2 and %d: %d\n", 56 N, total); 57 58 return 0; 59 }
prime_pthr.c 的源代码如下所示:
1 /* 2 * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All Rights Reserved. 3 * @(#)prime_pthr.c 1.4 (Oracle) 10/03/26 4 */ 5 6 #include <stdio.h> 7 #include <math.h> 8 #include <pthread.h> 9 10 #define THREADS 4 11 #define N 10000 12 13 int primes[N]; 14 int pflag[N]; 15 int total = 0; 16 17 int is_prime(int v) 18 { 19 int i; 20 int bound = floor(sqrt(v)) + 1; 21 22 for (i = 2; i < bound; i++) { 23 /* no need to check against known composites */ 24 if (!pflag[i]) 25 continue; 26 if (v % i == 0) { 27 pflag[v] = 0; 28 return 0; 29 } 30 } 31 return (v > 1); 32 } 33 34 void *work(void *arg) 35 { 36 int start; 37 int end; 38 int i; 39 40 start = (N/THREADS) * (*(int *)arg); 41 end = start + N/THREADS; 42 for (i = start; i < end; i++) { 43 if ( is_prime(i) ) { 44 primes[total] = i; 45 total++; 46 } 47 } 48 return NULL; 49 } 50 51 int main(int argn, char **argv) 52 { 53 int i; 54 pthread_t tids[THREADS-1]; 55 56 for (i = 0; i < N; i++) { 57 pflag[i] = 1; 58 } 59 60 for (i = 0; i < THREADS-1; i++) { 61 pthread_create(&tids[i], NULL, work, (void *)&i); 62 } 63 64 i = THREADS-1; 65 work((void *)&i); 66 67 for (i = 0; i < THREADS-1; i++) { 68 pthread_join(tids[i], NULL); 69 } 70 71 printf("Number of prime numbers between 2 and %d: %d\n", 72 N, total); 73 74 return 0; 75 }
当代码包含竞争情况时,内存访问的顺序是不确定的,因此每次运行的计算结果会不同。
通过编译和运行示例可以看出,由于代码中存在数据争用,每次执行 prime_omp 或 prime_pthr 时都会产生不正确且不一致的结果。
在下面的示例中,键入粗体形式的命令以编译并运行 prime_omp 程序:
% cc -xopenmp=noopt -o prime_omp prime_omp.c -lm % % ./prime_omp Number of prime numbers between 2 and 10000: 1229 % ./prime_omp Number of prime numbers between 2 and 10000: 1228 % ./prime_omp Number of prime numbers between 2 and 10000: 1229 |
在下面的示例中,键入粗体形式的命令以编译并运行 prime_pthr 程序:
% cc -mt -o prime_pthr prime_pthr.c -lm % % ./prime_pthr Number of prime numbers between 2 and 10000: 1140 % ./prime_pthr Number of prime numbers between 2 and 10000: 1122 % ./prime_pthr Number of prime numbers between 2 and 10000: 1141 |
请注意每个程序的三次运行结果的不一致性。可能需要运行这些程序三次以上才能看到不一致的结果。
接下来将会校验代码并创建实验,以便可以找出发生数据争用的位置。
线程分析器沿用与 Oracle Solaris Studio 性能分析器相同的“收集-分析”模型。
使用线程分析器的过程涉及三个步骤:
为了在程序中检测数据争用,必须首先对代码进行校验以监视运行时的内存访问。该校验可以在应用程序源代码上完成,也可以在已使用某些 Oracle 编译器优化标志进行编译的应用程序二进制代码上完成。本教程将介绍如何使用这两种校验程序的方法。
要校验源代码,必须使用特殊的编译器选项 -xinstrument=datarace 对应用程序进行编译。此选项会指示编译器对生成的代码进行校验,以便检测数据争用。
将 -xinstrument=datarace 编译器选项添加到用于编译程序的现有选项集中。
使用 -xinstrument=datarace 对程序进行编译时,务必还要指定 -g 选项,目的是生成其他信息以启用分析器的全部功能。对程序进行编译以检测数据争用时,不要指定高优化级别。请使用 -xopenmp=noopt 编译 OpenMP 程序。使用高优化级别时,报告的信息(如行号和调用栈)可能是错误的。
可以使用以下命令校验本教程的源代码:
% cc -xinstrument=datarace -g -xopenmp=noopt -o prime_omp_inst prime_omp.c -lm |
% cc -xinstrument=datarace -g -o prime_pthr_inst prime_pthr.c -lm |
请注意,本示例在结尾处使用了 _inst 指定输出文件,因此可以知道该二进制代码是校验后的二进制代码。不过,这不是必需的。
要校验程序的二进制代码而非源代码,需要使用 Oracle Solaris Studio 中包含的 discover 工具。discover(1) 手册页和《Oracle Solaris Studio 12.2 Discover 和 Uncover 用户指南》中对该工具进行了说明。
有关二进制代码校验要求的更多信息,请参见二进制代码级别校验。
对于本教程中的示例,请键入以下命令以使用优化级别 3 进行代码编译,进而创建可由 discover 使用的二进制代码。
% cc -xopenmp=noopt -g -o prime_omp_opt prime_omp.c -lm |
% cc -g -O3 -o prime_pthr_opt prime_pthr.c -lm |
然后,对所创建的 prime_omp_opt 和 prime_pthr_opt 优化二进制代码运行 discover:
% discover -i datarace -o prime_omp_disc prime_omp_opt |
% discover -i datarace -o prime_pthr_disc prime_pthr_opt |
这些命令将创建校验后的二进制代码 prime_omp_disc 和 prime_pthr_disc,可以将这些二进制代码与 collect 一起使用,以创建可使用线程分析器进行检查的实验。
使用带有 -r race 标志的 collect 命令来运行程序并在进程执行期间创建数据争用检测实验。对于 OpenMP 程序,确保使用的线程数多于一个。在本教程的样例中,使用了四个线程。
基于通过校验源代码创建的二进制代码创建实验:
% collect -r race -o prime_omp_inst.er prime_omp_inst |
% collect -r race -o prime_pthr_inst.er prime_pthr_inst |
基于通过使用 discover 工具创建的二进制代码创建实验:
% collect -r race -o prime_omp_disc.er prime_omp_disc |
% collect -r race -o prime_pthr_disc.er prime_pthr_disc |
为增大检测到数据争用的可能性,建议使用带有 -r race 标志的 collect 创建多个数据争用检测实验。对于每个实验,应使用不同的线程数和不同的输入数据。
例如,在 prime_omp.c 中,由以下行设置线程数:
#define THREADS 4 |
可通过将上面的 4 更改为大于 1 的其他某个整数(例如 8)来更改线程数。
prime_omp.c 中的以下行会将程序限制为查找 2 和 3000 之间的质数:
#define N 3000 |
可通过更改 N 的值提供不同的输入数据,从而使程序执行更多或更少的工作。
可以使用线程分析器、性能分析器或 er_print 实用程序检查数据争用检测实验。线程分析器和性能分析器都提供 GUI 界面;线程分析器显示的是一组简化的缺省标签,但在其他方面与性能分析器完全相同。
要启动线程分析器,请键入以下命令:
% tha |
线程分析器 GUI 具有菜单栏、工具栏以及包含多个标签的拆分窗格(不同标签对应不同的显示)。
缺省情况下,左侧窗格中会显示以下三个标签:
"Races"(争用)标签,显示程序中检测到的数据争用列表以及关联的调用栈跟踪。缺省情况下会选中此标签。
"Dual Source"(双源)标签,显示与所选数据争用的两次访问相对应的两个源代码位置。发生数据争用访问的源代码行会突出显示。
"Experiments"(实验)标签,显示实验中的装入对象并列出错误和警告消息。
线程分析器显示屏的右侧窗格中显示以下两个标签:
"Summary"(摘要)标签,显示从 "Races"(争用)标签中选择的数据争用访问的摘要信息。
"Race Details"(争用详细信息)标签,显示从 "Races"(争用)标签中选择的数据争用或调用栈跟踪的详细信息。
er_print 实用程序提供命令行界面。可以在交互式会话中使用 er_print 实用程序并在该会话期间指定子命令。也可以使用命令行选项以非交互方式指定子命令。
使用 er_print 实用程序检查争用时,以下子命令非常有用:
-races
该选项会报告在实验中发现的所有数据争用。在 (er_print) 提示符下指定 races,或者在 er_print 命令行上指定 -races。
-rdetail race_id
该选项会显示具有指定 race_id 的数据争用的详细信息。在 (er_print) 提示符下指定 rdetail,或者在 er_print 命令行上指定 -rdetail。如果指定的 race_id 为 all,将显示所有数据争用的详细信息。否则,请指定单个争用编号,例如为第一个数据争用指定 1。
-header
该选项会显示有关实验的描述性信息并报告所有错误或警告。在 (er_print) 提示符下指定 header,或者在命令行上指定 -header。
有关更多信息,请参阅 collect(1)、tha(1)、analyzer(1) 和 er_print(1) 手册页。
本节介绍如何使用 er_print 命令行和线程分析器 GUI 显示检测到的每个数据争用的以下信息:
数据争用的唯一 ID。
与数据争用相关联的虚拟地址 Vaddr
。如果不止一个虚拟地址,则会在括号中显示标签 "Multiple Addresses"(多个地址)。
两个不同线程对虚拟地址 Vaddr
的内存访问。将会显示访问的类型(读取或写入),以及源代码中发生访问的函数、偏移量和行号。
与数据争用相关联的调用栈跟踪总数。每个跟踪都引用发生两个数据争用访问时的一对线程调用栈。如果使用的是 GUI,则选择单个调用栈跟踪时会在 "Race Details"(争用详细信息)标签中显示这两个调用栈。如果使用的是 er_print 实用程序,rdetail 命令将会显示这两个调用栈。
要检查 prime_omp.c 中的数据争用,可以使用在2.2.2 创建数据争用检测实验中创建的实验之一。
要使用 er_print 显示 prime_omp_instr.er 实验中的数据争用信息,请键入以下命令。
% er_print prime_omp_inst.er |
在 (er_print) 提示符下,键入 races 可看到类似如下的输出:
(er_print)races Total Races: 2 Experiment: prime_omp_inst.er Race #1, Vaddr: 0x21ca8 Access 1: Write, is_prime, line 26 in "prime_omp.c" Access 2: Read, is_prime, line 23 in "prime_omp.c" Total Callstack Traces: 1 Race #2, Vaddr: (Multiple Addresses) Access 1: Write, main, line 50 in "prime_omp.c" Access 2: Write, main, line 50 in "prime_omp.c" Total Callstack Traces: 2 (er_print) |
该程序的此次特定运行期间发生了两次数据争用。
要在线程分析器中打开 prime_omp_inst.er 实验,请键入以下命令:
% tha prime_omp_inst.er |
以下屏幕抓图显示了在 prime_omp.c 中检测到的争用,如线程分析器所显示。
prime_omp.c 中显示了两个数据争用:
Race #1(争用 1)显示了第 26 行函数 is_prime 中的一次写入与第 23 行同一函数中的一次读取之间的争用。如果查看源代码,可以看到在这些行上,正在对 pflag[ ] 数组进行访问。在线程分析器中,通过单击 "Dual Source"(双源)标签可以很方便地查看位于两个行号位置的源代码,其中还有一些度量显示受影响的代码行上的争用访问次数。
Race #2(争用 2)显示了对 main 函数第 50 行的两次写入之间的争用。单击 "Dual Source"(双源)标签可看到对第 50 行 primes [ ] 数组的值进行了多次访问尝试。
Race #2(争用 2)表示数组 primes[ ] 的不同元素中所发生的一组数据争用。通过 Vaddr
显示 Multiple Addresses(多个地址)可以看出这一点。
使用线程分析器中的 "Dual Source"(双源)标签,可以同时看到与数据争用相关联的两个源代码位置。例如,在 "Races"(争用)标签中选择 prime_pthr.c 的 Race #2(争用 2),然后单击 "Dual Source"(双源)标签。将会看到类似如下的内容。
可能需要在每个源代码面板的标题上拖动鼠标才能看到 "Dual Source"(双源)标签左边界的 "Race Accesses"(竞争访问)度量。
要检查 prime_pthr.c 中的数据争用,可以使用在2.2.2 创建数据争用检测实验中创建的实验之一。
要使用 er_print 显示 prime_pthr_instr.er 实验中的数据争用信息,请键入以下命令:
% er_print prime_pthr_inst.er |
在 (er_print) 提示符下,键入 races 可看到类似如下的输出:
(er_print) races Total Races: 4 Experiment: prime_pthr_inst.er Race #1, Vaddr: (Multiple Addresses) Access 1: Write, is_prime + 0x00000270, line 27 in "prime_pthr.c" Access 2: Write, is_prime + 0x00000270, line 27 in "prime_pthr.c" Total Callstack Traces: 2 Race #2, Vaddr: 0xffbfe714 Access 1: Write, main + 0x0000025C, line 60 in "prime_pthr.c" Access 2: Read, work + 0x00000070, line 40 in "prime_pthr.c" Total Callstack Traces: 1 Race #3, Vaddr: (Multiple Addresses) Access 1: Write, work + 0x00000150, line 44 in "prime_pthr.c" Access 2: Write, work + 0x00000150, line 44 in "prime_pthr.c" Total Callstack Traces: 2 Race #4, Vaddr: 0x21a90 Access 1: Write, work + 0x00000198, line 45 in "prime_pthr.c" Access 2: Write, work + 0x00000198, line 45 in "prime_pthr.c" Total Callstack Traces: 2 (er_print) |
该程序的此次特定运行期间发生了四次数据争用。
要在线程分析器中打开 prime_pthr_inst.er 实验,请键入以下命令:
% tha prime_pthr_inst.er |
以下屏幕抓图显示了在 prime_pthr.c 中检测到的争用,如线程分析器所显示。请注意,这些争用与 er_print 所显示的争用相同。
prime_pthr.c 中显示了四个数据争用:
Race #1(争用 1)是第 27 行函数 is_prime 中对 pflag[ ] 数组的一次写入与同一行中对 pflag[ ] 的另一次写入之间的数据争用。
Race #2(争用 2)是第 60 行中对 main() 中内存位置 i 的一次写入与第 40 行中对同一内存位置(在 work() 中为 *arg)的一次读取之间的数据争用。
Race #3(争用 3)是第 44 行中对 primes[total]
的一次写入与同一行中对 primes[total]
的另一次写入之间的数据争用。
Race #4(争用 4)是第 45 行中对 total 的一次写入与同一行中对 total 的另一次写入之间的数据争用。
如果选择 Race #2(争用 2),然后单击 "Dual Source"(双源)标签,则会看到两个源代码位置,类似于以下屏幕抓图。
Race #2(争用 2)的第一次访问位于第 60 行,显示在顶部面板中。第二次访问位于第 40 行,显示在底部面板中。源代码的左侧会突出显示 "Race Accesses"(争用访问)度量。此度量显示了该行上报告的数据争用访问总次数。
线程分析器的 "Races"(争用)标签中列出的每个数据争用还有一个或多个相关联的调用栈跟踪。这些调用栈显示了代码中导致数据争用的执行路径。单击调用栈跟踪时,右侧面板中的 "Race Details"(争用详细信息)标签会显示导致数据争用的函数调用。
本节介绍诊断数据争用原因的基本策略。
误报数据争用是指线程分析器已报告但实际并未发生的数据争用。线程分析器会尽可能减少误报数量,但在某些情况下,该工具无法完成精确的工作并可能误报数据争用。
由于误报的数据争用不是真正的数据争用,并不会影响程序的行为,因此可将其忽略。
有关误报数据争用的一些示例,请参见2.5 误报。有关如何从报告中消除误报数据争用的信息,请参见A.1 线程分析器用户 API。
良性数据争用是指特意允许的数据争用,其存在不会影响程序正确性。
一些多线程应用程序会特意使用可能导致数据争用的代码。由于这些数据争用是专门设计的,所以不需要进行修复。但在某些情况下,要使这样的代码正确运行会相当棘手。应仔细检查这些数据争用。
有关良性争用的更多详细信息,请参见2.5 误报。
线程分析器可以帮助查找程序中的数据争用,但是它无法自动查找程序中的错误,也不会建议如何修复找到的数据争用。数据争用可能是由某个错误引入的。这种情况下,必须找到并修复该错误。仅仅消除数据争用并不是正确的做法,这样做可能会使进一步的调试更加困难。
下面介绍如何修复 prime_omp.c 中的错误。有关所列的完整文件内容,请参见2.1.2 prime_omp.c 的源代码。
将第 50 行和第 51 行移动到 critical(临界)段中,以便消除数组 primes[ ] 的元素上的数据争用。
47 #pragma omp parallel for 48 for (i = 2; i < N; i++) { 49 if ( is_prime(i) ) { #pragma omp critical { 50 primes[total] = i; 51 total++; } 52 } 53 }
也可以按照如下所示将第 50 行和第 51 行移动到两个 critical(临界)段中,但此更改方式无法更正程序:
47 #pragma omp parallel for 48 for (i = 2; i < N; i++) { 49 if ( is_prime(i) ) { #pragma omp critical { 50 primes[total] = i; } #pragma omp critical { 51 total++; } 52 } 53 }
包含第 50 行和第 51 行的临界段将会消除数据争用,因为线程会使用互斥锁控制它们对 primes[ ] 数组的访问。但是,程序仍是错误的。两个线程可能会使用同一 total 值更新 primes[ ] 的同一元素,而 primes[ ] 的某些元素可能根本不会被赋值。
第二个数据争用(第 23 行中从 pflag[ ] 的读取与第 26 行中对 pflag[ ] 的写入之间的数据争用)实际上是良性争用,因为它并不会导致错误结果。没有必要修复良性数据争用。
下面介绍如何修复 prime_pthr.c 中的错误。有关所列的完整文件内容,请参见2.1.3 prime_pthr.c 的源代码。
使用一个互斥锁消除第 44 行中 prime[ ] 上的数据争用以及第 45 行中 total 上的数据争用。
第 60 行中对 i 的写入与第 40 行中从同一内存位置(名称为 *arg)的读取之间的数据争用,以及第 27 行中 pflag[ ] 上的数据争用,表明不同的线程对变量 i 进行的共享访问存在问题。prime_pthr.c 中的初始线程在第 60-62 行通过循环方式创建子线程,并调度它们处理函数 work()。循环索引 i 按地址传递到 work()。由于所有线程都访问 i 的同一内存位置,因此每个线程的 i 值不会保持唯一,而会随着初始线程对循环索引的递增而改变。由于不同的线程使用同一个 i 值,因此就会发生数据争用。修复该问题的一种方法是按值(而不是按地址)将 i 传递给 work()。
下面是更正后的 prime_pthr.c 版本:
1 /* 2 * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All Rights Reserved. 3 * @(#)prime_pthr_fixed.c 1.3 (Oracle) 10/03/26 4 */ 5 6 #include <stdio.h> 7 #include <math.h> 8 #include <pthread.h> 9 10 #define THREADS 4 11 #define N 10000 12 13 int primes[N]; 14 int pflag[N]; 15 int total = 0; 16 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 17 18 int is_prime(int v) 19 { 20 int i; 21 int bound = floor(sqrt(v)) + 1; 22 23 for (i = 2; i < bound; i++) { 24 /* no need to check against known composites */ 25 if (!pflag[i]) 26 continue; 27 if (v % i == 0) { 28 pflag[v] = 0; 29 return 0; 30 } 31 } 32 return (v > 1); 33 } 34 35 void *work(void *arg) 36 { 37 int start; 38 int end; 39 int i; 40 41 start = (N/THREADS) * ((int)arg) ; 42 end = start + N/THREADS; 43 for (i = start; i < end; i++) { 44 if ( is_prime(i) ) { 45 pthread_mutex_lock(&mutex); 46 primes[total] = i; 47 total++; 48 pthread_mutex_unlock(&mutex); 49 } 50 } 51 return NULL; 52 } 53 54 int main(int argn, char **argv) 55 { 56 int i; 57 pthread_t tids[THREADS-1]; 58 59 for (i = 0; i < N; i++) { 60 pflag[i] = 1; 61 } 62 63 for (i = 0; i < THREADS-1; i++) { 64 pthread_create(&tids[i], NULL, work, (void *)i); 65 } 66 67 i = THREADS-1; 68 work((void *)i); 69 70 for (i = 0; i < THREADS-1; i++) { 71 pthread_join(tids[i], NULL); 72 } 73 74 printf("Number of prime numbers between 2 and %d: %d\n", 75 N, total); 76 77 return 0; 78 }
有时,线程分析器可能会报告实际在程序中并未发生的数据争用。我们将这些称为误报。大多数情况下,误报是由用户自定义的同步导致的,或由不同线程回收的内存导致的。有关更多信息,请参见2.5.1 用户自定义的同步和2.5.2 由不同线程回收的内存。
线程分析器可以识别由 OpenMP、POSIX 线程和 Solaris 线程提供的大多数标准同步 API 和构造。但是,该工具无法识别用户自定义的同步,如果代码中包含这样的同步,可能会误报数据争用。
为了避免报告此类误报的数据争用,线程分析器提供了一组 API,可用于在执行用户自定义的同步时通知该工具。有关更多信息,请参见A.1 线程分析器用户 API。
为了说明为何需要使用这些 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); 104 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 的值设置为 1 以表明已生成该数据,然后调用 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 行的读取之前执行。因此,该工具认为它们是同时执行的,并报告它们之间的数据争用。
libtha(3C) 手册页和A.1 线程分析器用户 API说明了如何使用 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() 接口执行的内存分配和释放操作。
为了获得更好的性能,某些多线程应用程序会特意允许数据争用。良性数据争用是指特意允许的数据争用,其存在不会影响程序正确性。以下示例将说明良性数据争用。
除了良性数据争用之外,有很大一类的应用程序允许数据争用,因为它们依赖于锁无关 (lock-free) 和等待无关 (wait-free) 算法,不过很难正确设计这些算法。线程分析器可以帮助确定这些应用程序中的数据争用位置。
prime_omp.c 中的线程通过执行函数 is_prime() 来检查某个整数是否为质数。
16 int is_prime(int v) 17 { 18 int i; 19 int bound = floor(sqrt(v)) + 1; 20 21 for (i = 2; i < bound; i++) { 22 /* no need to check against known composites */ 23 if (!pflag[i]) 24 continue; 25 if (v % i == 0) { 26 pflag[v] = 0; 27 return 0; 28 } 29 } 30 return (v > 1); 31 }
线程分析器会报告第 26 行中对 pflag[ ] 的写入与第 23 行中对 pflag[ ] 的读取之间存在数据争用。但是,此数据争用是良性的,因为它不会影响最终结果的正确性。在第 23 行,线程将检查对于给定的 i 值,pflag[i]
是否等于 0。如果 pflag[i]
等于 0,则表明 i 是已知的合数(换句话说,知道 i 不是质数)。因此,无需检查 v 是否可以被 i 整除;只需检查 v 是否可以被某个质数整除。因此,如果 pflag[i]
等于 0,该线程会继续处理 i 的下一个值。如果 pflag[i]
不等于 0 并且 v 可以被 i 整除,该线程会将 0 分配给 pflag[v]
以表明 v 不是质数。
从正确性方面来说,多个线程检查同一 pflag[ ] 元素并同时向其写入是可以的。pflag[ ] 元素的初始值为 1。当线程更新该元素时,它们会为该元素分配值 0。也就是说,这些线程会在该元素的同一内存字节的同一位中存储 0。在当前的体系结构中,可以放心地认为这些存储是原子操作。这意味着,某个线程读取该元素时,读取的值要么为 1,要么为 0。如果某个线程在给定的 pflag[ ] 元素(第 23 行)被分配值 0 之前对该元素进行检查,则该线程会执行第 25–28 行。在此期间,如果另一个线程也将 0 分配给同一 pflag[ ] 元素(第 26 行),最终结果不变。在本质上,这意味着第一个线程不必要地执行了第 25–28 行,但最终结果是相同的。
一组线程同时调用 check_bad_array() 以检查数组 data_array 中是否存在任何“错误”元素。每个线程检查数组的不同部分。如果线程发现某个元素有错误,它会将全局共享变量 is_bad 的值设置为真。
20 volatile int is_bad = 0; ... 100 /* 101 * 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 的初始值为 0。当线程更新 is_bad 时,它们会为其分配值 1。也就是说,这些线程会在 is_bad 的同一内存字节的同一位中存储 1。在当前的体系结构中,可以放心地认为这些存储是原子操作。因此,某个线程读取 is_bad 时,读取的值要么为 0,要么为 1。如果某个线程在 is_bad(第 108 行)被分配值 1 之前对其进行检查,则该线程会继续执行 for 循环。在此期间,如果另一个线程也将值 1 分配给 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。因此,所有线程将一致地读取它们。如果未使用内存屏障,此编程方法将失效。