是否可以在两个 CPU 内核上同时执行两个 lock() 语句? CPU内核是否同时滴答作响?

Posted

技术标签:

【中文标题】是否可以在两个 CPU 内核上同时执行两个 lock() 语句? CPU内核是否同时滴答作响?【英文标题】:Is it possible for two lock() statements to be executed at the same time on two CPU cores ? Do CPU cores tick at the same time? 【发布时间】:2021-07-03 21:40:14 【问题描述】:

我搜索了一个答案,并且我大致了解如何在多线程环境中使用锁等。这个问题困扰了我很长时间,我想我不是唯一一个。 TL;最后是 DR。

在我的情况下,我想防止从多个线程调用的方法在被另一个线程调用时被执行。

现在 C# 中的正常锁定场景如下所示:

static readonly object _locker = new object();

private static int counter;

public void Increase()

  lock (_locker)
  
    _counter++;//Do more than this here
  

据我了解,object _locker 充当bool,指示当前是否正在执行该方法。如果方法是“免费的”,则将其设置为锁定并执行方法并随后释放。如果方法被锁定,等待解锁,锁定,执行,解锁。

附加问题 1: 重复调用此方法是否保证了类似队列的行为?忽略父线程中的阻塞可能会导致问题的事实。想象Increase() 是父线程中的最后一次调用。

附加问题 2: 以布尔方式使用 object 感觉很奇怪。是否每个object 都包含一个“正在使用”标志,而一个“原始”object 仅用于包含此标志?为什么不Boolean

附加问题3:lock()如何修改readonly

功能上我也可以这样写:

static Boolean _locker = false;

private static int counter;

public void Increase()

  while(_locker)//Waits for lock==0
  
  

  _locker = true;//Sets lock=1

  _counter++;//Do more than this here

  _locker = false;//Sets lock=0

虽然锁示例看起来复杂且安全,但第二个示例感觉不对劲,不知何故在我脑海中敲响了警钟。

这个方法是否有可能在完全相同的 CPU 周期被两个内核同时执行?

我知道这是“但有时”的极端。我相信操作系统调度程序确实将线程从一个应用程序拆分到多个内核,那么为什么不应该同时在两个内核上执行汇编指令“加载_locked 的值以进行比较”?即使隔一个周期进入该方法,“read for comparison”和“write true to _locked”也会同时执行。

这甚至没有考虑到一行 C# 可以/将转换为多个汇编指令,并且在确认 locked==0 并写入 locked=1 后可能会中断一个线程。因为一行C#可以产生很多汇编指令,连lock()都可能被打断?

显然,这些问题以某种方式解决或避免了,我非常感谢您解释我的思维过程在哪里出错或我缺少什么。

TL;DR 两个 CPU 内核可以同时执行 lock() 语句吗?我无法解释软件在没有大的性能影响的情况下避免这种情况。

【问题讨论】:

你的第二个代码 sn-p 有缺陷,会出现并发问题。使用锁()。非竞争锁足够快;在您测量之前不要假设您有性能问题。 TLDR;锁使用 CPU 特性来确保一次只有一个线程可以进入锁。 objects 包含实现此功能所需的任何内部运行时状态,您无需知道那是什么。为什么反对?因为 Java 就是这样做的。 lock() 保证是原子的(在检查它是否被锁定和设置锁定状态之间不能被中断),但是你的 while 循环不是。反馈您的问题,不要在一个帖子中提出多个问题。 【参考方案1】:

是的,两个内核可以同时获取两个不同的锁。原子 RMW 操作只需要一个“缓存锁”,而不是全局总线锁,on现代 CPU。例如此测试代码 (on Godbolt) 是 C++ 代码,它编译为一个循环,该循环仅重复 xchg [rdi], ecx,每个线程在不同的缓存行中使用不同的 std::atomic<int> 对象。我的 i7-6700k 上的程序的总运行时间是 463 毫秒,无论它是在 1 个线程还是 4 个线程上运行,因此排除了任何类型的系统范围的总线锁定,确认 CPU 只使用MESI cache-lock within the core doing the RMW 来确保它是原子的不会干扰其他内核的操作。 无争用锁在每个线程只重复锁定/解锁自己的锁时完美扩展。

获取最后由另一个内核释放的锁可能会使该锁延迟数百个时钟周期(40 到 70 纳秒是典型的内核间延迟),以便 RFO(读取所有权)完成并获得独占所有权的缓存线,但不必重试或任何东西。 Atomic RMW 涉及内存屏障(在 x86 上),因此锁定后的内存操作甚至无法启动,因此 CPU 内核可能会停滞一段时间。与正常的加载/存储相比,这里的成本很高,乱序 exec 无法隐藏以及其他一些事情。


不,两个核心不能同时获得相同1,这就是互斥锁的全部意义所在。正确实现的没有与您的 spin-wait 示例相同的错误,然后单独存储 true

(注 1:对于一些固定的n,您可以使用计数锁/信号量来允许最多 n 线程进入临界区,您要解决的资源管理问题不是简单的互斥。但你只是在谈论互斥体。)


获取锁的关键操作是原子 RMW,例如 x86 xchg [rcx], eaxlock cmpxchg [rcx], edx,它存储了 1 (true) 和 相同的操作 检查旧值是什么。 (Can num++ be atomic for 'int num'?)。在 C++ 中,这意味着使用 std::atomic<bool> lock; / old = lock.exchange(true); 在 C# 中,你有 Interlocked.Exchange()。这将关闭您的尝试包含的竞赛窗口,其中两个线程可以退出 while(_locker) 循环,然后都盲目地存储 _locker = true

另请注意,如果您不使用volatileVolatile.Read() 阻止编译器假设没有其他线程正在写入您正在读取/写入的变量,那么滚动您自己的自旋循环会出现问题。 (没有 volatile,while(foo) 可以通过将明显循环不变的负载提升到循环外来优化为 if(!foo) infinite_loop)。

(实现锁的另一个有趣的部分是,如果它在您第一次尝试时可用,该怎么办。例如,您保持旋转多长时间(如果是这样,那么究竟如何,e.g. the x86 pause instruction between read-only checks) , 在等待时使用 CPU 时间,然后回退到进行系统调用以将 CPU 放弃给另一个线程或进程,并让操作系统在锁再次可用或可能再次可用时唤醒你。但这都是性能调整;实际上获取锁是围绕一个原子 RMW。)


当然,如果您要自己滚动,请使用 Interlocked.Increment(ref counter); 将增量本身设置为无锁原子 RMW,例如 in MS's docs


是否每个对象都包含一个“正在使用”标志,而一个“原始”对象只是用于包含此标志?为什么不是布尔值?

我们从对象大小知道 C# 不这样做。也许你应该只使用lock (counter) counter++; 而不是发明一个单独的。如果您没有想要管理的现有对象,而是使用一些更抽象的资源(例如调用某个函数),则使用虚拟对象将是有意义的。 (如果我错了,请纠正我,我不使用 C#;我在这里只是为了 cpu 架构和程序集标签。lock() 是否需要对象,而不是像 int 这样的原始类型?)

我猜他们做了 std::atomic<T> 的普通 C++ 实现对太大而无法无锁的对象所做的事情:实际互斥锁或自旋锁的哈希表,索引通过 C# 对象地址。 Where is the lock for a std::atomic?

即使这种猜测并不完全是 C# 所做的,但这种心智模型可以理解这种无需在每个对象中保留空间即可锁定任何东西的能力。

这可能会产生额外的争用(通过对两个不同的对象使用相同的互斥锁)。它甚至可能在不应该存在的地方引入死锁,这是实现必须解决的问题。也许通过将被锁定对象的身份放入互斥锁中,因此索引同一互斥锁的另一个线程可以看到它实际上被用于锁定不同的对象,然后对其进行处理......这可能是一个“托管”语言出现; Java 显然做了同样的事情,您可以锁定任何对象而无需定义单独的锁。

(C++ std::atomic 没有这个问题,因为互斥锁是在库函数中获取/释放的,不可能同时尝试获取两个锁。)


CPU 内核是否同时滴答作响?

不一定,例如英特尔“服务器”芯片(大多数至强)让每个内核独立控制其倍频器。然而,即使在多插槽系统中,所有内核的时钟通常仍来自同一来源,因此它们可以保持其 TSC(计算参考周期,而不是内核时钟)在内核之间同步。

英特尔“客户端”芯片,如 i7-6700 等台式机/笔记本电脑芯片,实际上确实为所有内核使用相同的时钟。内核要么处于低功耗睡眠(时钟停止),要么以与任何其他活动内核相同的时钟频率运行。

这些都与锁定或使原子 RMW 操作真正原子化无关,并且可能应该拆分为单独的 Q&A。我确信有很多非 x86 示例,但我碰巧知道 Intel CPU 是如何做事的。

【讨论】:

以上是关于是否可以在两个 CPU 内核上同时执行两个 lock() 语句? CPU内核是否同时滴答作响?的主要内容,如果未能解决你的问题,请参考以下文章

用户模式和内核模式:同时使用不同的程序

Linux上如何查看物理CPU个数,核数,线程数

linux内核调度算法--CPU时间片如何分配

linux内核调度算法--CPU时间片如何分配 转!

什么是逻辑处理器?

是否可以同时运行属于不同应用程序的两个内核?