【问题标题】:Lock that handles a high-contention, high-frequency situation处理高争用、高频情况的锁
【发布时间】:2016-11-15 02:29:33
【问题描述】:

我正在寻找一种在您有两个线程不断尝试以非常高的频率释放和重新获取同一个锁的情况下优雅降级的锁实现。

当然,很明显,在这种情况下,两个线程不会显着并行处理。从理论上讲,最好的结果是先运行整个线程 1,然后运行整个线程 2,而不进行任何切换——因为切换只会在这里产生大量开销。因此,我正在寻找一种锁定实现,它可以通过在切换前保持同一线程运行一段时间而不是不断切换来优雅地处理这种情况。

问题的长版本

我自己很想用“你的程序坏了,不要那样做”来回答这个问题,这里有一些理由说明我们为什么会陷入这种情况。

锁是“单一全局锁”,即非常粗糙的锁。 (它是 PyPy 中的全局解释器锁(GIL),但问题是关于如何做一般来说,如果你有一个 C 程序。)

我们有以下情况:

  • 一直存在争用。在这种情况下这是意料之中的:锁是一个全局锁,大多数线程需要获取它才能继续进行。所以我们预计其中很大一部分正在等待锁定。这些线程中只有一个可以进行。

  • 持有锁的线程有时可能会爆发短暂的释放。一个典型的例子是,如果这个线程重复调用“外部事物”,例如对文件的许多短写入。这些写入中的每一个通常都很快完成。锁仍然必须释放,以防万一这个外部事情比预期的花费更长的时间(例如,如果写入实际上需要等待磁盘 I/O),以便在这种情况下另一个线程可以获取锁。

如果我们对锁使用一些标准的互斥锁,那么一旦所有者释放锁,锁通常会切换到另一个线程。但问题是,如果程序运行多个线程,每个线程都想做长时间的短时间释放。程序最终花费大部分时间在 CPU 之间切换锁。

在切换之前运行同一个线程一段时间要快得多,至少只要在很短的时间内释放锁。 (例如,在 Linux/pthread 上,即使有其他等待线程,在获取后立即释放有时会立即重新获取锁;但我们希望在大多数情况下都有这样的结果,而不仅仅是有时。)

当然,一旦锁被释放了更长的时间,那么将锁的所有权转移到不同的线程是一个好主意。

所以我正在寻找有关如何做到这一点的一般想法。我猜它应该已经存在于某个地方——在论文中,还是在某个多线程库中?

作为参考,PyPy 尝试通过轮询来实现类似的东西:锁只是一个全局变量,具有同步的比较和交换,但没有操作系统调用;其中一个等待线程被赋予“窃取者”的角色;该“窃取者”线程每 100 微秒唤醒一次以检查变量。这并不是很糟糕(除了正在运行的线程消耗的 100% 时间之外,它还可能花费 1-2% 的 CPU 时间)。这实际上实现了我在这里所要求的,但问题是这是一个不完全支持更传统的锁情况的 hack:例如,如果线程 1 尝试向线程 2 发送消息并等待回答,两个线程切换平均每个需要 100 微秒 --- 如果消息被快速处理,这将是太多了。

【问题讨论】:

  • 为什么自旋锁在你的情况下不起作用?
  • 不确定你真正需要什么,但你有没有想过stdatomic.h
  • 嗯,有一个问题你不必解决,它永远不会是“高频”。没有可以按下的神奇按钮,您必须对它进行不同的编程。如今,许多语言运行时都很好地支持反应式编程。你必须在 C 中自己动手。
  • 这与人们通常寻找的完全相反。一般来说,高质量的锁具有良好的公平性。你的问题归结为“我用什么样的锁来野蛮地饿死我的一个线程?”。
  • 如果我理解正确,您需要一个具有“宽限期”的锁。一旦被释放,除了原主人之外,其他任何人都无法在短时间内抓住它。一旦间隔过去,任何服务员都可以抓住它。这是正确的吗?

标签: c multithreading


【解决方案1】:

作为参考,让我描述一下我们最终是如何实现它的。我不确定它,因为它仍然感觉像是一个 hack,但它似乎在实践中适用于 PyPy 的用例。

我们按照问题最后一段中的描述进行了操作,并添加了一个:“窃取者”线程每 100 微秒检查一次全局变量,通过调用 pthread_cond_timedwaitWaitForSingleObject 来执行此操作,系统提供的互斥锁,超时时间为 100 微秒。这提供了一个具有全局变量和常规互斥锁的“复合锁”。如果“窃取者”注意到值 0 是全局变量(每 100 微秒),则“窃取者”将成功窃取“锁”,如果常规互斥锁被另一个线程释放,则立即。 p>

然后是根据具体情况选择如何释放复合锁的问题。大多数外部函数(写入文件等)通常会很快完成,因此我们通过写入全局变量来释放和重新获取复合锁。只有在少数特定的函数情况下——比如 sleep() 或 lock_acquire()——我们期望调用线程经常阻塞;围绕这些函数,我们通过实际释放互斥锁来释放复合锁。

【讨论】:

    【解决方案2】:

    如果我理解问题陈述,您是在要求内核调度程序对您的用户空间应用程序“热”线程是否会在非常不久的将来尝试重新获取锁进行有根据的猜测,以通过允许“不太热”的线程获取互斥锁来避免隐式抢占它。

    我不知道内核如何做到这一点。我想到的只有两件事:

    • 不要释放互斥体,除非热线程实际上正在转换为空闲(应用程序特定条件)。在 Linux 中,您可以使用MONOTONIC_COARSE 来尝试减少检查挂钟以实现某种定时器的开销。
    • 增加热线程优先级。这更像是一种缓解策略,尝试减少热线程的抢占量。如果可以识别“热”线程,您可以执行以下操作:
    pthread_t thread = pthread_self();
    
    //Set max prio, FIFO
    struct sched_param params;
    params.sched_priority = sched_get_priority_max(SCHED_FIFO);
    
    int rv = pthread_setschedparam(thread, SCHED_FIFO, &params);
    if(rv != 0){
      //Print error
      //...
    }
    

    【讨论】:

      【解决方案3】:

      Spinlock 可能更适合您的情况。它们避免了上下文切换,并且在线程可能只在短时间内持有锁的情况下非常高效。

      正因为如此,它们被广泛用于操作系统内核。

      【讨论】:

      • 有问题的锁只持有很短的时间(问题标题中的“高争用”部分)。
      猜你喜欢
      • 1970-01-01
      • 2021-07-19
      • 1970-01-01
      • 2018-01-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-01-11
      • 1970-01-01
      相关资源
      最近更新 更多