Чи є кращий шаблон очікування для 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 спасибі Джаред - це розглядає виняток, коли це відбувається не дуже акуратно
ймовірно, на пляжі

Гадаю, потрібно! оператор if, інакше він видасть виняток, коли це все-таки трапиться.
SwDevMan81,

Коли б ви використовували кожну? (Авто проти ручного)
Вінко Врсалович

Авто завжди дозволить пропустити рівно одну нитку очікування, оскільки вона скидається, як тільки випускається перша. Вручну дозволяється будь-які потоки, які очікують, поки їх не буде скинуто вручну.
ForbesLindesay

@Vinko - Додаючи до коментаря Тускана, якщо ви хочете перевірити стан ResetEvent після його спрацьовування або часу очікування, вам доведеться використовувати ManualResetEvent, оскільки AutoResetEvent скидається після повернення із виклику WaitOne. Отже, приклад, який має OP, вимагає ManualResetEvent, де приклад JaredPar не перевіряє стан і буде краще працювати з AutoResetEvent
SwDevMan81

29

Використовуйте події.

Нехай річ, на яку ви чекаєте, піднімає подію, коли вона закінчується (або не вдається закінчити протягом відведеного часу), а потім обробити подію у вашому основному додатку.

Таким чином у вас немає жодної Sleepпетлі.


+1 Дякую, Кріс, як ти обслуговуєш події, що не відбуваються протягом певного часу (що в цих випадках мені цікаво). У своїй голові я все ще використовую сон.
ймовірно, на пляжі

@Richard - Див . Відповідь
JaredPar

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. Тепер потрібно лише компенсувати години. Загалом, ваші оптимізації не матимуть такого ефекту, як позбавлення від сну.
KeithS

Thread.Yield - це петля, якщо немає потоків очікування, готових до запуску
Девід Хеффернан

2
-1: Боже мій! Давайте спалимо цей процесор, пускаючи його в петлю як божевільний! Подивіться, коли пишете код, який працює на CLR, вам справді слід спробувати мислити на більш високому рівні. Це не збірка, і ви не кодуєте PIC!
Бруно Рейс

Якщо щось відбувається у фоновому режимі, Thread.Yield () призупинить цикл. Оскільки щось має відбуватися (що б ми не чекали, ДУЖЕ принаймні), це не спалить процесор.
KeithS

9

Якщо можливо, асинхронну обробку оберніть у Task<T>. Це забезпечує найкраще з усіх світів:

  • Ви можете відповісти на завершення подібно, використовуючи продовження завдання .
  • Ви можете зачекати, використовуючи ручку очікування завершення, оскільки Task<T>знаряддя IAsyncResult.
  • Завдання легко складати за допомогою Async CTP; вони також добре грають Rx.
  • Завдання мають дуже вбудовану систему обробки винятків (зокрема, вони правильно зберігають трасування стека).

Якщо вам потрібно використовувати тайм-аут, це може надати Rx або Async CTP.


Чи повинен хтось насправді використовувати Async CTP у виробництві? Я усвідомлюю, що це буде близьке до кінцевого продукту, але це все ще CTP.
VoodooChild

Це твій вибір; Я, звичайно, є. Якщо ви віддаєте перевагу, ви також можете легко складати завдання з 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();
}

2

Заклик Thread.Sleepзавжди - це активне очікування, якого слід уникати.
Однією з альтернатив може бути використання таймера. Для зручності використання ви можете інкапсулювати це у клас.


Thread.Sleep завжди отримує погану обгортку, що змушує мене задуматися, коли насправді хороший час для використання Thread.Sleep?
CheckRaise

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

Але якщо ви не можете використовувати Tasks з якихось причин (наприклад, вимога .Net 2.0), тоді ви можете використовувати, 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);
        }
    }
}

За допомогою підходу Wait / Pulse ви явно не створюєте події, тому вам не потрібно дбати про їх утилізацію.

Приклад використання:

var rh = new RunHelper(
    () =>
    {
        Thread.Sleep(1000);
    });
if (rh.Wait(500))
{
    Console.WriteLine("Success.");
}
else
{
    Console.WriteLine("Timeout.");
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.