Безопасно ли перехватывать ObjectDisposedException для ManualResetEvent.WaitOne ()? - PullRequest
2 голосов
/ 17 февраля 2012

Это тесно связано с Безопасно ли сигнализировать и немедленно закрывать ManualResetEvent? и может предоставить одно решение этой проблемы.

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

Поэтому я хочу, чтобы работа выполнялась только один раз.

Обновление: Позвольте мне добавить, что это не проблема инициализации, которую можно решить с помощью Lazy .net 4. Под «один раз» я имею в виду один раз за задачу , и эти задачи определяются во время выполнения. Это может быть не ясно из приведенного ниже упрощенного примера.

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

static void Main(string[] args)
{
    ManualResetEvent flag = new ManualResetEvent(false);
    object workResult = null;
    for (int ix = 0; ix < 10; ++ix)
    {
        ThreadPool.QueueUserWorkItem(s =>
        {
            try
            {
                flag.WaitOne();
                Console.WriteLine("Work Item Executed: {0}", workResult);
            }
            catch (ObjectDisposedException)
            {
                Console.WriteLine("Finished before WaitOne: {0}", workResult);
            }
        });
    }
    Thread.Sleep(1000);
    workResult = "asdf";
    flag.Set();
    flag.Close();
    Console.WriteLine("Finished");
}

Я думаю, суть моего вопроса:

Прекращен ли вызов WaitOne из-за исключения ObjectDisposedException, эквивалентного успешному вызову WaitOne, с точки зрения барьеров памяти?

Это должно обеспечить безопасный доступ к переменной workResult этими другими потоками.

Мое предположение: это должно быть безопасно, иначе как WaitOne мог бы безопасно определить, что объект ManualResetEvent был закрыт в первую очередь?

Ответы [ 3 ]

3 голосов
/ 17 февраля 2012

Вот что я вижу:

  • Вы получаете ObjectDisposedException, потому что в вашем коде указано следующее состояние гонки:
    • flag.close может быть вызвано до того, как всем потокам удалось вызвать flag.waitOne.

Как вы справитесь с этим, зависит от того, насколько важно выполнение кода после flag.waitOne.

Вот один подход:

Если все запущенные потоки действительно должны выполняться, у вас может быть дополнительная синхронизация до вызова flag.close. Вы можете достичь этого, используя StartNew на Task.Factory вместо Thread.QueueUserWorkItem. Задания можно ждать до завершения, и , затем вы бы вызвали flag.close, тем самым устраняя условие гонки и необходимость обрабатывать ObjectDisposedException

Ваш код станет:

    static void Main(string[] args)
    {
        ManualResetEvent flag = new ManualResetEvent(false);
        object workResult = null;
        Task[] myTasks = new Task[10];
        for (int ix = 0; ix < myTasks.Length; ++ix)
        {
            myTasks[ix] = Task.Factory.StartNew(() =>
            {
                flag.WaitOne();
                Console.WriteLine("Work Item Executed: {0}", workResult);
            });
        }

        Thread.Sleep(1000);
        workResult = "asdf";
        flag.Set();
        Task.WaitAll(); // Eliminates race condition
        flag.Close();
        Console.WriteLine("Finished");
    }

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

В качестве дополнительного примечания, ManualResetEvent.waitOne выполняет барьер памяти, поэтому переменная workresult будет самой последней, обновленной без каких-либо дополнительных барьеров памяти или энергозависимых чтений.

Итак, чтобы ответить на ваш вопрос, если вы по-настоящему должны избежать дополнительной синхронизации и обработать исключение ObjectDisposed, следуя вашему подходу, я бы сказал, что удаленный объект не выполнил для вас барьер памяти, вы придется вызвать Thread.MemoryBarrier в вашем блоке перехвата, чтобы убедиться, что последнее значение было прочитано.

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

Удачи!

0 голосов
/ 20 февраля 2012

Размышляя немного больше о реальной проблеме, которую мне пришлось решить, которая немного сложнее, чем упрощенный пример, я решил использовать Monitor.Wait и Monitor.PulseAll.

Джо АлбахариПотоки в C # оказались чрезвычайно полезными, в данном конкретном случае применяется следующий раздел: http://www.albahari.com/threading/part4.aspx#_Signaling_with_Wait_and_Pulse

0 голосов
/ 17 февраля 2012

Несколько баллов:

  1. Если это .NET 4, то Lazy - лучший способ сделать это.

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

...