数据竞争是否糟糕?

时间:2013-12-04 11:34:53

标签: multithreading theory race-condition

我喜欢解决理论计算问题。

假设一切都是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()

但是我认为两个程序都是相同的,它们产生相同的输出,具有相同的种族问题。然而不知怎的,人们试图说服我数据竞争状况不好,我不明白为什么我的第一个程序会比我的第二个程序更差。

3 个答案:

答案 0 :(得分:3)

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

  

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

现在,假设这是正确的(它的维基百科,它往往在编程上相当不错,但确实经常是非常错误的),它定义了数据竞争"在这种情况下,纯粹是一个明显不好的案件;那些可能导致价值冲突的人。显然必须避免这种情况,因此必须避免在这里明确定义数据竞争。

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

我通常会在比赛条件上留下原始答案:


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

这不好吗?那要看。 在任何其他人之前注意。不仅许多案例都不好,我将在下面详细描述,但那些不好的案例往往特别难以找到并修复,这本身就应该倾向于使情况更糟。

数据竞争不好的一个明显例子是破坏数据的地方。我们假设我们更改了您的示例,以便xy大于架构的字大小,并且我们正在设置x = -1。我们还假设有两个补码。现在,y的可能值不仅包括-10,还包括-42949672964294967295

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

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

例如,考虑我们从0开始,然后:

X = 0

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

假设Thread 0 Thread 1 ++x x = -50 字大小或更小,我们仍然可能有问题。如果操作不是并发的,则有两个可能的值。 x可以等于x(增量,然后分配-50)或-50可以等于x(分配-50然后增加)。但是,同时我们最终可能-49的值为x,因为线程0读取1,线程1指定0然后线程0增量并指定-50

现在,很可能这完全没问题。它很可能虽然不是。

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

  1. 识别数据竞争。确定它是无害的(或相对无害的*),并让它成为。
  2. 识别数据竞争。确定它可能导致问题并修复它。
  3. 识别数据竞争。只需修复它,因为这样我们就无法确定它实际上是无害的时候是无害的。
  4. 识别数据竞争。确定它可能导致问题。更改代码,以便比赛不会导致问题。
  5. 案例编号2的重要性显而易见 - 我们将具有错误的代码转换为不是代码。

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

    数字1的重要性更复杂,在一些非常低级别的并发代码中避免锁定很重要,因此有些情况下我们想要容忍种族。 4号是一种将数字2从第2位变为数字1的方法,当数据竞争是问题所固有的时候出现(我们无法删除它)或者我们做的那种低数字1涉及的级别并发。

    这是C#中一个有趣的例子:

    1

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

    重点第1点:如果您决定容忍这样的比赛,您应该添加评论以及为什么。否则,每当有人遇到此代码时,他们都必须再次检查它是否安全,而不是检查您所说的推理

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

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

    theResource

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

    总而言之,数据竞争本身并不是坏事。它们虽然使事情变得复杂,但这些并发症可能会导致问题。当您进行数据竞争时,您可以选择证明它不是问题,更改您的代码以容忍竞争,以便它不再是问题,或者更改您的代码以消除竞争。其中,只是取消比赛通常是最简单的选择。

    *我的意思并不是“相对无害的”#34;在这里模糊的方式,但相对于替代方案。例如。如果我们决定在给出的C#示例中离开比赛,那是因为我们已经确定冗余对象创建的成本比防止它的相对成本的危害小。

答案 1 :(得分:0)

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

答案 2 :(得分:0)

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

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

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

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