锁定处理高争用,高频率的情况

时间:2016-07-12 17:48:55

标签: c multithreading

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

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

问题的长版本

由于我自己很想通过“你的程序被破坏,不要那样做”来回答这个问题,这里有一些理由说明为什么我们最终会遇到这种情况。

锁定是“单一全局锁定”,即非常粗略的锁定。 (它是PyPy中的全局解释器锁(GIL),但问题是关于如何一般地执行它,比如说你有一个C程序。)

我们有以下情况:

  • 一直存在争议。在这种情况下,这是预期的:锁是一个全局锁,需要获取大多数线程才能进行。所以我们希望他们中的很大一部分都在等待锁定。这些线程中只有一个可以进展。

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

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

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

当然,只要锁定被释放一段时间,就可以将锁的所有权转移到另一个线程。

所以我正在寻找有关如何做到这一点的一般想法。我想它应该存在于某个地方---在一篇论文中,或者在一些多线程库中?

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

3 个答案:

答案 0 :(得分:1)

作为参考,让我描述一下我们如何最终实现它。我不确定,因为它仍然感觉像是一种骇客,但实际上它在PyPy的用例中似乎有用。

我们按照问题的最后一段所述进行了操作,并增加了一个内容:“窃取器”线程每100微秒检查一次全局变量,方法是调用pthread_cond_timedwaitWaitForSingleObject带有系统提供的常规互斥体,超时时间为100微秒。这给出了具有全局变量和常规互斥锁的“复合锁”。如果“窃取者”注意到全局变量0(每100微秒)值为0,则将成功窃取“锁”;如果常规互斥锁被另一个线程释放,则会立即成功。 >

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

答案 1 :(得分:0)

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

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

  • 请勿释放互斥体,除非热线程实际上正在转换为空闲状态(特定于应用程序的条件)。在Linux中,您可以使用MONOTONIC_COARSE来减少检查壁钟以实现某种计时器的开销。
  • 增加热线三重奏。这是一种缓解策略,它可以在 attempt 中减少热线程的抢占量。如果可以识别“热”线程,则可以执行以下操作:
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
  //...
}

答案 2 :(得分:-1)

Spinlock在您的情况下可能会更好。它们避免了上下文切换,并且如果线程可能仅在短时间内保持锁定,则效率很高。

由于这个原因,它们被广泛用于OS内核中。