Sun Studio 12:线程分析器用户指南

第 2 章 数据争用教程

以下是有关如何使用线程分析器检测和修复数据争用的详细教程。本教程由以下部分组成:

2.1 教程源文件

本教程依赖两个包含数据争用的程序:

2.1.1 omp_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  } 

2.1.2 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
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.2.1 omp_prime.c pthr_prime.c 中的数据争用

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

2.2 创建实验

线程分析器沿用与 Sun Studio 性能分析器相同的“收集分析”模型。使用线程分析器包括以下三个步骤:

2.2.1 对源代码进行校验

为了在程序中启用数据争用检测,必须首先使用特殊的编译器选项编译源文件。对 C、C++ 和 Fortran 语言来说,此特殊选项是:-xinstrument=datarace

-xinstrument=datarace 编译器选项添加到现有的用来编译程序的一组选项。只能将该选项应用于您怀疑有数据争用的源文件。


注 –

确保在编译程序时指定 -g。为检测争用而编译程序时,不要指定高优化级别。使用 -xopenmp=noopt 编译 OpenMP 程序。使用高优化级别时,报告的信息(如行号和调用栈)可能是不正确的。


以下是对源代码进行校验的示例命令:

2.2.2 创建数据争用检测实验

collect 命令与 -r on 标志一起使用,以运行程序并在执行过程中创建数据争用检测实验。对于 OpenMP 程序,请确保所用的线程超过一个。以下是创建数据争用实验的示例命令:

为增大检测到数据争用的可能性,建议将 collectr race 标志一起使用,以创建若干个数据争用检测实验。在不同的实验中使用不同的线程数和不同的输入数据。

2.2.3 检查数据争用检测实验

可以使用线程分析器、性能分析器或 er_print 实用程序检查数据争用检测实验。线程分析器和性能分析器都提供 GUI 界面;前者提供的是一组简化的缺省选项卡,但在其他方面与性能分析器完全相同。

线程分析器 GUI 具有菜单栏、工具栏和包含各种选项卡的拆分窗格(不同选项卡对应不同的显示)。在左窗格上,缺省情况下显示以下三个选项卡:

在线程分析器显示屏的右窗格上,显示以下两个选项卡:

另一方面,er_print 实用程序提供命令行界面。在使用 er_print 实用程序检查争用时,以下子命令很有用:

有关更多信息,请参阅 collect.1tha.1analyzer.1er_print.1 手册页。

2.3 了解实验结果

本部分说明如何使用 er_print 命令行和线程分析器 GUI 显示有关检测到的每个数据争用的以下信息:

2.3.1 omp_prime.c 中的数据争用

% 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

图 2–1 在 omp_primes.c 中检测到的数据争用

显示 omp_primes.c 的 Races(争用)选项卡的线程分析器窗口屏幕拍图。

omp_primes.c 中有以下四个数据争用:

2.3.2 pthr_prime.c 中的数据争用

% 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

图 2–2 在 pthr_primes.c 中检测到的数据争用

显示 pthr_primes.c 的 Races(争用)选项卡的线程分析器窗口屏幕拍图

pthr_prime.c 中有以下六个数据争用:

GUI 的一个优势在于,它允许您并排查看与数据争用关联的两个源位置。例如,在 Races(争用)选项卡中选择 pthr_prime.c 的六号争用,然后单击“双重数据源”选项卡。您将看到以下内容:

图 2–3 数据争用的源位置详细信息

显示 Races Source(争用源)选项卡信息的线程分析器窗口屏幕拍图。

在顶部 "Race Source" 窗格中显示六号争用(第 39 行)的第一次访问,在底部窗格中则显示该数据争用的第二次访问。突出显示其中发生数据争用访问的源代码(第 39 行和第 65 行)。在每个源代码行的左侧显示缺省度量(互斥争用访问度量)。该度量显示在该行上报告的数据争用访问次数。

2.4 诊断数据争用的原因

此部分提供诊断数据争用原因的基本策略。

2.4.1 检查数据争用是否为误报

误报数据争用是线程分析器报告了实际上未发生的数据争用。线程分析器尝试减少误报数。但是,存在该工具无法执行准确的作业并可能误报数据争用的情况。

可以忽略误报的数据争用,因为它不是真正的数据争用,因此不会影响程序的行为。

有关误报数据争用的一些示例,请参见 2.5 误报。有关如何避免误报数据争用的信息,请参见A.1 线程分析器的用户 API

2.4.2 检查数据争用是否为良性

良性数据争用是指其存在不会影响程序正确性的有意数据争用。

有些多线程应用程序会有意使用可能导致数据争用的代码。由于那里的数据争用是设计使然,因此无需进行修复。但是,在某些情况下,使这样的代码正确运行是相当棘手的。应仔细检查这些数据争用。

有关良性争用的更多详细信息,请参见 2.5 误报

2.4.3 修复错误而不是修复数据争用

线程分析器可以帮助查找程序中的数据争用,但是它无法自动查找程序中的错误,也无法建议如何修复所找到的数据争用。数据争用也可能是由错误引入的。找到并修复错误是很重要的。仅仅消除数据争用并不是正确的方法,这样做可能会使进一步调试变得更加困难。修复错误而不是修复数据争用。

2.4.3.1 修复 omp_prime.c 中的错误

以下说明如何修复 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[] 中的某些元素可能根本未赋值。

2.4.3.2 修复 pthr_prime.c 中的错误

以下说明如何修复 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() 中更新 totalprimes[] 的同时报告结果(第 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 误报

有些情况下,线程分析器可能会报告程序中未真正发生的假数据争用。这些情况称为误报。大多数情况下,误报是由2.5.1 用户定义的同步2.5.2 由不同线程再循环的内存导致的。

2.5.1 用户定义的同步

线程分析器可以识别由 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

2.5.2 由不同线程再循环的内存

一些内存管理例程再循环线程释放的内存以供另一线程使用。线程分析器有时无法识别由不同线程使用的同一内存位置的使用期限不重叠。如果出现此情况,则该工具可能误报数据争用。以下示例说明此类误报。

   /*----------*/                         /*----------*/
    /* 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(),则 ptr1ptr2 将获取不同的值,因此这两个线程之间没有数据争用。但是,如果线程 2 在线程 1 调用 myfree() 之后调用 mymalloc(),则 ptr1ptr2 可能具有相同值。由于线程 1 不再访问该内存,因此不存在数据争用。但是,如果该工具不知道 mymalloc() 正在再循环内存,则它将报告 ptr1 数据的写入和 ptr2 数据的写入之间存在数据争用。当 C++ 运行时库为临时变量再循环内存时,此类误报通常发生在 C++ 应用程序中。它通常还发生在实现自己的内存管理例程的用户应用程序中。当前,线程分析器能够识别通过标准 malloc()calloc()realloc() 接口执行的内存分配和释放操作。

2.6 良性数据争用

一些多线程应用程序有意允许数据争用,以便获得更佳性能。良性数据争用是指其存在不会影响程序正确性的有意数据争用。以下示例说明良性数据争用。


注 –

除了良性数据争用外,大量应用程序允许数据争用,因为它们依赖于很难正确设计的锁释放和等待释放算法。线程分析器可以帮助确定这些应用程序中存在数据争用的位置。


2.6.1 用于查找素数的程序

文件 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 行。

2.6.2 用于验证数组值类型的程序

一组线程并发调用 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 循环的时间超过了所需时间。

2.6.3 使用双检锁的程序

单件可确保在整个程序中只有一个特定类型的对象。双检锁是一种常用的有效方法,用于在多线程应用程序中初始化单件。以下代码说明这样的实现。

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。因此,所有线程将一致地读取它们。如果未使用内存屏障,则此编程方法将失效。