假设我有一个多线程应用程序并使用相同的输入运行它。检测每个负载和存储是否足以检测写入和写入数据竞争?我的意思是从记录的加载和存储地址中,如果我们可以看到哪个线程执行了哪个加载以及哪个线程执行了哪个存储,我们可以通过注意重叠的地址来检测写入-读取和写入-写入数据竞争。还是我错过了什么?
3 回答
还是我错过了什么?
你错过了很多。正如 Pubby 所说,如果您看到 read,然后在 T1 中写入,然后在 T2 中读取,然后再写入,您就不能说没有种族。您需要了解所涉及的锁。
您可能想要使用一种工具,例如 Google 的ThreadSanitizer。
更新:
但我的方法会涵盖所有比赛还是至少部分比赛?
您在此处和其他答案上的评论似乎表明您不了解比赛是什么。
你的方法可能会暴露一些种族,是的。保证不会覆盖其中的大部分(这将使练习徒劳无功)。
这是一个来自维基百科的简单示例,我稍作修改:
作为一个简单的例子,让我们假设两个线程 T1 和 T2 每个都想对全局整数的值执行加一的算术运算。理想情况下,会发生以下操作序列:
- 整数 i = 0; (记忆)
- T1 将 i 的值从内存中读入 register1: 0
- T1 增加寄存器 1 中 i 的值:(寄存器 1 内容)+ 1 = 1
- T1 将寄存器 1 的值存储在内存中:1
- T2 将 i 的值从内存中读入 register2: 1
- T2 乘以寄存器 2 中 i 的值:(寄存器 2 内容)* 2 = 2
- T2 将寄存器 2 的值存储在内存中:2
- 整数 i = 2; (记忆)
在上面显示的情况下,i 的最终值为 2,正如预期的那样。但是,如果两个线程同时运行而没有锁定或同步,则操作的结果可能是错误的。下面的替代操作序列演示了这种情况:
- 整数 i = 0; (记忆)
- T1 将 i 的值从内存中读入 register1: 0
- T2 将 i 的值从内存中读入 register2: 0
- T1 增加寄存器 1 中 i 的值:(寄存器 1 内容)+ 1 = 1
- T2 乘以寄存器 2 中 i 的值:(寄存器 2 内容)* 2 = 0
- T1 将寄存器 1 的值存储在内存中:1
- T2 将寄存器 2 的值存储在内存中:0
- 整数 i = 0; (记忆)
i 的最终值是 0,而不是预期的结果 2。这是因为第二种情况的递增操作不是互斥的。互斥操作是在访问某些资源(例如内存位置)时不能中断的操作。在第一种情况下,T1 在访问变量 i 时没有被中断,所以它的操作是互斥的。
所有这些操作都是原子的。出现竞争条件是因为这个特定的顺序与第一个顺序不具有相同的语义。你如何证明语义与第一个不同?好吧,您知道在这种情况下它们是不同的,但是您需要证明每个可能的顺序以确定您没有竞争条件。这是一件非常困难的事情,并且具有巨大的复杂性(可能是 NP-hard 或需要 AI-complete),因此无法可靠地检查。
如果某个订单永远不会停止会发生什么?你怎么知道它一开始就永远不会停止?你基本上只剩下解决停机问题了,这是一项不可能完成的任务。
如果您正在谈论使用连续读取或写入来确定比赛,请注意以下事项:
- 整数 i = 0; (记忆)
- T2 将 i 的值从内存中读入 register2: 0
- T2 乘以寄存器 2 中 i 的值:(寄存器 2 内容)* 2 = 0
- T2 将寄存器 2 的值存储在内存中:0
- T1 将 i 的值从内存中读入 register1: 0
- T1 增加寄存器 1 中 i 的值:(寄存器 1 内容)+ 1 = 1
- T1 将寄存器 1 的值存储在内存中:1
- 整数 i = 1; (记忆)
这与第一个具有相同的读取/存储模式,但给出不同的结果。
您将学到的最明显的事情是有多个线程使用相同的内存。这本身并不一定是坏事。
好的用途包括通过信号量、原子访问和 RCU 或双缓冲等机制进行保护。
不良用途包括竞争条件、真假共享:
- 竞争条件主要源于排序问题 - 如果某个任务 A 在其执行结束时写入某些内容,而任务 B 在其开始时需要该值,则最好确保仅在 A 完成后读取 B。信号量、信号或类似的东西是一个很好的解决方案。或者当然在同一个线程中运行它。
- 真正的共享意味着两个或多个内核积极地读写同一个内存地址。这会减慢处理器的速度,因为它必须不断地将任何更改发送到其他内核的缓存(当然还有内存)。你的方法可以捕捉到这一点,但可能不会突出它。
- 虚假共享甚至比真实共享更复杂:处理器缓存不是在单个字节上工作,而是在“缓存行”上工作——它保存多个值。如果核心 A 一直在敲击一行的字节 0,而核心 B 一直在写入字节 4,则缓存更新仍将停止整个处理器。