WaitHandle.WaitAny 和 Semaphore 类

6

编辑: 我想为自己的问题辩解一下,现在看来可能有点荒唐,但是当时真的很有道理(参见下面的编辑 2)。

对于一个 .NET 3.5 项目,我有两种类型的资源(R1R2>),需要检查它们的可用性。每种资源类型最多同时可以有10个实例。

当其中任何一种类型的资源变得可用时,我的工作线程需要唤醒(有可变数量的线程)。在早期的实现中,只有一种资源类型,我使用了一个信号量来检查可用性。

现在我需要等待两个分别跟踪资源可用性的信号量 (S1S2)。

WaitHandle[] waitHandles = new WaitHandle[] { s1, s2 };
int signalledHandle = WaitHandle.WaitAny(waitHandles);

switch (signalledHandle)
{
    case 0:
        // Do stuff
        s1.Release();
    case 1:
        // Do stuff
        s2.Release();
}

然而,这里有一个问题。根据MSDN关于WaitAny的文档:
如果在调用期间有多个对象变为信号状态,则返回值是所有已发出信号对象中索引值最小的信号对象的数组索引。
这表明在调用WaitAny之后,我可能会将两个Semaphore计数都减少1。因为signalledHandle会指示s1被标记,所以我将开始使用资源R1,并在完成后释放它。但是,由于我不知道是否标记了S2,因此该资源的可用计数现在可能不正确。如果这种情况发生10次,我的信号量将永久处于“空”状态,且资源R2将不再使用。
如何处理这种情况?应该切换到使用简单计数器和AutoResetEvent来信号任一计数器更改吗?我是否错过了一些更优雅的方法?
编辑1:根据Ravadre的说法,在WaitAny之后只有一个Semaphore实际上会被更改。稍微修改他的示例似乎证实了这一点,但是否有人可以指向某些官方文档来指定这一点?
编辑2:我在回家的路上想到了这个问题。只有那时我意识到这对于WaitAny才有意义。这个问题不仅限于信号量,而是适用于几乎任何类型的同步对象,使得WaitAny实际上没有用处。

我已经添加了一个有价值的(虽然不是官方的)资源,你可能想要检查一下。 - Marcin Deptuła
2个回答

5

如果我正确理解了您的问题,我认为您的解决方案完全没问题,只是您对msdn引用的解释过度了。当调用WaitHandle.WaitAny()时,您将获得最低索引,但您只会锁定一个waitHandle(在这种情况下是信号量),请查看以下示例代码:


Semaphore s1 = new Semaphore(1, 2);
Semaphore s2 = new Semaphore(1, 2);

WaitHandle[] handles = new WaitHandle[] { s1, s2 };

int x = WaitHandle.WaitAny(handles);

int prevS1 = s1.Release();
int prevS2 = s2.Release();

在这种情况下,prevS1将等于0,因为信号量s1“被等待”,所以它的计数器已经减少到0,而prevS2将等于1,因为它的状态自实例化以来没有改变(Release()方法在释放之前返回计数器,因此返回1表示“它是1,现在它是2”)。
您可能还想查看另一个资源:http://www.albahari.com/threading/part2.aspx#_Wait_Handles。虽然它不是“官方”的来源,但我认为没有理由认为它不可靠。

在此示例中,您无条件释放了两个信号量。MSDN 的另一个引用:“确保线程不会过多地释放信号量是程序员的责任。”如果您执行两次此 WaitAny/Release 序列,则 s2.Release() 将引发异常:添加给定计数到信号量会导致超过其最大计数。 - Thorarin
是的,但这只是一个示例,这就是为什么我实例化了信号量以使最大计数器==2,所以我知道我可以释放它们一次,而不用担心异常。我在这里证明的是,即使释放了两个信号量(计数器> 0)并且WaitAny()返回0(从您的引用中-最小索引),只有第一个信号量被锁定,而另一个则没有改变。 - Marcin Deptuła
同样的修改似乎也支持您的说法,即只会更改一个信号量(在信号量上下文中,我个人觉得locked有点令人困惑),但我希望看到一些官方规范来确保这一点。 - Thorarin
1
锁定对我来说也不太适合,虽然我找不到更好的替代品(修改后的缺点是它不能说明是否已发布或“锁定”)。至于保证 - MSDN声称,WaitAny()返回“满足等待的对象的数组索引”,理论上表明我们正在谈论一个对象,仅有一个。在参考文献中没有任何地方提到更改多个对象状态的内容。 - Marcin Deptuła
我认为Ravadre是正确的。从我的理解来看,他试图传达的事实是,在WaitAny返回后,获取返回索引的Semaphore。因此,即使另一个Semaphore同时被释放,它也不会被获取,因此不需要被释放。当然,我不确定这一点,也没有文档可以指导您。但是,它似乎应该像这样工作。 - paracycle
我同意,它应该像这样工作,否则WaitAny将几乎没有用处。但当时我发现MSDN上的措辞有些令人困惑。 - Thorarin

0

对于您的目的,调用WaitHandle.WaitAny()方法时结果并不重要。重要的是有一个WaitHandle被标记为已发出信号,因此您需要再次尝试获取锁定/同步。

void Main() {
 var semaphoreOne = new SemaphoreSlim(0, 1);
 var semaphoreTwo = new SemaphoreSlim(0, 1);

 ReleaseSemaphoreAfterWhile(semaphoreOne);

 bool firstAccepted;
 bool secondAccepted = false;
 while ((firstAccepted = semaphoreOne.Wait(0)) == false &&
  (secondAccepted = semaphoreTwo.Wait(0)) == false) {
  var waitHandles = new [] {
   semaphoreOne.AvailableWaitHandle, semaphoreTwo.AvailableWaitHandle
  };
  WaitHandle.WaitAny(waitHandles);
  Console.WriteLine("SemaphoreOne Before Lock = " + semaphoreOne.CurrentCount);
  Console.WriteLine("SemaphoreTwo Before Lock = " + semaphoreTwo.CurrentCount);
 }

 if (firstAccepted) {
  Console.WriteLine("semaphore 1 was locked");
 } else if (secondAccepted) {
  Console.WriteLine("semaphore 2 was locked");
 } else {
  throw new InvalidOperationException("no semaphores were signaled");
 }
}

Random rd = new Random();
public void ReleaseSemaphoreAfterWhile(SemaphoreSlim semaphore) {
var sleepWork =(int)rd.Next(100, 1000);
 ThreadPool.QueueUserWorkItem(t => {
  Thread.Sleep(10000 + sleepWork);
  semaphore.Release();
 });
}

还有其他实现方式可以使用相同的思路/逻辑,但是使用while循环的方式可以确保只有一个信号量会被获取,如果没有空间,它会锁定线程直到任何WaitHandle被标记为已通知 - 考虑SemaphoreSlim实例.Release()方法。

不幸的是(如评论中所指出),网络上存在一些关于线程同步的误解,但是上面的代码应该可以帮助您解决问题。


再次阅读AvailableWaitHandle的文档,等待它并不会获取信号量本身。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接