原文地址: https://blog.regehr.org/archives/490, 翻译并略作改动。

竞态条件(race Condition)是当事件的时间或顺序影响程序的正确性时发生的缺陷。一般来说,需要某种外部计时或排序非确定性来产生竞态条件;典型的例子有上下文切换、操作系统信号、多处理器上的内存操作和硬件 中断。

当程序中有两次内存访问时,就会发生数据竞争(Data Race):

  • 目标为同一内存位置
  • 由两个线程同时执行
  • 不是读取操作
  • 不是同步操作

上边这个定义来自微软研究院的 Sebastian Burckhardt。该定义的两个方面需要注意:

  • “同时”意味着没有像锁这样的东西强制一个操作在另一个操作之前或之后发生。
  • “不是同步操作”是指程序可能包含特殊的内存操作,例如用于实现锁的操作,这些操作本身并不同步。

在实践中,它们两者存在相当大的重合:许多 Race Condition 是由 Data Race 引起的,并且许多 Data Race 导致 Race Condition。另一方面,两者也可以相互独立,可能产生没有 Data Race 的 Race Condition,也可能产生没有 Race Condition 的 Data Race。

让我们从一个在两个银行账户之间转移资金的简单函数开始:

transfer1 (amount, account_from, account_to) {
  if (account_from.balance < amount) return NOPE;
  account_to.balance += amount;
  account_from.balance -= amount;
  return YEP;
}

当然,这并不是银行真正转移资金的方式,但这个例子非常有用。我们知道,账户余额应该是非负的,并且转移之后不能凭空创造(多出)或损失(丢失)金钱。当在没有外部同步的情况下从多个线程调用时,该函数会产生 Data Race(多个线程可以同时尝试更新帐户余额)和 Race Condition(在并行上下文中它将创造或损失金钱)。

我们可以尝试这样修复它:

transfer2 (amount, account_from, account_to) {
  atomic {
    bal = account_from.balance;
  }
  if (bal < amount) return NOPE;
  atomic {
    account_to.balance += amount;
  }
  atomic {
    account_from.balance -= amount;
  }
  return YEP;
}

这里的“atomic”(原子性)是由语言运行时实现的,也许简单地通过在原子块开始时获取线程互斥体(Mutex)并在结束时释放它,也许使用某种事务(Transaction),或者也许通过禁用中断 —— 出于示例的目的,只要 atomic 块内的代码以原子方式执行就能解决竞争问题。

当被多个线程调用时,transfer2 没有数据竞争,但显然它是一个极其愚蠢的函数,包含 Race Condition,这将导致它几乎与非同步函数 transfer1 一样严重地创造或损失金钱。从技术角度来看,transfer2 的问题在于它允许其他线程查看关键不变量(金钱守恒)被破坏的内存状态。

为了保持不变性,我们必须使用更好的锁定策略。只要原子的语义是在块的任何出口处结束原子部分,解决方案就可以很直接:

transfer3 (amount, account_from, account_to) {
  atomic {
    if (account_from.balance < amount) return NOPE;
    account_to.balance += amount;
    account_from.balance -= amount;
    return YEP;
  }
}

该函数不受 Data Race 和 Race Condition 的影响。我们可以稍微改变一下它以制作一个具有 Data Race 但没有 Race Condition 的示例吗?这很简单:

transfer4 (amount, account_from, account_to) {
  account_from.activity = true;
  account_to.activity = true;
  atomic {
    if (account_from.balance < amount) return NOPE;
    account_to.balance += amount;
    account_from.balance -= amount;
    return YEP;
  }
}

在这里,我们设置标志来指示帐户上发生了某种活动。这些标志上的 Data Race 有害吗?也许不是。例如,在晚上,我们可能会关闭所有事务处理线程,然后选择 10 个随机帐户,这些帐户被标记为具有手动审核活动。为此,Data Race 是完全无害的。

我们最终涵盖了所有可能性:

data race no data race 
race conditiontransfer1transfer2 
no race conditiontransfer4 transfer3 

本练习的重点是查看 transfer2transfer4,它们说明免于 Data Race 是一个非常弱的属性,对于建立计算机程序的并发正确性来说既不是必要的也不是充分的。为什么有人关心 Data Race?有几个原因:

  • 首先,只要小心一点,无 Data Race 的程序就可以被证明独立于它所在的任何弱内存模型的突发奇想。这让我们呼吸更轻松,因为推理在弱内存模型上运行的活跃程序对人类来说几乎是不可能的。
  • 其次,Data Race 是 C 和 C++ 中未定义的行为,并且在其他语言中往往具有复杂的语义(请参阅这篇文章)。
  • 第三,Data Race 很容易自动发现 —— 从定义中可以看出,它只需要监控内存访问;不需要了解应用程序语义。另一方面,Race Condition 与应用程序级不变量密切相关,这使得它们更难以推理。

这篇文章中的所有内容都非常明确,但我发现那些比较了解的人对 Data Race 和 Race Condition 之间的区别感到非常困惑(例如,因为他们正在研究并发正确性)。甚至当人们完全清楚基本概念时,也非常容易混淆它们,他们有时会说“Race Condition”,而实际上真正的意思是“Data Race”,这让事情变得更加混乱。当然,我发现自己也在这样做。


相关阅读