联锁和线程安全操作



1.出于好奇,当以下操作同时从2或3个线程调用时,它们在幕后会做什么?

Interlocked.Add(ref myInt, 24);
Interlocked.Increment(ref counter);

C#是否创建了一个内部队列,告诉线程2,现在轮到您执行操作,然后它告诉线程1现在轮到您,然后线程3执行操作?这样他们就不会互相干扰了?

2.为什么C#不自动执行此过程?

当程序员在多线程方法中编写以下内容时,这不是很明显吗:

myByte++;
Sum = int1 + int2 + int3;

并且这些变量与其他线程共享,他是否希望这些操作中的每一个都作为原子操作执行而不中断?

为什么程序员必须明确地告诉它这样做?

难道这不是每个程序员都想要的吗?这不是";互锁的";方法只是给语言增加了不必要的复杂性?

谢谢。

像下面这样的操作在幕后做什么

就其内部实现方式而言,当存在争用时,CPU硬件会仲裁哪个核心获得缓存线的所有权。请参阅';int num';?以获得C++和x86 asm/cpu体系结构的详细说明。

关于:为什么CPU和编译器希望早加载晚存储:
请参阅Java指令重新排序和CPU内存重新排序
Atomic RMW防止了这种情况,在大多数ISAs上,seq_cst存储语义也是如此,您先进行纯存储,然后进行全屏障存储。(AArch64在stlrldar之间有一个特殊的交互,以防止StoreLoad对seq_cst操作进行重新排序,但仍允许与其他操作一起进行重新排序。)

当程序员在多线程方法中编写类似以下的东西时[…]

这到底意味着什么?它不是在多个线程中运行相同的方法,这是一个问题,它是在访问共享数据。编译器应该如何知道哪些数据将同时从多个线程非只读访问,而不是在关键部分内?

一般来说,没有合理的方法来证明这一点,只有在一些简单的情况下如果一个编译器要尝试,它必须是保守的,错误地将更多的东西变成原子,这将付出巨大的性能代价另一种错误是正确性问题,如果编译器根据一些未记录的启发法猜测错误,就会发生这种情况,这将使该语言无法用于多线程程序。

除此之外,并不是所有的多线程代码都需要始终保持顺序一致性;通常获取/释放或放松原子是好的,但有时不是。对于程序员来说,明确他们的算法建立在什么样的排序和原子性上是很有意义的

此外,您还仔细设计了无锁的多线程代码,以便按合理的顺序进行操作。在C++中,您不必使用Interlock...,而是可以创建一个变量std::atomic<int> shared_int;。(或者使用std::atomic_ref<int>对其他代码可以非原子访问的变量执行原子操作,比如使用Interlocked函数)。

在源代码中没有明确指示哪些操作是原子操作,以及什么排序语义,这将使读取和维护此类代码变得更加困难。正确的无锁算法不仅仅是通过让编译器将单个运算符转换为原子操作来实现的。


每个操作提升为原子操作将破坏性能。大多数数据是不共享的,即使在访问某些共享数据结构的函数中也是如此。

原子RMW(如x86lock add [rdi], eax)比非原子操作慢得多,特别是因为非原子操作允许编译器将变量优化到寄存器中。

x86上的原子RMW是一个完整的内存屏障,因此每次使用+=++时,使每个操作都成为原子操作会破坏内存级别的并行性。

例如,如果在L1d高速缓存中处于热状态,则lock xadd [mem], reg的Skylake上每18个周期吞吐量为一个,而add reg, reg的吞吐量为每0.25个周期一个(https://uops.info),更不用说消除优化客场和联合作战的机会了。并降低了无序执行导致工作重叠的能力。

这是您在评论中提出的问题的部分答案:

为什么不呢?作为一名程序员,如果我确切地知道应该把这些保护放在哪里,为什么编译器不能呢?

为了让编译器做到这一点,它需要了解程序中所有可能的执行路径。这实际上就是此处讨论的路径测试问题:https://softwareengineering.stackexchange.com/questions/277693/does-path-coverage-guarantee-finding-all-bugs

这篇文章指出,这个相当于停顿问题,这是计算机科学的一个因为它说这是一个无法解决的问题。

最酷的是,你想在一个可能有多个处理器运行多个执行线程的世界里做到这一点。这使得无法解决的问题更加难以解决。

另一方面,程序员应该知道他/她的程序做什么。。。

最新更新