【问题标题】:Is there a better waiting pattern for c#?c# 有更好的等待模式吗?
【发布时间】:2011-10-23 06:16:32
【问题描述】:

我发现自己曾多次编写过这种类型的代码。

for (int i = 0; i < 10; i++)
{
   if (Thing.WaitingFor())
   {
      break;
   }
   Thread.Sleep(sleep_time);
}
if(!Thing.WaitingFor())
{
   throw new ItDidntHappenException();
}

它只是看起来像糟糕的代码,有没有更好的方法来做到这一点/这是糟糕设计的症状吗?

【问题讨论】:

    标签: c# multithreading design-patterns


    【解决方案1】:

    实现此模式的更好方法是让您的Thing 对象公开一个消费者可以等待的事件。例如ManualResetEventAutoResetEvent。这极大地简化了你的消费者代码如下

    if (!Thing.ManualResetEvent.WaitOne(sleep_time)) {
      throw new ItDidntHappen();
    }
    
    // It happened
    

    Thing 一侧的代码实际上也不再复杂。

    public sealed class Thing {
      public readonly ManualResetEvent ManualResetEvent = new ManualResetEvent(false);
    
      private void TheAction() {
        ...
        // Done.  Signal the listeners
        ManualResetEvent.Set();
      }
    }
    

    【讨论】:

    • +1 感谢 Jared - 处理异常情况的处理不是很整齐
    • 我认为你需要! if 语句,否则在发生时会抛出异常。
    • 你什么时候使用它们? (自动与手动)
    • Auto 将始终只允许一个等待线程通过,因为它会在第一个线程释放后立即重置。手动允许任何等待通过的线程,直到手动重置。
    • @Vinko - 添加到 Tuskan 的评论中,如果您想在触发或超时后检查 ResetEvent 的状态,那么您将不得不使用 ManualResetEvent,因为 AutoResetEvent 在返回后被重置来自 WaitOne 电话。因此,OP 的示例需要 ManualResetEvent,其中 JaredPar 的示例不检查状态,并且可以更好地使用 AutoResetEvent
    【解决方案2】:

    使用事件。

    让您等待的事物在完成时(或未能在分配的时间内完成)引发事件,然后在您的主应用程序中处理该事件。

    这样你就没有任何Sleep 循环。

    【讨论】:

    • +1 谢谢克里斯,您如何应对在特定时间内未发生的事件(在这些情况下我关心)。在我的脑海中,我仍然会使用睡眠。
    • @Richard - 见JaredPar's answer
    【解决方案3】:

    如果您的程序在等待时(例如在连接到数据库时)没有其他事情可做,那么循环并不是等待某事的可怕方式。但是,我发现您的问题存在一些问题。

        //It's not apparent why you wait exactly 10 times for this thing to happen
        for (int i = 0; i < 10; i++)
        {
            //A method, to me, indicates significant code behind the scenes.
            //Could this be a property instead, or maybe a shared reference?
            if (Thing.WaitingFor()) 
            {
                break;
            }
            //Sleeping wastes time; the operation could finish halfway through your sleep. 
            //Unless you need the program to pause for exactly a certain time, consider
            //Thread.Yield().
            //Also, adjusting the timeout requires considering how many times you'll loop.
            Thread.Sleep(sleep_time);
        }
        if(!Thing.WaitingFor())
        {
            throw new ItDidntHappenException();
        }
    

    简而言之,上面的代码看起来更像是一个“重试循环”,它被搞得更像是一个超时。以下是我构建超时循环的方式:

    var complete = false;
    var startTime = DateTime.Now;
    var timeout = new TimeSpan(0,0,30); //a thirty-second timeout.
    
    //We'll loop as many times as we have to; how we exit this loop is dependent only
    //on whether it finished within 30 seconds or not.
    while(!complete && DateTime.Now < startTime.Add(timeout))
    {
       //A property indicating status; properties should be simpler in function than methods.
       //this one could even be a field.
       if(Thing.WereWaitingOnIsComplete)
       {
          complete = true;
          break;
       }
    
       //Signals the OS to suspend this thread and run any others that require CPU time.
       //the OS controls when we return, which will likely be far sooner than your Sleep().
       Thread.Yield();
    }
    //Reduce dependence on Thing using our local.
    if(!complete) throw new TimeoutException();
    

    【讨论】:

    • Thread.Yield 很有趣,尽管 DateTime.Now 比 DateTime.UtcNow 慢,并且每次迭代都会评估 startTime.Add(timeout)。
    • 过早的优化是万恶之源。当然,您是对的,但是将 TimeSpan 添加到 DateTime 并不是很昂贵,并且 DateTime.Now 只需抵消小时数。总体而言,您的优化不会像摆脱睡眠那样产生很大的影响。
    • Thread.Yield 如果没有准备好运行的等待线程,则为 noop
    • -1:我的天哪!让我们疯狂地循环烧掉那个 CPU!看,当编写在 CLR 上运行的代码时,你真的应该尝试在更高的层次上思考。这不是汇编,你不是在编码 PIC!
    • 如果后台发生了什么事情,Thread.Yield() 将暂停循环。因为应该有一些事情发生(无论我们在等待什么,至少),它不会烧毁 CPU。
    【解决方案4】:

    如果可能,将异步处理封装在 Task&lt;T&gt; 中。这提供了世界上最好的:

    • 您可以使用task continuations 以类似事件的方式响应完成。
    • 您可以使用完成的可等待句柄等待,因为Task&lt;T&gt; 实现了IAsyncResult
    • 使用Async CTP 可以轻松组合任务;他们还与Rx 配合得很好。
    • 任务有一个非常干净的内置异常处理系统(特别是,它们正确地保留了堆栈跟踪)。

    如果您需要使用超时,那么 Rx 或 Async CTP 可以提供。

    【讨论】:

    • 真的应该有人在生产中使用异步 CTP 吗?我意识到它会接近最终产品,但它仍然是 CTP。
    • 这是你的选择;我当然是。如果您愿意,也可以使用 Rx 轻松编写任务。
    【解决方案5】:

    我会看看WaitHandle 类。特别是等待直到设置对象的ManualResetEvent 类。您还可以为其指定超时值并检查它是否在之后设置。

    // Member variable
    ManualResetEvent manual = new ManualResetEvent(false); // Not set
    
    // Where you want to wait.
    manual.WaitOne(); // Wait for manual.Set() to be called to continue here
    if(!manual.WaitOne(0)) // Check if set
    {
       throw new ItDidntHappenException();
    }
    

    【讨论】:

      【解决方案6】:

      Thread.Sleep 的调用始终是应避免的主动等待。
      一种替代方法是使用计时器。为了方便使用,您可以将其封装到一个类中。

      【讨论】:

      • Thread.Sleep 总是得到一个糟糕的包装,这让我想知道,什么时候是使用 Thread.Sleep 的真正好时机?
      • @CheckRaise:当你想等待一段定义的时间时使用它,而不是等待一个条件。
      【解决方案7】:

      我通常不鼓励抛出异常。

      // Inside a method...
      checks=0;
      while(!Thing.WaitingFor() && ++checks<10) {
          Thread.Sleep(sleep_time);
      }
      return checks<10; //False = We didn't find it, true = we did
      

      【讨论】:

      • @lshpeck - 是否有理由不在这里抛出异常?如果姊妹服务正在运行,我预计会发生这种情况
      • 实际代码正在检查另一个服务是否正在执行一项工作。如果服务未开启,它将失败,因此会抛出异常并在堆栈中进一步捕获。
      【解决方案8】:

      我认为您应该使用 AutoResetEvents。当您等待另一个线程完成它的任务时,它们工作得很好

      例子:

      AutoResetEvent hasItem;
      AutoResetEvent doneWithItem;
      int jobitem;
      
      public void ThreadOne()
      {
       int i;
       while(true)
        {
        //SomeLongJob
        i++;
        jobitem = i;
        hasItem.Set();
        doneWithItem.WaitOne();
        }
      }
      
      public void ThreadTwo()
      {
       while(true)
       {
        hasItem.WaitOne();
        ProcessItem(jobitem);
        doneWithItem.Set();
      
       }
      }
      

      【讨论】:

        【解决方案9】:

        下面是使用System.Threading.Tasks 的方法:

        Task t = Task.Factory.StartNew(
            () =>
            {
                Thread.Sleep(1000);
            });
        if (t.Wait(500))
        {
            Console.WriteLine("Success.");
        }
        else
        {
            Console.WriteLine("Timeout.");
        }
        

        但是,如果由于某种原因(例如 .Net 2.0 的要求)您不能使用任务,那么您可以使用 JaredPar 的回答中提到的 ManualResetEvent 或使用类似的东西:

        public class RunHelper
        {
            private readonly object _gate = new object();
            private bool _finished;
            public RunHelper(Action action)
            {
                ThreadPool.QueueUserWorkItem(
                    s =>
                    {
                        action();
                        lock (_gate)
                        {
                            _finished = true;
                            Monitor.Pulse(_gate);
                        }
                    });
            }
        
            public bool Wait(int milliseconds)
            {
                lock (_gate)
                {
                    if (_finished)
                    {
                        return true;
                    }
        
                    return Monitor.Wait(_gate, milliseconds);
                }
            }
        }
        

        使用等待/脉冲方法,您无需显式创建事件,因此您无需关心如何处理它们。

        使用示例:

        var rh = new RunHelper(
            () =>
            {
                Thread.Sleep(1000);
            });
        if (rh.Wait(500))
        {
            Console.WriteLine("Success.");
        }
        else
        {
            Console.WriteLine("Timeout.");
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-08-10
          • 2010-10-30
          • 2012-03-23
          • 1970-01-01
          • 2011-08-10
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多