在某些东西变得线程安全之前,你有多低?

时间:2008-12-12 07:28:50

标签: c++ multithreading thread-safety

我一直在想,在某些东西是自动线程安全的之前你需要做的事情有多深?

快速举例:

int dat = 0;
void SetInt(int data)
{
    dat = data;
}

..这种方法会被视为线程安全吗?为了确保,我通常将所有的设置方法都包含在互斥锁中,但每次我这样做时,我都忍不住认为这是一个无用的性能开销。我想这一切都分解为编译器生成的程序集?什么时候线程能够闯入代码?每个汇编指令还是每个代码行?在设置或销毁方法堆栈期间线程是否可以中断?像i ++这样的指令是否会被认为是线程安全的 - 如果不是,那么++ i呢?

Lotsa在这里提出问题 - 我不希望直接回答,但有关该主题的一些信息会很棒:)

[更新]因为现在对我来说很明显(对于你们这些人来说,3),线程中唯一原子保护的东西是汇编指令,我知道这个问题:互斥和信号量包装类怎么样? ?像这样的类通常使用制作callstacks的方法 - 而且通常使用某种内部计数器的自定义semaphoreclasses不能保证原子/线程安全(无论你想要什么,只要你知道我的意思,我不在乎: P)

10 个答案:

答案 0 :(得分:4)

考虑:

1)编译器优化 - “dat”是否按计划存在?除非它是“外部可观察”行为,否则C / C ++抽象机器不保证编译器不会优化它。您的二进制代码中可能根本没有“dat”,但您可能正在写入寄存器,而线程将/可能具有不同的寄存器。在抽象机器上阅读C / C ++标准,或者只是谷歌“挥发”并从那里探索。 C / C ++标准关注单线程健全性,多线程可以轻易地绊倒这种优化。

2)原子商店。任何有可能跨越字界的东西都不是原子的。 Int-s通常是,除非你将它们打包成具有例如字符的结构,并使用指令来删除填充。但是你每次都需要分析这个方面。研究你的平台,谷歌搜索“填充”。请记住,不同的CPU有不同的规则。

3)多CPU问题。你在CPU0上写了“dat”。甚至可以在CPU1上看到变化吗?或者你会写一个本地注册?缓存?缓存是否与您的平台保持一致?访问是否保证按顺序保存?阅读“弱记忆模型”。 Gogle为“memory_barriers.txt Linux” - 这是一个良好的开端。

4)用例。您打算在分配后使用“dat” - 是同步的吗?但我猜这很明显。

通常“线程安全”不会超出保证函数同时从不同线程调用的情况下工作,但这些调用不能相互依赖,即它们不会交换任何数据。那个电话。例如,您从thread1和thread2调用malloc()并且它们都获取内存,但它们不访问彼此的内存。

反例是strtok(),它不是线程安全的,并且在不相关的调用时会收支平衡。

一旦您的线程开始通过数据相互交谈,通常的线程安全性并不能保证很多。

答案 1 :(得分:3)

通常,线程上下文切换可以在任何两个汇编语言指令之间的任何时间发生。 CPU完全不知道汇编语言如何映射到源代码。此外,对于多个处理器,其他指令可以在同一时间在不同的CPU内核上执行。

话虽如此,在示例中,您将CPU大小的单词分配给内存位置通常是原子操作。这意味着从观察者(另一个线程)的角度来看,分配尚未开始或已经完成。中间没有国家。

多处理有许多细微之处,因此了解您正在使用的硬件和操作系统环境的可能性是很好的。

答案 2 :(得分:3)

线程状态可以在任何两个机器指令之间切换。如果计算机能够在单个机器指令中执行分配,则分配应该在单个处理器机器上是线程安全的。通常,假设可以在单个指令中计算分配右侧的计算结果并将其存储在分配的左侧指定的位置,这是不安全的。在某些处理器上可能没有可用的存储器到存储器复制指令,并且可能需要首先将数据加载到寄存器中。如果在加载和存储指令之间发生上下文切换,则赋值的结果是不确定的(不是线程安全的)。这是大多数指令集包含原子测试和设置操作的原因之一,该操作允许您将内存位置用作锁。这允许其他线程检查锁定可用性并等待继续,直到获得锁定。

在您的情况下,我不确定操作是否在硬件级别以线程安全的方式完成,因为执行分配的多个竞争线程的结果只是让其中一个完成了存储最后和“赢”。如果您在右侧执行任何类型的计算,但是涉及使用多个变量的计算,那么我肯定会把它放在一个关键部分,因为您希望计算结果与状态一致计算开始时的那些变量。如果不是在一个关键部分,那么变量的值可能会在另一个线程的中间流中发生变化,并且最终会得到任何一个线程无法实现的结果。

答案 3 :(得分:3)

在大多数平台(包括x86)上,“本机”数据类型(32位)的分配是原子的。这意味着赋值将完全发生,并且您不会冒险使用“中途更新”的dat变量。但这是你获得的唯一保证。

我不确定double数据类型的assignemnt。您可以在x86规范中查找它,或检查.NET是否做出任何明确的保证。但一般来说,不是“原生大小”的数据类型不是原子的。更小的那些,比如bool可能不是(因为写一个bool,你可能必须读取整个32位字,覆盖一个字节,然后再写入整个32字节的字)

通常,线程可以在任何两个汇编指令之间中断。 这意味着上面的代码是线程安全的,只要你不尝试从dat 读取(你可能认为这会使它变得毫无用处)。

原子性和线程安全性并不完全相同。线程安全完全取决于上下文。你对dat的赋值是原子的,所以读取dat值的另一个线程要么看到旧的值,要么看到新值,但绝不会看到“介于两者之间”。但这并不能保证线程安全。另一个线程可能会读取旧值(比如它是数组的大小),并根据它执行操作。但是,您可能会在读取旧值后立即更新dat,也许可以将其设置为较小的值。另一个线程现在可以访问新的较小的数组,但相信它具有较旧的较大尺寸。

i ++和++我也线程安全,因为它们包含多个操作(读取值,增量值,写入值),并且通常包含读取和写入的任何内容不是线程安全的。 在为函数调用设置调用堆栈时,也可以中断线程,是的。在任何汇编程序指令之后。

答案 4 :(得分:2)

确保某些东西自动线程安全的唯一方法是确保没有可变的共享状态。这就是功能编程如今越来越受欢迎的原因。

因此,如果所有线程共享X,那么您必须确保X不会更改。任何更改的变量必须是该线程的本地变量。

答案 5 :(得分:2)

这是线程安全的,它并不适用于各种情况。

假设 dat 变量保存数组中元素的数量。另一个线程开始使用 dat 变量扫描数组,并缓存其值。在此期间,您可以更改 dat 变量的值。另一个线程再次扫描阵列以进行其他操作。另一个线程是使用旧的 dat 值还是新值?我们不知道,我们不能确定。根据模块的编译,它可能使用旧的缓存值或新值,这两种情况都有问题。

您可以在另一个线程上显式缓存 dat 变量的值,以获得更可预测的结果。例如,如果此 dat 变量保存超时值并且您只写入此值而另一个线程读取,那么我在这里看不到问题。即使是这种情况,也不能说这是线程安全的 !!!

答案 6 :(得分:1)

好吧,我不相信一切都必须是线程安全的。由于使代码线程安全的复杂性和性能都有成本,因此在实现任何内容之前,您应该问自己代码是否需要是线程安全的。在许多情况下,您可以将线程感知限制为代码的特定部分。

显然,这需要一些思考和规划,但编写线程安全代码也是如此。

答案 7 :(得分:0)

增量操作在x86处理器上并不安全,因为它不是原子的。在Windows上,您需要调用InterlockedIncrement函数。此函数生成完整的内存条件。您也可以使用intel线程构建块(TBB)库中的tbb :: atomic。

答案 8 :(得分:0)

对交易记忆有很多研究 类似于数据库交易的东西,但更精细。

理论上,这允许多个线程读/写对象做任何他们喜欢的事情。但是对象上的所有操作都是事务感知的。如果一个线程修改了一个对象状态(并完成了它的事务),那么在该对象上具有打开事务的所有其他线程将被回滚并自动重新启动。

这是在硬件级别完成的,因此软件不需要涉及与锁定相关的问题。

好的理论。等不及它成为现实。

答案 9 :(得分:-4)

上面的代码是线程安全的!

要注意的主要是静态(即共享)变量。

这些不是线程安全的,除非更新由某种锁定机制(如互斥锁)管理。这显然适用于任何操作系统提供的共享内存。

因此,只要您的代码没有静态数据,它本身就是线程安全的。

然后,您需要检查您使用的库或系统调用是否是线程安全的。这在大多数系统调用的文档中明确说明。