【问题标题】:Windows critical sections fairnessWindows 临界区公平性
【发布时间】:2014-06-24 07:15:21
【问题描述】:

我对使用 EnterCriticalSection 和 LeaveCriticalSection 方法在 Windows 上的关键部分的公平性有疑问。 MSDN 文档规定:“不能保证线程获得临界区所有权的顺序,但是,系统对所有线程都是公平的。” 这个问题来自我写的一个应用程序,它阻塞了一些永远不会进入临界区的线程,即使是在很长一段时间之后;所以我用一个简单的c程序进行了一些测试,以验证这种行为,但是当你有很多线程并且内部有一些等待时间时,我注意到了奇怪的结果。 这是测试程序的代码:

CRITICAL_SECTION CriticalSection;

DWORD WINAPI ThreadFunc(void* data) {
  int me;
  int i,c = 0;;
  me = *(int *) data;
  printf(" %d started\n",me);
  for (i=0; i < 10000; i++) {
     EnterCriticalSection(&CriticalSection);
     printf(" %d Trying to connect (%d)\n",me,c);
     if(i!=3 && i!=4 && i!=5)
         Sleep(500);
     else
         Sleep(10);
    LeaveCriticalSection(&CriticalSection);
     c++;
     Sleep(500);
  }
  return 0;
}

int main() {
  int i;
  int a[20];
  HANDLE thread[20];

  InitializeCriticalSection(&CriticalSection);
  for (i=0; i<20; i++) {
        a[i] = i;
        thread[i] = CreateThread(NULL, 0, ThreadFunc, (LPVOID) &a[i], 0, NULL);
  }
}

这样做的结果是一些线程被阻塞了很多个周期,而另一些则经常进入临界区。我还注意到,如果您更改更快的睡眠(10 毫秒),一切都可能恢复公平,但我没有发现睡眠时间和公平之间有任何联系。 但是,这个测试示例比我的实际应用程序代码要好得多,后者要复杂得多,并且显示某些线程实际上处于饥饿状态。为了确保饥饿的线程是活着的并且可以工作,我做了一个测试(在我的应用程序中),我在临界区进入 5 次后杀死线程:结果是,最后,每个线程都进入,所以我确保它们都还活着并且被互斥体阻塞。 我是否必须假设 Windows 对线程真的不公平? 你知道这个问题的任何解决方案吗?

编辑:在 linux 中使用 pthreads 的相同代码,按预期工作(没有线程饿死)。

EDIT2:我找到了一个可行的解决方案,强制公平,使用 CONDITION_VARIABLE。 可以从这篇文章 (link) 中推断出来,并进行必要的修改。

【问题讨论】:

  • MSDN 文章没有提到公平。适当地,自 Vista 和 Server 2003 SP1 以来就没有。公平导致锁车队,后台is here。
  • 信号量、临界区和互斥锁并不总是同步的最佳选择。对于竞争激烈的资源,您必须非常小心使用什么,有时让单个线程管理该资源是可取的,因为您无需阻塞该资源上的任何其他线程。一个经典的例子是 UI。
  • @HansPassant:我读了这篇文章,谢谢,所以我不得不假设不使用 CriticalSection 数据结构,因为它不公平。我引用了这个链接:link,它确实说明了我写的内容。

标签: c++ c windows multithreading critical-section


【解决方案1】:

由于关键部分保留了很长时间,因此无论如何您都会在这里遇到饥饿问题。
我认为 MSDN 可能暗示调度程序在唤醒线程方面是公平的,但由于没有锁定获取顺序,因此它实际上可能并不像您期望的那样“公平”。 您是否尝试过使用互斥锁而不是临界区?另外,您是否尝试过调整旋转次数?

如果您可以避免长时间锁定关键部分,那么这可能是解决此问题的更好方法。

例如,您可以重组您的代码,让一个线程处理您长时间运行的操作,而其他线程将对该线程的请求排队,在完成事件上阻塞。在管理队列时,您只需要在短时间内锁定临界区。当然,如果这些操作也必须与其他操作互斥,那么您需要小心。如果所有这些东西都不能同时运行,那么你也可以通过队列来序列化它。

或者,也许看看使用 boost asio。您可以使用线程池和链来防止多个异步处理程序同时运行,否则同步会成为问题。

【讨论】:

  • 我还没有尝试过互斥锁,因为我认为它处理的公平性比关键部分的处理方式要好(但也许我错了)。我知道我可以通过重组代码来使用一些解决方法,我只是想了解是否可以避免它。我将尝试使用互斥锁(自旋计数不参与其中,但正如预期的那样没有帮助)。
  • 不幸的是,互斥体的行为相同。
【解决方案2】:

我认为你应该回顾一些事情:

  • 在 10000 个案例中的 9997 个中,您分支到 Sleep(500)。几乎每次成功尝试获取关键部分时,每个线程都会将关键部分保留多达 500 毫秒。

  • 线程在释放临界区后执行另一个Sleep(500)。结果,一个线程占据了几乎 50 % (49.985 %) 的可用时间,因为它持有临界区——无论如何!

  • 幕后:Joe Duffy:互斥锁的等待列表按 FIFO 顺序保存,操作系统总是唤醒位于此类等待队列前面的线程。

假设您这样做是为了显示行为:启动其中的 20 个线程可能会导致最后一个线程在处理器完全运行时访问单个逻辑处理器上的临界区的最短等待时间为 10 秒可用于此测试。

你做测试需要多长时间 / 什么 CPU?什么Windows版本?您应该能够写下更多事实:线程活动与线程 ID 的直方图可以说明很多关于公平性的信息。

应在短时间内获取关键部分。在大多数情况下,可以更快地处理共享资源。临界区中的Sleep 几乎可以肯定地指向设计缺陷。

提示:减少花在关键部分的时间或调查Semaphore Objects。

【讨论】:

    猜你喜欢
    • 2011-09-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-17
    • 2013-03-26
    • 2013-04-07
    • 2021-09-05
    相关资源
    最近更新 更多