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.