在某些东西本身变得线程安全之前,你会走多低?

Posted

技术标签:

【中文标题】在某些东西本身变得线程安全之前,你会走多低?【英文标题】:How low do you go before something gets thread-safe by itself? 【发布时间】:2008-12-12 07:28:49 【问题描述】:

我一直在想,在某些东西自动成为线程安全之前,你必须深入到什么程度?

快速示例:

int dat = 0;
void SetInt(int data)

    dat = data;

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

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

[更新] 因为我现在很清楚(谢谢你们

【问题讨论】:

更新了原始问题并进行了跟进:) 关于原子性和汇编指令,这不是真的。内存提取可能会在 CPU 执行其他操作时执行,或者某些形式的分支预测可能会执行两个分支并丢弃不正确的分支。 x86 确实有一些原子操作,但不是全部都有。 教科书的答案是,这取决于您遵循的线程标准,该标准通常提供一组特定的保证。在 POSIX 线程的情况下,除非您的编译器、平台库或更新的语言标准提供了原子操作,否则当另一个线程正在或可能正在访问它时,您不能修改任何对象...期间。 【参考方案1】:

注意事项:

1) 编译器优化——“dat”是否按您的计划存在?除非它是“外部可观察的”行为,否则 C/C++ 抽象机不保证编译器不会对其进行优化。您的二进制代码中可能根本没有“dat”,但是您可能正在写入寄存器,并且线程将/可能具有不同的寄存器。在抽象机器上阅读 C/C++ 标准,或者简单地在谷歌上搜索“volatile”并从那里进行探索。 C/C++ 标准关心单线程的完整性,多线程很容易在这样的优化中绊倒。

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

3) 多 CPU 问题。你在 CPU0 上写了“dat”。甚至会在 CPU1 上看到更改吗?或者你会写到本地寄存器吗?缓存?缓存是否与您的平台保持一致?访问是否保证井井有条?阅读“弱内存模型”。 Gogle 搜索“memory_barriers.txt Linux”——这是一个好的开始。

4) 用例。您打算在分配后使用“dat” - 这是同步的吗?但我想这是显而易见的。

通常“线程安全”并不仅仅保证一个函数在同时从不同线程调用时能够工作,但这些调用不能相互依赖,即它们不交换任何关于那个电话。例如,您从 thread1 和 thread2 调用 malloc(),它们都获得了内存,但它们不访问彼此的内存。

一个反例是 strtok(),它不是线程安全的,并且会在不相关的调用上达到平衡。

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

【讨论】:

+1 用于覆盖不连贯的缓存。那些谈论线程在“汇编指令之间”被中断的人完全忽略了这一点,仅仅因为你的操作已经完全执行并不意味着其他线程现在、很快或永远都会看到结果......【参考方案2】:

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

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

多处理有许多微妙之处,因此最好了解您正在工作的硬件和操作系统环境的可能性。

【讨论】:

是的,我是这么认为的。那么,在分配调用中,像 double 这样的非 cpu 大小的数据类型会是线程安全的吗?并且 int-assignment 是一个原子操作,难道你没有很好的事务同步问题吗?哦,我的,还有可移植性问题哦,好吧,猜猜它又回到了看反汇编.. Greg,您忘记了必须取消引用该成员(假设此设置器在一个类中)。据我所知(而且我知道的很少),生成的操作总是原子的。 不仅操作不是原子的(而且字长不是参数),而且它也不是围栏。要么使用特定的关键字来保证原子性,要么使用互斥锁等。 > "在任意两个汇编语言指令之间" 一些汇编指令(如 add)必须读取、修改,然后写回该内存。对于多核 CPU,即使是一条汇编指令也可能是不安全的。如果不能使用互斥体,则需要使用 'cmpxchg' 或类似的编译器内在函数。【参考方案3】:

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

在您的情况下,我不确定操作是否在硬件级别以线程安全的方式完成是否重要,因为执行分配的多个竞争线程的结果只是让其中一个完成最后存储并“获胜”。但是,如果您在右侧执行任何类型的计算,涉及使用多个变量的计算,那么我肯定会将其放在关键部分,因为您希望计算结果与状态一致计算开始时的那些变量。如果不在关键部分,变量的值可能会在另一个线程中途更改,您最终可能会得到任何一个线程都不可能的结果。

【讨论】:

【参考方案4】:

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

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

一般来说,线程可以在任意两条汇编指令之间中断。 这意味着只要您不尝试从 dat 中读取,您上面的代码就是线程安全的(您可能会争辩说,这使它相当无用)。

原子性和线程安全并不是一回事。线程安全完全取决于上下文。您对 dat 的分配是原子的,因此读取 dat 值的另一个线程将看到旧值或新值,但永远不会看到“介于两者之间”的值。但这并不能使它成为线程安全的。另一个线程可能会读取旧值(比如数组的大小),并基于该值执行操作。但是您可能会在读取旧值后立即更新 dat,也许将其设置为较小的值。另一个线程现在可能会访问您的新的、更小的数组,但相信它具有旧的、更大的大小。

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

【讨论】:

【参考方案5】:

确保某些东西自动成为线程安全的唯一方法是确保不存在可变的共享状态。这就是为什么现在函数式编程越来越受欢迎的原因。

所以,如果你所有的线程共享 X,那么你必须确保 X 不会改变。任何发生变化的变量都必须是该线程的本地变量。

【讨论】:

【参考方案6】:

不是线程安全的,它并不适用于所有情况。

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

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

【讨论】:

按照正常定义,代码是线程安全的。只有当调用程序不是线程安全的时候才会出现上述情况。【参考方案7】:

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

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

【讨论】:

【参考方案8】:

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

【讨论】:

【参考方案9】:

有很多关于事务性内存的研究。 类似于 DB 事务,但粒度更细。

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

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

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

【讨论】:

听起来确实很性感!但是,未来会很顺利:( 我读过一篇关于事务内存软件实现的论文,我认为是在 Haskell 中,它允许编译器强制执行一定程度的安全性(例如,事务函数不能调用不可逆的函数) )。显然,硬件帮助可以使这种东西的性能更高。【参考方案10】:

以上代码是线程安全的!

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

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

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

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

【讨论】:

啊!一定是“代码盲”,这根本不是线程安全的!对不起。

以上是关于在某些东西本身变得线程安全之前,你会走多低?的主要内容,如果未能解决你的问题,请参考以下文章

java 之前的安全的类回顾,以及以后需要线程安全时使用哪些类

什么是线程安全,实现线程安全都有哪些方法

对于单例模式面试官会怎样提问呢?你又该如何回答呢?

如何使用 Java 在多线程环境中测试某些东西 [重复]

线程安全测试

Java中线程安全的集合浅析