C#是否有更好的等待模式?


78

我发现自己已经对这种类型的东西进行了几次编码。

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

它看起来像是不良代码,是否有更好的方法可以做到?这是不良设计的征兆吗?

Answers:


98

实现此模式的一种更好的方法是让您的Thing对象公开一个事件,供消费者等待。例如aManualResetEventAutoResetEvent。这极大地简化了您的使用者代码,如下所示

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语句,否则发生时将引发异常。
SwDevMan81 2011年

您什么时候使用?(自动与手动)
Vinko Vrsalovic

自动将始终只允许一个等待线程通过,因为一旦释放第一个线程,它将自动重置。手动允许所有正在等待直到手动重置的线程。
ForbesLindesay

@Vinko-在Tuskan的注释中,如果要在ResetEvent触发或超时后检查其状态,则必须使用ManualResetEvent,因为AutoResetEvent在从WaitOne调用返回后将被重置。因此,OP所使用的示例将需要一个ManualResetEvent,而JaredPar的示例将不检查状态,并且可以更好地与AutoResetEvent一起使用
SwDevMan81 2011年

29

使用事件。

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

这样,您就不会有任何Sleep循环。


+1感谢克里斯,您如何处理在一定时间内未发生的事件(我在这些情况下很在意)。在我的脑海里,我仍然在睡觉。
可能在

@Richard-参见JaredPar的答案
ChrisF

12

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

    //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();

1
Thread.Yield很有趣,尽管DateTime.Now比DateTime.UtcNow慢,并且每次迭代都会评估startTime.Add(timeout)。
史蒂夫·邓恩

1
过早的优化是万恶之源。您当然是对的,但是将TimeSpan添加到DateTime并不是很昂贵,并且DateTime.Now只需要抵消小时。总体而言,您的优化不会像摆脱睡眠一样有效。
KeithS 2011年

如果没有等待运行的线程正在运行,则Thread.Yield是一个小问题
David Heffernan

2
-1:天哪!让我们疯狂地循环它来烧掉CPU!看起来,编写在CLR上运行的代码时,您实际上应该尝试更高层次的思考。这不是汇编,您不编写PIC代码!
布鲁诺·雷斯

如果后台发生任何事情,则Thread.Yield()将暂停循环。因为应该发生一些事情(无论我们在等待什么,至少是非常在意),所以它不会消​​耗CPU。
KeithS 2011年

9

如果可能,请将异步处理包装在中Task<T>。这提供了世界上最好的:

  • 您可以使用任务延续以类似事件的方式响应完成。
  • 您可以等待使用完成的等待句柄,因为Task<T>实现IAsyncResult
  • 使用可以轻松地编写任务Async CTP。他们也玩得很好Rx
  • 任务具有非常干净的内置异常处理系统(特别是,它们正确保留了堆栈跟踪)。

如果需要使用超时,则Rx或Async CTP可以提供该超时。


任何人都应该在生产中真正使用Async CTP吗?我意识到这将接近最终产品,但仍然是CTP。
VoodooChild 2011年

这是你的选择; 我当然是 如果愿意,您也可以轻松地用Rx编写Tasks。
Stephen Cleary

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();
}

2

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


Thread.Sleep总是包装不好,这使我感到奇怪,什么时候才是真正使用Thread.Sleep的好时机?
CheckRaise 2011年

2
@CheckRaise:在要等待定义的时间(而不是条件)时使用它。
布鲁诺·雷斯

2

我通常不鼓励抛出异常。

// 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-是否有理由不在此处抛出异常?如果姐妹服务正在运行,这是我期望发生的事情
可能在

实际的代码正在检查另一个服务是否正在执行一项工作。如果该服务未打开,它将失败,因此将引发异常并在堆栈中进一步捕获该异常。
可能在

2

我认为您应该使用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();

 }
}

2

这是您可以使用的方法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的要求)而无法使用Tasks,则可以ManualResetEvent按照JaredPar的回答中所述使用,也可以使用类似以下的方法:

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.");
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.