65.9K
CodeProject 正在变化。 阅读更多。
Home

CRWCriticalSection:解决读者/写者问题的一种带有超时的新方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.63/5 (21投票s)

2003年10月28日

CPOL

6分钟阅读

viewsIcon

82890

downloadIcon

2066

用于同步读者和写者线程的类

引言

我是在很不情愿的情况下开始接触多线程的。我一开始对我的应用程序运行缓慢感到满意。如果真的有必要,我会实现一些带有计时器的东西。出于极度的懒惰,我甚至有时会使用 VB 的 `DoEvents` 的 C++ 等价物(一个 `PeekMessage` 循环)。请不要告诉任何人:如果这件事被捅出去,我就知道是你干的。总之,用户们变得越来越坚持,我开始在我的应用程序中添加线程。

这简直是一场噩梦,我花了很长时间才能够稳定它们。第一个问题是不同的线程同时访问对象……或者更糟的是,一个线程删除了对象,而另一个线程正在访问它。你会遇到一些非常糟糕的异常故障。当然,不会马上出现!只有当你的应用程序在生产环境中运行时才会出现。为了克服这一点,你通常会在代码中到处添加关键区域(Critical Sections),以确保同一时间只有一个线程可以访问对象。

然后就出现了第二个问题:可怕的“弗里兹先生”。两个线程互相锁定,应用程序就变成了一个死的有机体……你甚至不会得到一个 GPF(通用保护故障)来帮助你理解问题所在。不用说,这种行为再次只发生在你的客户那里,而从未在你舒适的调试环境中出现过。

即使你已经设法通过在所有线程中以相同的顺序调用关键区域来解决你的锁定问题,性能问题也随之而来。每个线程都在等待另一个线程完成某项工作,整个过程非常缓慢,你的用户又开始抱怨了!

几个月前,我正处于那个阶段。我的应用程序很稳定,但速度很慢,我甚至没有一点头绪如何改进它。我通常对抱怨的用户的回应是建议他们购买一台拥有 82 个处理器的服务器。他们通常不会认真对待我的想法。好吧,他们早就停止那样做了!接近绝望时,我在 codeproject 的文章中漫无目的地闲逛,偶然发现了这个:使用信号量解决读者/写者问题,一篇由 Joris Koster 撰写的文章。标题太好了,我立刻就知道它符合我的需求。我的某些线程正在读取一些非常复杂的结构。其他线程实际上正在修改结构。我只能允许一个写者同时进行。但读者数量不限。我读了这篇文章,标题很好,但文章非常出色。绝对是 5+。

只有一个问题:优先级总是偏向读者,写者可能会永远等待(作者称之为写者饥饿)。这对我来说并不好。另一个问题是 Joris 的代码没有超时。在我之前的死锁问题之后,我认为有一个超时机制会很有用。这样问题就可以被追溯,程序可以优雅地重新启动。

背景

对于我的小型类,我使用了三种同步对象。我认为提醒一下它们的作用会有所帮助。专家请跳过本节(而且你们为什么还要阅读这篇文章——难道你们没读我的免责声明吗?)。

关键区域 (The critical section)

这可以确保同一时间只有一个线程可以进入代码的某些部分。这样你就可以确保在一个给定的代码“关键区域”内,对象不会被其他线程修改。这通常看起来像这样

// Enter part of my code where I want my object to be "stable"
::EnterCriticalSection(&m_csMyObject);

// Modify my object
...

// Let other threads play around with my object
LeaveCriticalSection(&m_csMyObject);

事件 (The event)

这可以实现类似这样的东西

while( !isOtherThreadReady)
;

只不过等待不会占用处理器时间。换句话说,你可以让一个线程休眠,然后用一个事件唤醒它。实际的代码看起来像这样

// Wait for ten seconds and give-up if I do not get the event
if ( WaitForSingleObject(m_hEvent,10000) != WAIT_OBJECT_0 )
return false;

互斥体 (The mutex)

互斥体与事件完全一样,除了如果多个线程在等待它,只有一个线程会接收到事件,而其他线程将继续等待。

实现

读取者的实现。(The reader's implementation.)

我们在这里等待最终的写入操作完成。我们使用一个互斥体,因为可能还有其他写者也在等待,我们不希望两个线程同时进行。

bool CRWCriticalSection::LockReader(DWORD dwTimeOut)
{
// We make sure that nobody is writing
if ( WaitForSingleObject(m_hWritingMutex,dwTimeOut) != WAIT_OBJECT_0 )
return false;
...

这里我们有一个关键区域,因此我们可以安全地维护读者数量。如果我们至少有一个读者,我们就重置一个事件来禁止写者开始修改。

// We need a critical section to update the number of readers
::EnterCriticalSection(&m_csReading);
{
m_nReaders++;
if ( m_nReaders == 1 )
{
// We must indicate that there are some readers
ResetEvent(m_hNobodyIsReading);
}
}
::LeaveCriticalSection(&m_csReading);
...

最后,我释放互斥体,让另一个线程进入。

ReleaseMutex(m_hWritingMutex);
return true;
}

UnlockReader() 非常简单。我只是维护读者数量的状态以及“无人正在读取”标志/事件。

void CRWCriticalSection::UnlockReader()
{
// We need a critical section to update the number of readers
::EnterCriticalSection(&m_csReading);
{
m_nReaders--;
ASSERT(m_nReaders >= 0);
if ( m_nReaders == 0 )
{
// We indicate that there are no more readers
SetEvent(m_hNobodyIsReading);
}
}
::LeaveCriticalSection(&m_csReading);
}

写者的实现。(The writer's implementation)

我们正在等待互斥体,以指示是否有写线程正在工作,或者是否有读者正在获取继续执行的权限。

bool CRWCriticalSection::LockWriter(DWORD dwTimeOut)
{
// Only one writer at a time
if ( WaitForSingleObject(m_hWritingMutex,dwTimeOut) != WAIT_OBJECT_0 )
return false;
... 

现在我们已经拥有了 `m_hWritingMutex`,我们就知道没有其他读者或写者线程会继续执行。我们只需等待所有其他读者线程离开。如果在此阶段发生超时,我们必须记住释放互斥体……

// Wait for all readers to leave
if ( WaitForSingleObject(m_hNobodyIsReading,dwTimeOut) != WAIT_OBJECT_0 )
{
// We have waited too long, so we have to set the "Writing" mutex
ReleaseMutex(m_hWritingMutex);
return false;
}

return true;
}

UnlockWriter() 再简单不过了。我释放互斥体,让另一个读者或写者进入。

void CRWCriticalSection::UnlockWriter()
{
// Let other readers and writers in
ReleaseMutex(m_hWritingMutex);
}

使用代码

我准备了一个示例,所以如果你下载了项目,你就可以看到一些东西,而不是一些枯燥的 C++ 代码。虽然不太有用,但它将允许你看到系统在压力下的行为。我用它来调试我的类。

一件非常重要的事情:当你锁定一个事件时,请确保你解锁它。这似乎是显而易见的,但如果在伪关键区域执行过程中引发了异常,你就会导致死锁。因此,我总是像这样使用该区域

if ( m_csCriticalSection.LockReader(30000) )
{
  try
  {
  // My buggy code
  ...
  }
  catch(...)
  {
  ASSERT(false);
  }
  m_csCriticalSection.UnlockReader();
}

历史

版本 1.0 27/10/2003 首次发布

结论、评论、感谢和道歉。(Conclusion, remarks, thanks and apologies)

在使用多线程之前,请仔细考虑。如果可以,请使用计时器。如果实在没有其他办法,那么在开始实现解决方案之前,请仔细分析你的问题。识别你的关键对象,并尝试理解哪个线程读取,哪个线程修改。为此,使用 C++ 关键字 const 是个好主意!

整个类绝对可以改进。互斥体没有考虑到线程等待了多久……所以也许一种考虑到这一点甚至考虑优先级的算法将是绝妙的……这超出了我的需求。但以类似的方式,我受到了 Joris Koster 的启发,有人也会受到这篇文章的启发,并希望在这里发布他们的实现!我还使用了 3 种不同的同步对象,这可能有点多余。如果你发现更简单的方法,请告诉我。

我已经说过,但我会再说一遍,感谢 Joris 的精彩文章。没有你,我可能早就失业了!

最后,如果我的英语不够流畅,我表示歉意……法语是我的母语。欢迎对代码和术语提出修正。
© . All rights reserved.