【问题标题】:Confusion about definition of data race对数据竞争定义的困惑
【发布时间】:2019-03-19 04:17:41
【问题描述】:

当程序中有两个内存访问时,就会发生数据竞争:

  • 定位相同的位置
  • 由两个线程同时执行
  • 不读取
  • 不是同步操作

这个定义取自from,它是从一篇研究论文中借用的,所以我们可以假设它是正确的。

现在考虑这个例子:

import java.util.concurrent.*;

class DataRace{
   static boolean flag = false;
   static void raiseFlag() {
      flag = true;
   }
   public static void main(String[] args) {
      ForkJoinPool.commonPool().execute(DataRace::raiseFlag);
      System.out.println(flag);
  }
}

据我了解,这符合数据竞赛的定义。我们有两条指令访问相同的位置(标志),它们都不是读取,都是并发的并且不是同步操作。所以输出取决于线程如何交错,可以是“真”或“假”。

如果我们假设这是一场数据竞争,那么我可以在访问之前添加锁来解决这个问题。但是即使我在两个线程中都添加了锁,我们也知道锁中也存在竞争条件。所以任何线程都可以获得锁,输出仍然可以是“真”或“假”。

所以这是我的困惑,下面是我想问的两个问题:

  1. 这是一场数据竞赛吗?如果没有,为什么不呢?

  2. 如果是数据竞争,为什么建议的解决方案不起作用?

【问题讨论】:

    标签: multithreading concurrency race-condition data-race


    【解决方案1】:

    首先,线程执行的任意顺序并不是数据竞争本身,即使它可能导致它。如果您需要同步 2 个或更多线程以按特定顺序执行其代码,则必须使用像 monitors 这样的等待机制。监视器是可以进行互斥(锁定)等待的结构。监视器也称为条件变量,Java supports 它们。

    现在的问题是数据竞赛是什么。当 2 个或更多线程同时访问同一内存位置并且其中一些访问是写入时,就会发生数据竞争。这种情况会导致内存位置可能包含不可预测的值。

    一个经典的例子。让我们有一个 32 位操作系统和 64 位长的变量,如 longdouble 类型。让我们拥有long 变量。

    long SharedVariable;
    

    以及执行以下代码的线程 1。

    SharedVariable=0;
    

    以及执行以下代码的线程 2。

    SharedVariable=0x7FFF_FFFF_FFFF_FFFFL;
    

    如果对该变量的访问不受锁保护,则在两个线程执行后,SharedVariable 可以具有以下值之一。

    SharedVariable==0
    SharedVariable==0x7FFF_FFFF_FFFF_FFFFL
    **SharedVariable==0x0000_0000_FFFF_FFFFL**
    **SharedVariable==0x7FFF_FFFF_0000_0000L**
    

    最后 2 个值是意外的 - 由数据争用引起。

    这里的问题是,在 32 位操作系统上,可以保证对 32 位变量的访问是原子的 - 因此平台保证即使 2 个或更多线程同时访问相同的 32 位内存位置对该内存位置的访问是原子的——只有一个线程可以访问这样的变量。但是因为我们有 64 位变量,在 CPU 级别上,写入 64 位长变量将转换为 2 个 CPU 指令。所以代码SharedVariable=0;被翻译成这样的:

    mov SharedVariableHigh32bits,0
    mov SharedVariableLow32bits,0
    

    而代码SharedVariable=0x7FFF_FFFF_FFFF_FFFFL; 被翻译成这样的:

    mov SharedVariableHigh32bits,0x7FFFFFFF
    mov SharedVariableLow32bits,0xFFFFFFFF
    

    没有锁,CPU可以按照以下顺序执行这4条指令。

    订单 1。

    mov SharedVariableHigh32bits,0 // T1
    mov SharedVariableLow32bits,0 // T1
    mov SharedVariableHigh32bits,0x7FFFFFFF // T2
    mov SharedVariableLow32bits,0xFFFFFFFF // T2
    

    结果是:0x7FFF_FFFF_FFFF_FFFFL

    订单 2。

    mov SharedVariableHigh32bits,0x7FFFFFFF // T2
    mov SharedVariableLow32bits,0xFFFFFFFF // T2
    mov SharedVariableHigh32bits,0  // T1
    mov SharedVariableLow32bits,0  // T1
    

    结果是:0

    订单 3。

    mov SharedVariableHigh32bits,0x7FFFFFFF // T2
    mov SharedVariableHigh32bits,0 // T1
    mov SharedVariableLow32bits,0 // T1
    mov SharedVariableLow32bits,0xFFFFFFFF // T2
    

    结果是:0x0000_0000_FFFF_FFFFL

    订单 4。

    mov SharedVariableHigh32bits,0 // T1
    mov SharedVariableHigh32bits,0x7FFFFFFF // T2
    mov SharedVariableLow32bits,0xFFFFFFFF // T2
    mov SharedVariableLow32bits,0 // T1
    

    结果是:0x7FFF_FFFF_0000_0000L

    因此,竞争条件导致了一个严重的问题,因为您可以获得一个完全出乎意料且无效的值。通过使用锁,您可以阻止它,但是仅仅使用锁并不能保证执行顺序——哪个线程首先执行它的代码。因此,如果您使用锁,您将只获得 2 个执行顺序 - 顺序 1 和顺序 2,不会获得意外值 0x0000_0000_FFFF_FFFFL0x7FFF_FFFF_0000_0000L。但是,如果您需要同步哪个线程先执行哪个第二个,您不仅需要使用锁,还需要使用监视(条件变量)提供的等待机制。

    顺便说一句,根据article,Java 保证对除longdouble 之外的所有原始类型变量进行原子访问。在 64 位平台上,即使是对 longdouble 的访问也应该是原子的,但看起来标准并不能保证这一点。

    即使标准保证原子访问,使用锁总是更好。锁定义了memory barriers,它阻止了一些编译器优化,这些优化可以在 CPU 指令级别重新排序您的代码,并在使用变量控制执行顺序时导致问题。

    所以这里的简单建议是,如果您不是并发编程专家(我也不是),并且不要编写需要通过使用无锁技术获得绝对最大性能的软件,请始终使用锁- 即使访问保证具有原子访问的变量。

    【讨论】:

    • 对内存区域的原子访问取决于 1) 编译器为给定的高级语言操作生成的 CPU 指令 2) 对象的对齐 3) CPU 的具体保证;操作系统不参与其中
    • 即使在人们期望数据竞争是无害的平台上,优化器也可能有其他想法。例如,在某些 ARM 平台上,给定uint16_t a; uint32_t b;b = a*0x01000001; 的最有效代码会暂时扰乱b 的值即使它已经恰好等于a*0x01000001。跨度>
    猜你喜欢
    • 1970-01-01
    • 2011-01-26
    • 1970-01-01
    • 2012-04-24
    • 1970-01-01
    • 2011-12-02
    • 1970-01-01
    • 1970-01-01
    • 2013-05-09
    相关资源
    最近更新 更多