C# - Решение потенциального тупика

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

Это выглядит примерно так (отсутствует некоторая бизнес-логика, но идея ясна, я думаю):

Отредактировано: (добавлен отсутствующий код общих состояний, которые действительно нуждались в блокировке - это больше похоже на проблемный код)

(Мое решение опубликовано в ответе ниже)

public static class LongOperationHelper
{
    private static object _synchObject = new object();
    private static Dictionary<string, int> _calls = new Dictionary<string, int>();

    private static Action<string> DisplayLongOperationRequested;
    private static Action<string> StopLongOperationRequested;

    public static void Begin(string messageKey)
    {
        lock (_synchObject)
        {
            if (_calls.ContainsKey(messageKey))
            {
                _calls[messageKey]++;
            }
            else
            {
                _calls.Add(messageKey, 1);

                DispatcherHelper.InvokeIfNecesary(() =>
                {
                    //Raise event for the MainViewModel to display the long operation layer
                    DisplayLongOperationRequested?.Invoke(messageKey);
                });
            }
        }
    }

    public static void End(string messageKey)
    {
        lock (_synchObject)
        {
            if (_calls.ContainsKey(messageKey))
            {
                if (_calls[messageKey] > 1)
                {
                    _calls[messageKey]--;
                }
                else
                {
                    _calls.Remove(messageKey);

                    DispatcherHelper.InvokeIfNecesary(() =>
                    {
                        //Raise event for the MainViewModel to stop displaying the long operation layer
                        StopLongOperationRequested?.Invoke(messageKey);
                    });
                }
            }
            else
            {
                throw new Exception("Cannot End long operation that has not began");
            }
        }
    }
}

Итак, как вы, вероятно, видите, здесь есть потенциальный тупик, если:

  1. Кто-то вызывает Begin из потока, отличного от пользовательского интерфейса.
  2. Входит в замок
  3. Кто-то вызывает Begin или End из потока пользовательского интерфейса и блокируется
  4. Первый вызов Begin пытается отправить поток пользовательского интерфейса.

Результат: тупик!

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

Спасибо!

Используйте async/await вместо блокировок и Invoke. Они тебе не нужны. Опубликуйте код, который вы используете, в Begin, End или как вы сообщаете о прогрессе. Должно быть легко вызывать все, что занимает много времени, с помощью await Task.Run(whatever); и обновлять пользовательский интерфейс до / после оператора await. Вы можете использовать IProgress<T> для сообщения о прогрессе из другого потока, не требуя вызова диспетчера.

Panagiotis Kanavos 01.11.2018 13:57

Я не уверен, зачем вам вообще нужен замок.

Gabriel Luci 01.11.2018 13:57

@Eibi, он не будет работать должным образом, пока вы не Удалить весь этот код. Разместите код действительный, если хотите, чтобы люди оказывали конкретную помощь. В противном случае все, что можно сделать, это опубликовать общий код. Этот Helperпричины проблема и не нужен

Panagiotis Kanavos 01.11.2018 14:02

@Eibi: делегаты, которые вы передаете в Dispatcher.Invoke, будут помещены в очередь и выполнены последовательно в одном и том же потоке диспетчера, так почему вы вообще используете блокировку?

mm8 01.11.2018 14:02

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

Eibi 01.11.2018 14:05

Я изменю код, чтобы показать необходимость синхронизации потоков.

Eibi 01.11.2018 14:08

@Eibi, вам не нужна блокировка, при необходимости вы можете использовать одну из параллельных коллекций. Или вы можете изменить свой код так, чтобы он не изменял глобальный список. Он может генерировать нужные данные локально и возвращать их из фоновой задачи. Вы никогда, никогда все равно используете блокировку, когда используете диспетчер - вы пытаетесь вызвать что-то в том же потоке, который вы только что заблокировали. В любом случае вы не показали ничего, что работает в фоновом режиме.

Panagiotis Kanavos 01.11.2018 14:27

@Eibi, вы показали, как пытались решить какую-то проблему, но не объяснили, в чем проблема. Опубликованный вами код ничего не решает. Делать у вас фоновая ветка или задача? Как начать? Должен ли он производить продукцию? Доступ к глобальному состоянию? Зачем это нужно?

Panagiotis Kanavos 01.11.2018 14:28

Предложение: неразумно оставлять текущий метод внутри блокировки. Вы должны удерживать блокировку как можно короче и не должны вызывать никаких других методов. Вы определенно не перескакиваете по потокам, вызывая Dispatcher.

Michael Puckett II 01.11.2018 15:12

Спасибо @Michael Puckett II, полностью согласен! поэтому я взял код диспетчера пользовательского интерфейса из-под блокировки и оставил его минимальным.

Eibi 01.11.2018 15:24
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
10
152
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

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

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

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

Вызовы этого помощника не «все выполняются в одном потоке», как вы говорите, их также можно вызывать из фонового потока. следовательно, необходима синхронизация.

Eibi 01.11.2018 15:11

@Eibi Неважно, откуда идут звонки. Код ничего не делает, кроме планирования выполнения кода в потоке пользовательского интерфейса, который является один поток. Итак, весь значимый код является работает в одном потоке.

Servy 01.11.2018 15:15

Если под «осмысленным кодом» вы ссылаетесь на код, который зарегистрирован для этих событий и выполняет фактическое отображение (привязка WPF в этом случае), чем я согласен, он выполняется в потоке пользовательского интерфейса (который является одиночным :)). но как инфраструктура этот помощник должен быть потокобезопасным.

Eibi 01.11.2018 15:18

@Eibi Преобразование вашего вопроса в совершенно другой вопрос неуместно.

Servy 01.11.2018 15:22

Я определенно не знал. Я согласен, что в нем не хватало некоторых деталей, поэтому редактирование :). Я просто добавил причину блокировки в виде некоторых внутренних состояний (в данном случае Dictionary). Для меня это все тот же вопрос.

Eibi 01.11.2018 15:25

@Eibi Полное изменение кода для синхронизации радикально меняет вопрос, как его синхронизировать. Это все равно что сказать: «как мне приготовить по этому рецепту?» тот же вопрос, если вы измените рецепт на совершенно другой рецепт. Задаю один и тот же вопрос о двух совершенно разных фрагментах кода это разные вопросы.

Servy 01.11.2018 15:27

Честно говоря, при всем уважении, иногда действительно не понимаю некоторых негативных реакций. Я делаю все возможное, чтобы поделиться опытом и знаниями (в этом-то и идея, не так ли?). Я сделал все, чтобы вопрос и ответ были максимально ясными (пропустил первый раз, потом отредактировал и исправил). Судя по вашему ответу, вы упустили мою точку зрения в этом вопросе. Разве вы не видите, как здесь прилагаются усилия по обмену мыслями? К сожалению, такая реакция заставляет меня полностью удалить вопрос ...

Eibi 01.11.2018 15:57

@Eibi Я точно ответил на ваш вопрос. То, что вы намеревались задать другой вопрос радикально, не отменяет того факта, что вы не задавали этот вопрос. Ваш вопрос не был не понятно, ответ просто бесполезен для вас, потому что ваша реальная проблема сильно отличалась от той, о которой вы спрашивали. Преобразование вашего вопроса в совершенно другой вопрос - не лучший ответ на это.

Servy 01.11.2018 15:59

Извините, но я бы знал, если бы вы ответили на мой вопрос. Я знаю, о чем просил, до и после изменения. Эта часть не была изменена. Оставался вопрос: как обеспечить потокобезопасность, чтобы избежать тупика? Это было раньше и сейчас. Как ты думаешь, ты мне помог ?! Я попросил помощи, чтобы найти способ обеспечить безопасность потоков и избежать взаимоблокировок.

Eibi 01.11.2018 16:11

@Eibi Ответ на ваш первоначальный вопрос заключался в том, чтобы снять блокировки. Они не нужны для правильной синхронизации выполняемой вами работы, и их удаление устраняет тупик. Это было решение твоей проблемы. Затем вы изменили задачу, что изменило ответ. Ответ на ваш исходный вопрос бесполезен для вас, потому что ваш вопрос не отражает вашу проблему, а не потому, что ответ на нее не дает.

Servy 01.11.2018 17:01
Ответ принят как подходящий

Вот код "без тупиков": Я переместил отправку в поток пользовательского интерфейса за пределы блокировки.

(Может ли кто-то все еще видеть здесь потенциальный тупик?)

public static class LongOperationHelper
{
    private static object _synchObject = new object();
    private static Dictionary<string, int> _calls = new Dictionary<string, int>();

    private static Action<string> DisplayLongOperationRequested;
    private static Action<string> StopLongOperationRequested;

    public static void Begin(string messageKey)
    {
        bool isRaiseEvent = false;

        lock (_synchObject)
        {
            if (_calls.ContainsKey(messageKey))
            {
                _calls[messageKey]++;
            }
            else
            {
                _calls.Add(messageKey, 1);

                isRaiseEvent = true;
            }
        }

        //This code got out of the lock, therefore cannot create a deadlock
        if (isRaiseEvent)
        {
            DispatcherHelper.InvokeIfNecesary(() =>
            {
                //Raise event for the MainViewModel to display the long operation layer
                DisplayLongOperationRequested?.Invoke(messageKey);
            });
        }
    }

    public static void End(string messageKey)
    {
        bool isRaiseEvent = false;

        lock (_synchObject)
        {
            if (_calls.ContainsKey(messageKey))
            {
                if (_calls[messageKey] > 1)
                {
                    _calls[messageKey]--;
                }
                else
                {
                    _calls.Remove(messageKey);

                    isRaiseEvent = true;
                }
            }
            else
            {
                throw new Exception("Cannot End long operation that has not began");
            }
        }

        //This code got out of the lock, therefore cannot create a deadlock
        if (isRaiseEvent)
        {
            DispatcherHelper.InvokeIfNecesary(() =>
            {
                StopLongOperationRequested?.Invoke(messageKey);
            });
        }
    }
}

Другие вопросы по теме