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

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