【问题标题】:calling AutoResetEvent's Set() method mutiple times consecutively连续多次调用 AutoResetEvent Set() 方法
【发布时间】:2021-06-12 01:57:00
【问题描述】:

一本书展示了一个简单的等待锁

class SimpleWaitLock : IDisposable {
   private readonly AutoResetEvent m_available;

   public SimpleWaitLock() {
      m_available = new AutoResetEvent(true);   // Initially free
   }

   public void Enter() {
      // Block in kernel until resource available
      m_available.WaitOne();
   }

   public void Leave() {
      // Let another thread access the resource
      m_available.Set();
   }

   public void Dispose() {
      m_available.Dispose();
   }
}

然后说:

自动重置事件的行为与最大计数为 1 的信号量非常相似,并且 Set 可以在 自动重置事件,仍然只有一个线程会被解锁

我有点困惑,假设我们调用了两次 Set 方法:

class SimpleWaitLock : IDisposable {
   ...
   public void Enter() {
      m_available.WaitOne();  
   }
   public void Leave() {
      m_available.Set();  
      m_available.Set();
   }
}

假设我们有三个线程A、线程B和线程C,线程A持有锁,线程B和线程C阻塞。当threadA释放资源(第一次m_available.Set();)调用时,内核对象被发出一次信号,因此threadB获得了锁(内核对象被发出“正在使用”的信号),所以ThreadC仍然处于阻塞状态。

但是现在threadA调用了 m_available.Set();的第二次调用,所以内核对象再次被信号为空闲(未使用),那么它不是要解锁threadC,所以threadB和threadC现在可能同时运行?如果我的理解是正确的,为什么书上说只发布一个线程?

【问题讨论】:

    标签: c# .net


    【解决方案1】:

    自动重置事件的行为与最大计数为 1 的信号量非常相似,并且可以在自动重置事件上连续多次调用 Set,但仍然只有一个线程会被解除阻塞

    这意味着每次调用.Set()方法都会释放另一个线程。

    让我们换个角度想。

    考虑一排排长队试图上火车的人。但在他们和火车之间是金属探测器和保安。

    保安 (AutoResetEvent) 控制着谁通过金属探测器,何时通过。

    在保安人员允许客人上车之前,人们必须在金属探测器前(.WaitOne())前的地面标记线上等候。

    当警卫想让一位客人上火车时,他发出信号(.Set()) 下一个人通过金属探测器上车。

    因为大多数人都了解排队等候的工作原理,所以他们都会向前移动,下一个人会自动在排队等候 - 而不是像疯子一样跑过并被保安拦住。 (EventResetMode.AutoReset)

    让我们根据你的例子来说明这一点。

    假设我们有 A 人、B 人、C 人在等火车。

           A                   B                    C  
    guard.WaitOne();     guard.WaitOne();     guard.WaitOne();
    

    保安示意A人上前。

    guard.Set();

           A                   B                    C
    walking to train   guard.WaitOne();     guard.WaitOne();
    

    当他这样做时,线路会自动转移,因此 B 正在排队等候,因为 AutoResetEvent 会自动让下一个人等待被叫到,而不是保安明确告诉下一个人停下来。

    如果他再叫一次,不管过了多久,或者他身后有多少人走到火车上(但还没有上火车),另一个人会向前走。

    guard.Set();

           A                   B                    C
    walking to train   walking to train     guard.WaitOne();
    

    AutoResetEvent 不限制并发任务的数量(从金属探测器步行到火车的人数),除非您有匹配的 .WaitOne()s 和 .Set()。每个 Set 和 Wait 应该是 1:1。

    如果你连续多次拨打.Set(),保安会让那么多人通过,假设有很多人在等。

    如果您只希望一个人同时从金属探测器走到火车(握住把手),您应该让那个人在他/她到达火车时对保安大喊大叫。 (此人在火车上时拨打.Set()。)

    让我们重新开始吧

    假设我们有 A 人、B 人、C 人在等火车。

           A                   B                    C  
    guard.WaitOne();     guard.WaitOne();     guard.WaitOne();
    

    保安示意A人上前。

    guard.Set();

           A                   B                    C
    walking to train   guard.WaitOne();     guard.WaitOne();
    

    A 上了火车,然后对保安大喊他们在火车上。

           A                   B                    C
    Sitting On Train   guard.WaitOne();     guard.WaitOne();
    

    保安随后呼叫下一个人。

    guard.Set();

           A                   B                    C
    Sitting On Train   walking to train     guard.WaitOne();
    

    【讨论】:

      【解决方案2】:

      这是完整的报价:

      因此,自动重置事件的行为与最大计数为 1 的信号量非常相似。两者之间的区别在于,可以在自动重置事件上连续多次调用 Set,但仍然只有一个线程会被调用未阻塞,而在信号量上连续多次调用 Release 会不断增加其内部计数,这可能会解除阻塞许多线程。顺便说一句,如果你在一个信号量上调用 Release 太多次,导致它的计数超过它的最大计数,那么 Release 将抛出一个 SemaphoreFullException。

      作者将信号量与已知的自动重置事件进行比较,并强调它包含内部计数器或获得异常的区别。

      基于sourcesSet()方法调用原生SetEvent() function,这里有一个清晰的文档:

      自动重置事件对象的状态保持信号状态,直到释放单个等待线程,此时系统自动将状态设置为非信号状态。如果没有线程在等待,则事件对象的状态保持信号状态。

      测试行为的小程序:

      
      var res = new AutoResetEvent(false);
      for(var pp = 0; pp < 100; pp++)
      {
          var threads = Enumerable.Range(0, nthreds).Select(x => new Thread(() => res.WaitOne())).ToList();
          threads.ForEach(t => t.Start());
          while(threads.Any(t => t.ThreadState != System.Threading.ThreadState.WaitSleepJoin))
              Thread.Sleep(10);
      
          // direct invocations to speedup code execution
          // keep in sync with nthreds
          var resStatus = new[]{
              res.Set(),
              res.Set(),
              res.Set(),
              res.Set(),
              res.Set(),
              res.Set(),
              res.Set(),
              res.Set(),
              res.Set(),
              res.Set(),
              res.Set(),
              res.Set()
          };
      
          if(!resStatus.All(_ => _))
          {
              Console.WriteLine("Set didn't succeed!");
              resStatus.ToList().ForEach(Console.WriteLine);
          }
      
          for(var i = 0; i < 10; i++)
              if(threads.Any(t => t.IsAlive))
                  Thread.Sleep(100);
      
          if(threads.Any(t => t.IsAlive))
          {
              Console.WriteLine("Somethin bad happened!");
              threads.ForEach(t => Console.WriteLine(t.ThreadState));
          }
      }
      
      Console.WriteLine("Done!");
      Console.ReadKey();
      

      【讨论】:

        猜你喜欢
        • 2012-01-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-09-15
        • 2023-04-01
        • 1970-01-01
        • 1970-01-01
        • 2014-05-21
        相关资源
        最近更新 更多