Как проверить наличие ожидающих потоков в блокировке

#c# #multithreading #locking #thread-safety

#c# #многопоточность #блокировка #безопасность потоков

Вопрос:

Итак, я знаю, что вы можете использовать семафор и, возможно, другие методы, чтобы определить, будет ли блокировка успешной и / или сколько потоков ожидают блокировки, но можете ли вы сделать это из самой блокировки?

Моя ситуация такова, что у меня есть обработчик событий, который может вызываться многими объектами внутри a List . Я хочу выполнить тест внутри блокировки, чтобы убедиться, что все объекты находятся в правильном состоянии, прежде чем продолжить. Объекты переходят в соответствующее состояние, а затем вызывают это событие, поэтому в модели с одним потоком, когда последний вызывает это событие, все объекты будут в правильном состоянии, и мы продолжим. Но мне пришло в голову, что с несколькими потоками несколько объектов могут быть переведены в состояние, которое я проверяю, но пока еще не обработали это событие, поскольку они ожидают блокировки. Поэтому проверка условного состояния в блокировке будет истинной, но продолжение будет плохим, пока все потоки не завершат обработку этого события. Мне нужно, чтобы это было верно только при обработке последнего потока, чтобы гарантировать, что мы не продолжим слишком рано.

Например, в полуреалистичном коде:

 object _LockObj = new object();

void Event_Handler(object sender, EventArgs e)
{
    MyObject originator = sender as MyObject;
    if(originator == null)
        return;

    *Do stuff with the originator*

    lock(_LockObj)
    {
        if(ListOfMyObjects.FindAll(o => o.State == DesiredState)
                          .Count == ListOfMyObjects.Count 
                                 amp;amp; *nothing waiting at the lock*)
        {
            *Proceed*
        }
    }
}
  

Я совершенно готов признать, что мой подход с самого начала вонючий и что решение, которое я ищу, является спорным, если я делаю это правильно в первую очередь, но я не уверен, как сделать это правильно потокобезопасным способом.

Я собирался добавить еще больше состояний MyObject и управлять потоком, установив соответствующее состояние в *Do stuff with the originator* разделе, но мне показалось неправильным переводить MyObject отсюда, не говоря уже о том, что затем я должен реализовать состояние ожидания для каждого состояния, которое вызывает событие, которое становится вонючее, а не проще!

Итак, если есть простой способ выполнить проверку «ничего не ждет при блокировке«, то я бы хотел пойти именно так, если только мне не хватает действительно простого в реализации шаблона, который помогает в этом сценарии.

Комментарии:

1. Вы запрашиваете ошибку гонки потоков в качестве функции. Через наносекунду после того, как вы пришли к выводу, что ни один поток не ожидает, поток может войти в метод.

Ответ №1:

Во-первых, к сожалению, тестирование других потоков, ожидающих выполнения инструкции lock(), не даст вам того, что вы хотите. Например, если объекты A, B и C перешли в желаемое состояние и вызвали свои события, возможно, что Event_Handler вызывается только для одного объекта, скажем, A в этот момент. Один и тот же обработчик будет запущен для B и C в какой-то момент позже. Итак, все ваши объекты теперь предположительно находятся в желаемом состоянии, и никакие потоки не ожидают блокировки, но вы можете. не хочу «продолжать». Даже если вы это сделаете, вам нужно учитывать, что то же самое может произойти мгновением позже для B, а затем снова для C, поэтому «продолжить» может выполняться три раза…

Это простое предложение, но вы можете использовать счетчик, чтобы проверить, сколько объектов фактически вызвало событие. Вы также можете использовать другой список, если счетчик не дает достаточной информации. Важной частью является то, что какой бы метод вы ни использовали, обновляйте счетчик / список / другой механизм только внутри блокировки на том же _LockObj самом. Затем, вероятно, сбросьте этот счетчик после запуска proceed. Если вам нужно получить доступ к счетчику в другом месте, убедитесь, что вы используете тот же lock(_LockObj) , это гарантирует, что вы не измените его, находясь внутри другого критического раздела.

 object _LockObj = new object();
int _counter = 0;

void Event_Handler(object sender, EventArgs e)
{
    MyObject originator = sender as MyObject;
    if(originator == null)
        return;

    *Do stuff with the originator*

    lock(_LockObj)
    {
          _counter;
        if (_counter == ListOfMyObjects.Count)
        {
            *Proceed*

            _counter = 0;
        }
    }
}
  

Редактировать: удалена избыточная проверка состояния объектов.

Комментарии:

1. Разве двойная проверка не бесполезна на данный момент? Если две части if отключены, вы никогда не сможете его ввести.

2. Спасибо @xanatos, обновили код. Возможно, что другие части кода обновят состояние объектов, и нам все равно нужно это проверить, но если это так, мы должны (А) посмотреть больше кода здесь и (б) также предположить, что _counter изменяет свое значение и в других разделах.

3. Ах, как глупо с моей стороны не учитывать сценарий, в котором события входят в метод после проверки: ( Итак, каким бы ни было мое решение, мне нужно отслеживать завершения независимо, а затем продолжать только после успешного завершения моего независимого теста. Спасибо за проверку работоспособности 🙂

Ответ №2:

Вы думали о простой синхронной очереди рабочих элементов? Итак, когда возникает событие, оно просто ставит обработку в очередь, и обработка выполняется по порядку отдельным потоком?

Комментарии:

1. Не уверен, как это помогает, если вы не можете уточнить, поскольку мне нужно отложить продолжение до тех пор, пока это не будет уместно, и вопрос в том, как мне теперь, когда продолжить. На данный момент я собираюсь реализовать что-то, основанное на приведенном выше ответе Джонно (поскольку крайний срок еще не истек), если не появится что-то более простое.

2. В однопоточной модели вы говорите, что она работает нормально, предположительно, у вас есть механизм для определения того, когда событие запускается последним. Если вы используете ту же логику внутри рабочего элемента, чтобы он мог определить, должен ли он выполнять обработку, вы можете обрабатывать его так же, как один поток. По сути, вы перенаправляете события, вызванные несколькими потоками, в синхронную очередь, чтобы вы могли обрабатывать ее как единый поток. Я признаю, что, возможно, поторопился с этим ответом, поэтому я предполагаю, что вы действительно хотите, чтобы бит обработки был асинхронным, а остальные — нет?