C #에 대한 더 나은 대기 패턴이 있습니까?


78

나는 이런 종류의 것을 몇 번 코딩하는 것을 발견했습니다.

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

잘못된 코드처럼 보입니다. 더 나은 방법이 있습니까? / 잘못된 디자인의 증상입니까?

답변:


98

이 패턴을 구현하는 훨씬 더 좋은 방법은 Thing객체가 소비자가 기다릴 수있는 이벤트를 노출하도록하는 것입니다. 예를 들어 a ManualResetEvent또는 AutoResetEvent. 이렇게하면 소비자 코드가 다음과 같이 크게 단순화됩니다.

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

언제 각각을 사용 하시겠습니까? (자동 대 설명서)
Vinko Vrsalovic

Auto는 첫 번째 스레드가 해제되는 즉시 재설정되므로 항상 정확히 하나의 대기 스레드를 허용합니다. 수동은 수동으로 재설정 될 때까지 대기중인 모든 스레드를 허용합니다.
ForbesLindesay 2011-08-10

@Vinko-Tuskan의 설명에 추가하면, ResetEvent가 트리거되거나 시간 초과 된 후 상태를 확인하려면 ManualResetEvent를 사용해야합니다 .AutoResetEvent가 WaitOne 호출에서 반환 된 후 재설정되기 때문입니다. 따라서 OP의 예제에는 ManualResetEvent가 필요합니다. 여기서 JaredPar의 예제는 상태를 확인하지 않고 AutoResetEvent와 함께 더 잘 작동합니다
SwDevMan81

29

사용 이벤트.

완료 될 때 (또는 할당 된 시간 내에 완료되지 않은 경우) 이벤트를 발생시키려는 항목이 이벤트를 발생시킨 다음 기본 애플리케이션에서 이벤트를 처리하도록합니다.

그렇게하면 Sleep루프 가 없습니다 .


+1 감사합니다 Chris, 특정 시간 내에 발생하지 않는 이벤트를 어떻게 처리합니까 (이 경우에 관심이 있음). 머릿속에서는 여전히 수면을 사용하고 있습니다.
아마 해변


12

프로그램이 기다리는 동안 (예 : DB에 연결하는 동안) 수행 할 다른 작업이 없다면 루프는 무언가를 기다리는 끔찍한 방법이 아닙니다. 그러나 귀하의 문제가 몇 가지 있습니다.

    //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)는 매 반복마다 평가됩니다.
Steve Dunn

1
조기 최적화는 모든 악의 근원입니다. 물론 맞지만 DateTime에 TimeSpan을 추가하는 것은 그리 비싸지 않으며 DateTime.Now는 시간을 상쇄하면됩니다. 전반적으로 최적화는 수면을 제거하는 것만 큼 많은 영향을 미치지 않습니다.
KeithS

Thread.Yield는 실행할 준비가 된 대기 스레드가없는 경우 noop입니다
David Heffernan 2011 년

2
-1 : 세상에! 미친 듯이 루프에 넣어 CPU를 태우 자! CLR에서 실행되는 코드를 작성할 때 실제로 더 높은 수준에서 생각해야합니다. 이것은 어셈블리가 아니며 PIC를 코딩하지 않습니다!
Bruno Reis

백그라운드에서 어떤 일이 발생하면 Thread.Yield ()는 루프를 일시 중단합니다. 어떤 일이 일어나야하기 때문에 (최소한 우리가 기다리는 것이 무엇이든간에) CPU를 태우지 않을 것입니다.
KeithS

9

가능한 경우 비동기 처리를 Task<T>. 이것은 모든 세계의 최고를 제공합니다.

  • 작업 연속 을 사용하여 이벤트와 같은 방식으로 완료에 응답 할 수 있습니다 .
  • Task<T>구현 하기 때문에 완료의 대기 가능 핸들을 사용하여 기다릴 수 있습니다 IAsyncResult.
  • 작업은 다음을 사용하여 쉽게 구성 할 수 있습니다 Async CTP. 그들은 또한 Rx.
  • 작업에는 매우 깨끗한 기본 제공 예외 처리 시스템이 있습니다 (특히 스택 추적을 올바르게 유지함).

시간 제한을 사용해야하는 경우 Rx 또는 Async CTP가이를 제공 할 수 있습니다.


실제로 프로덕션에서 비동기 CTP를 사용해야하는 사람이 있습니까? 최종 제품에 가까울 것이라는 것을 알고 있지만 여전히 CTP입니다.
VoodooChild

그것은 당신의 선택입니다. 확실히 그렇습니다. 원하는 경우 Rx로 작업을 쉽게 작성할 수도 있습니다.
Stephen Cleary 2011 년

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-08-10

2
@CheckRaise : 조건이 아닌 정의 된 시간 동안 기다릴 때 사용합니다.
Bruno Reis

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를 사용할 수 없다면 ManualResetEventJaredPar의 답변에서 언급 한 것처럼 사용하거나 다음과 같이 사용할 수 있습니다.

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

Wait / Pulse 접근 방식을 사용하면 이벤트를 명시 적으로 만들지 않으므로 이벤트를 처리 할 필요가 없습니다.

사용 예 :

var rh = new RunHelper(
    () =>
    {
        Thread.Sleep(1000);
    });
if (rh.Wait(500))
{
    Console.WriteLine("Success.");
}
else
{
    Console.WriteLine("Timeout.");
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.