7

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

快速示例:

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

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

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

[更新] 因为我现在很清楚(谢谢你们 <3),线程中唯一保证原子的东西是汇编指令,我知道我开始思考:互斥锁和信号量包装类呢?像这样的类通常使用制作调用堆栈的方法 - 并且通常使用某种内部计数器的自定义信号量类不能保证是原子/线程安全的(无论你想怎么称呼它,只要你知道我的意思,我不在乎: )

4

10 回答 10

4

考虑因素:

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() ,它不是线程安全的,并且会在不相关的调用上达到平衡。

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

于 2008-12-12T10:34:45.677 回答
3

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

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

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

于 2008-12-12T07:33:08.757 回答
3

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

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

于 2008-12-12T07:54:09.870 回答
3

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

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

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

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

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

于 2008-12-12T08:59:19.193 回答
2

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

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

于 2008-12-12T07:35:32.827 回答
2

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

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

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

于 2008-12-12T08:02:45.063 回答
1

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

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

于 2008-12-12T09:04:23.520 回答
0

Increment operation isn't thrad safe on x86 processors because it is not atomic. On windows you need to call InterlockedIncrement functions. This function generate full memory barier. Also you can use tbb::atomic from intel threading building blocks(TBB) library.

于 2008-12-12T08:11:02.973 回答
0

有很多关于事务性记忆的研究。
类似于数据库事务的东西,但粒度要细得多。

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

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

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

于 2008-12-12T10:45:48.340 回答
-4

The above code is threadsafe!

The main thing to look out for is static (i.e.shared) variables.

These are not thread safe unless update is managed by some sort of locking machanism such as a mutex. The same obviously applies to any OS provided shared memory.

So as long as your code has no static data it will be thread safe in itself.

You then need to check whether any libraries or system calls you use are thread safe. This is stated explicitly in the documentation of most system calls.

于 2008-12-12T08:10:29.103 回答