【问题标题】:Are Data Races bad?数据竞赛不好吗?
【发布时间】:2013-12-20 21:00:41
【问题描述】:

我喜欢解决理论上的计算争论。

假设一切初始为 0

Thread0       Thread1
x=1       |   y=x

这里我们有一个数据竞赛。据我了解(假设 x 适合体系结构的字长并在字边界上对齐,通常是这样),结果是 x=1 ^ y=0 或 x=1 ^ y=1 .

现在我的第二个示例使用显式锁定(假设 lock() 获得一些全局锁定),据我了解,这不再是数据竞争条件。

Thread0       Thread1
lock()    |   lock()
x=1       |   y=x
unlock()  |   unlock()

但是我认为这两个程序是相同的,它们产生相同的输出,具有相同的种族问题。然而不知何故,人们试图说服我数据竞争条件很糟糕,我不明白为什么我的第一个程序会比我的第二个更糟糕。

【问题讨论】:

  • 是的,有时锁是不必要的。这取决于 x 和 y 以及 = 操作是否是原子的。但是当有疑问时锁定。
  • 它们是最糟糕的一种错误,您会因为不得不调试而感到不快。当然,这种锁定设计防止竞争,它只确保更新是原子的int 类型的变量实际上很少需要。细粒度锁定很少是正确的锁定。
  • @HansPassant 好吧,比赛本身并不是一个错误,因为有些比赛是可以容忍的,有些是必须的(如果比赛的部分条件与我们无法控制的事情有关)。我当然同意,当它们确实引起错误时,它们往往是特别讨厌的错误。
  • 我认为您的推理存在缺陷。或者,也许您应该定义“数据竞赛”。因为你说第一个程序有数据竞争,而“据 [你] 理解”第二个没有,但你也说它们是相同的。这是一个明显的矛盾:两件事不可能同时相同和不同。我同意他们是相同的。我也相信两者都有相同的数据竞赛。非常明确地说:第二个程序与第一个程序具有相同的数据竞争。
  • @JonHanna 是的。同时,我在下面阅读了您的答案,对此我没有什么要补充的。如果可以的话,我会投票五次。

标签: multithreading theory race-condition


【解决方案1】:

编辑。来自维基百科的完整引用是:

C++11 引入了对多线程的正式支持,并将数据竞争严格定义为非原子变量之间的竞争条件。虽然竞争条件通常会继续存在,但程序员必须避免“数据竞争”,如果访问是为了写入,程序员必须确保一次只有一个线程可以访问任何变量。

现在,假设这是正确的(它是维基百科,它在编程方面往往相当不错,但确实经常是非常错误的),它在这种情况下将“数据竞争”纯粹定义为明显不好的情况之一;那些可能导致价值剪切的东西。显然必须避免这种情况,因此必须避免数据竞争(此处定义的)。

根据这个定义,您问题中的两个程序都没有数据竞争。

我一般会在比赛条件下留下我原来的答案:


第二个例子也有数据竞争。事实上,它与第一个具有完全相同的数据竞争。

这很糟糕吗?那要看。 在其他任何内容之前请注意。正如我将在下面详细描述的那样,不仅许多案例很糟糕,而且那些糟糕的案例往往特别难以找到和修复,这本身就应该倾向于假设更糟糕的情况。

数据竞争不好的一个明显情况是它破坏了数据。假设我们更改了您的示例,使xy 大于体系结构的字长,我们正在设置x = -1。我们还将假设二进制补码。现在y 的可能值不仅是-10,还有-42949672964294967295

在这种情况下,您建议的锁定不会完全删除数据竞争,但会删除可能导致剪切的部分:y 的唯一可能值将再次是 -1 和 @987654331 @。

另一个问题是序列化。通常需要能够将一系列并发事件视为一组有限的顺序事件中的一个。

例如,假设我们以X = 0 开头,然后有:

Thread 0    Thread 1
++x         x = -50

现在,这里仍然存在可能导致虚假价值的风险。

假设x 是字大小或更小,我们仍然可能遇到问题。如果操作不是并发的,则有两个可能的值。 x 可以等于-50(递增,然后分配-50)或x 可以等于-49(分配-50 然后递增)。但是,同时我们可能会以 x 的值结束 1,因为线程 0 读取 0,线程 1 分配 -50,然后线程 0 递增并分配 1

现在,这很可能完全没问题。很有可能不是。

作为程序员,我们有四种可能性:

  1. 识别数据争用。确定它是无害的(或相对无害的*),然后让它成为现实。
  2. 识别数据争用。确定它可能会导致问题并加以解决。
  3. 识别数据争用。只需修复它,因为这样我们就不会错误地确定它是无害的,而实际上它并非如此。
  4. 识别数据争用。确定它会导致问题。更改代码,以免比赛引起问题。

案例 2 的重要性显而易见 - 我们将有错误的代码转换为没有错误的代码。

案例 3 的重要性归结为时间和可证明性。我们很可能会降低代码效率(许多停止数据竞争的方法至少有一些开销),但通常需要更少的开发人员时间来消除竞争而不是证明它是无害的,并且错误示例的成本是稍微慢一点的代码而在另一个方向出错的代价是难以修复的错误。

数字 1 的重要性更复杂,在一些非常低级的并发代码中避免锁定可能很重要,因此在某些情况下我们希望容忍竞争。数字 4 是一种将事物从数字 2 转变为数字 1 的方法,并且当数据竞争是问题所固有的(我们无法删除它)或者我们正在执行的那种低级并发时出现第 1 项涉及。

这是一个有趣的 C# 示例:

public static SomeResource GetTheResource()
{
  get
  {
    if(_theResource == null)
      _theResource = CreateTheResource();
    return _theResource
  }
}

数据竞争应该是显而易见的;在设置theResource 并且所有 CPU 的缓存都看到更新之前,我们可能会从不同的线程多次分配给它。这是一个错误吗?很多人会说是,但实际上这取决于。有可能在短时间内使用不同版本的theResource 是安全的,而我们真正失去的只是从多次调用CreateTheResource() 开始时的一些效率。在对性能有很高要求的代码中,我们可能会决定容忍这种最初的较低效率,以实现无锁定的长期效率增益。或者我们锁定可能是至关重要的。或者我们可能只是锁定,因为我们没有避免它的迫切需要,而且假设 可能 是一个问题更简单。

要点 1:如果您确实决定容忍这样的比赛,您应该添加评论,说明原因。否则,每次有人遇到此代码时,他们都必须再次检查它是否安全,而不是最多检查您陈述的推理

要点 2:虽然这里的原则与语言无关,但每种情况下的细节通常不是。在这种情况下,容忍竞争不仅取决于临时的多个副本是否安全,还取决于垃圾收集清理那些多余的副本。如果我们改为在 C++ 中分配一个指向堆的指针,那么即使在其他方面是安全的,上述内容也最好是泄漏的。

一个更复杂的情况是这样的(同样是一个 C# 示例,但适用于其他语言):

internal sealed class LockFreeQueue<T>
{
  private sealed class Node
  {
    public readonly T Item;
    public Node Next;
    public Node(T item)
    {
      Item = item;
    }
  }
  private volatile Node _head;
  private volatile Node _tail;
  public LockFreeQueue()
  {
    _head = _tail = new Node(default(T));
  }
#pragma warning disable 420 // volatile semantics not lost as only by-ref calls are interlocked
  public void Enqueue(T item)
  {
    Node newNode = new Node(item);
    for(;;)
    {
      Node curTail = _tail;
      if (Interlocked.CompareExchange(ref curTail.Next, newNode, null) == null)   //append to the tail if it is indeed the tail.
      {
        Interlocked.CompareExchange(ref _tail, newNode, curTail);   //CAS in case we were assisted by an obstructed thread.
        return;
      }
      else
      {
        Interlocked.CompareExchange(ref _tail, curTail.Next, curTail);  //assist obstructing thread.
      }
    }
  }    
  public bool TryDequeue(out T item)
  {
    for(;;)
    {
      Node curHead = _head;
      Node curTail = _tail;
      Node curHeadNext = curHead.Next;
      if (curHead == curTail)
      {
        if (curHeadNext == null)
        {
          item = default(T);
          return false;
        }
        else
          Interlocked.CompareExchange(ref _tail, curHeadNext, curTail);   // assist obstructing thread
      }
      else
      {
        item = curHeadNext.Item;
        if (Interlocked.CompareExchange(ref _head, curHeadNext, curHead) == curHead)
        {
          return true;
        }
      }
    }
  }
#pragma warning restore 420
}

此代码不会阻止数据争用,而是会对它们做出反应。如果一个操作受到另一个线程的影响,那么线程不会出错或返回不正确的结果,而是处理竞争并返回其他内容(在某些情况下甚至会帮助其他线程)。

因此,总而言之,数据竞争本身并不是坏事。它们虽然使事情复杂化,但这些复杂性可能会导致问题。当您遇到数据竞争时,您可以在证明它不是问题、更改代码以容忍竞争以使其不再是问题或更改代码以消除竞争之间做出选择。其中,仅移除种族通常是最简单的选择。

*我在这里的意思不是含糊不清的“相对无害”,而是相对于替代方案。例如。如果我们决定在给出的 C# 示例中退出竞争,那是因为我们已经确定创建冗余对象的成本比防止它的相对成本有害。

【讨论】:

  • 非常感谢您的回复,让很多事情更清楚了!我想我应该更清楚地说明我确实理解为什么任何类型的比赛条件都可能很糟糕。当我谈到数据竞赛时,我想我特别指的是 C11/C++11 数据竞赛。据我从其他资源中了解到(尚未找到权威资源),任何具有数据竞争的 C11/C++11 程序都会有未定义的结果(不仅仅是y=1y=0,如我的示例,但基本上是人类的终结)。我想我主要想知道为什么 C11 认为数据竞争如此糟糕以至于需要进行这样的处理。
  • 看看我最近的编辑;根据您提到的维基百科文章,C ++ 11 将“数据竞争”定义为那些可能导致剪切的竞争,正如我提到的第一个可能的不良影响。这里未定义的结果实际上可能包括 -4294967296 和 4294967295 或其他值(C++ 不假定二进制补码机器)。这些当然是要避免的,从标准的角度来看,没有他们可以定义的行为。您的示例没有这样的比赛,并且确实有定义的行为(either y==1y==0,但没有其他任何东西)。
【解决方案2】:

我感谢大家的回答,尽管他们并没有真正回答我希望我提出的问题。答案确实让我能够更好地推理我的实际问题,并最终在网上找到一些答案:

http://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-could-possibly-go-wrong

所以我想我的问题应该是:

C(++)11 标准将我的第一个示例定义为数据竞争(如果我不使用“atomic”关键字),而第二个则没有。因此,第一个具有未定义的行为(尽管似乎没有编译器实现会导致除x==1 &amp;&amp; y==0|1 之外的任何结果,根据标准,x 和 y 的任何结果值都是正确的编译器行为)。我想知道为什么会这样。我认为英特尔文档非常详尽地回答了这个问题。

【讨论】:

    【解决方案3】:

    如果 xy 适合机器寄存器,则默认情况下分配是原子的,因此锁不会改变结果。在第二种情况下,同样可能得到 y = 0 或 y = 1。

    【讨论】:

    • 谢谢,这是我的理解。因此我的问题是:为什么人们说数据竞赛不好?根据维基百科:“程序员必须避免“数据竞赛”在 C++11 中?
    • @Claude:我看待事物的方式只有在预期某个输出时才会存在数据竞争。如果您希望在执行这些指令后y 始终为 0,那么您将遇到数据竞争,因为它可能并不总是正确的。如果y 的值在这里无关紧要,那么就不存在数据竞争。
    • software.intel.com/en-us/blogs/2013/01/06/…,当涉及到真正的编译器时,事情似乎并不那么简单:(
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多