Блокировка C # на основе свойства класса - PullRequest
0 голосов
/ 07 сентября 2018

Я видел много примеров использования lock, и обычно это что-то вроде этого:

private static readonly object obj = new object();

lock (obj)
{
    // code here
}

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

Это возможно? Это имеет смысл вообще?

Вот что я имел в виду:

public class GmailController : Controller
{

    private static readonly ConcurrentQueue<PushRequest> queue = new ConcurrentQueue<PushRequest>();

    [HttpPost]
    public IActionResult ProcessPushNotification(PushRequest push)
    {
        var existingPush = queue.FirstOrDefault(q => q.Matches(push));
        if (existingPush == null)
        {
            queue.Enqueue(push);
            existingPush = push;
        }
        try
        {
            // lock if there is an existing push in the
            // queue that matches the requested one
            lock (existingPush)
            {
                // process the push notification
            }
        }
        finally
        {
            queue.TryDequeue(out existingPush);
        }
    }
}

Справочная информация : у меня есть API, где я получаю push-уведомления от API Gmail, когда наши пользователи отправляют / получают электронные письма. Однако, если кто-то отправляет сообщение двум пользователям одновременно, я получаю два push-уведомления. Моей первой идеей было запросить базу данных перед вставкой (в зависимости от темы, отправителя и т. Д.). В некоторых редких случаях запрос второго вызова выполняется до SaveChanges предыдущего вызова, поэтому я получаю дубликаты.

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

Ответы [ 2 ]

0 голосов
/ 12 сентября 2018

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

public class UserScopeLocker : IDisposable
{
    private static readonly object _obj = new object();

    private static ICollection<string> UserQueue = new HashSet<string>();

    private readonly string _userId;

    protected UserScopeLocker(string userId)
    {
        this._userId = userId;
    }

    public static UserScopeLocker Acquire(string userId)
    {
        while (true)
        {
            lock (_obj)
            {
                if (UserQueue.Contains(userId))
                {
                    continue;
                }
                UserQueue.Add(userId);
                return new UserScopeLocker(userId);
            }
        }
    }

    public void Dispose()
    {
        lock (_obj)
        {
            UserQueue.Remove(this._userId);
        }
    }
}

... тогда вы бы использовали это так:

[HttpPost]
public IActionResult ProcessPushNotification(PushRequest push)
{
    using(var scope = UserScopeLocker.Acquire(push.UserId))
    {
        // process the push notification
        // two threads can't enter here for the same UserId
        // the second one will be blocked until the first disposes
    }
}

Идея такова:

  • UserScopeLocker имеет защищенный конструктор, обеспечивающий вызов Acquire.
  • _obj является частной статической только для чтения, только UserScopeLocker может заблокировать этот объект.
  • _userId - это закрытое поле только для чтения, которое гарантирует, что даже его собственный класс не сможет изменить его значение.
  • lock выполняется при проверке, добавлении и удалении, поэтому два потока не могут конкурировать с этими действиями.

Возможные недостатки, которые я обнаружил:

  • Поскольку UserScopeLocker полагается на IDisposable для освобождения некоторого UserId, я не могу гарантировать, что вызывающая сторона будет правильно использовать оператор using (или вручную удалить объект области).
  • Я не могу гарантировать, что область не будет использоваться в рекурсивной функции (что может привести к тупику).
  • Я не могу гарантировать, что код внутри оператора using не вызовет другую функцию, которая также пытается получить область действия для пользователя (это также может привести к взаимоблокировке).
0 голосов
/ 07 сентября 2018

Позвольте мне сначала убедиться, что я понимаю предложение. Проблема заключается в том, что у нас есть некоторый ресурс, совместно используемый несколькими потоками, назовите его database, и он допускает две операции: Read(Context) и Write(Context). Предложение состоит в том, чтобы иметь гранулярность блокировки, основанную на свойстве контекста. То есть:

void MyRead(Context c) 
{
  lock(c.P) { database.Read(c); }
}
void MyWrite(Context c)
{
  lock(c.P) { database.Write(c); }
}

Так что теперь, если у нас есть вызов MyRead, где свойство context имеет значение X, и вызов MyWrite, где свойство context имеет значение Y, и эти два вызова участвуют в двух разных потоках, они не сериализовано. Однако, если у нас есть, скажем, два вызова MyWrite и вызов MyRead, и во всех из них свойство context имеет значение Z, эти вызовы сериализуются .

Возможно ли возможно ? Да. Это не делает это хорошей идеей. Как реализовано выше, это плохая идея, и вы не должны делать это.

Поучительно узнать, почему это плохая идея.

Во-первых, это просто не работает, если свойство имеет тип значения, например целое число. Вы можете подумать, что мой контекст - это идентификационный номер, это целое число, и я хочу сериализовать все обращения к базе данных, используя идентификационный номер 123, и сериализовать все обращения, используя идентификационный номер 345, но не сериализовать эти обращения в отношении каждого. Другой. Блокировки работают только для ссылочных типов , а , заключая в бокс тип значения, всегда дает вам только что выделенное поле , поэтому блокировка никогда не будет оспорена, даже если идентификаторы были тот же самый. Это было бы полностью сломано.

Во-вторых, он плохо работает, если свойство является строкой. Замки логически «сравниваются» по эталону , а не значению . С целыми числами в штучной упаковке вы всегда получаете разные ссылки. Со строками вы иногда получаете разные ссылки! (Из-за непоследовательного применения interning .) Вы можете оказаться в ситуации, когда вы блокируете «ABC», а иногда ожидает другую блокировку на «ABC», а иногда нет. !

Но нарушается фундаментальное правило: вы никогда не должны блокировать объект, если только этот объект не был специально разработан как объект блокировки, а тот же код, который контролирует доступ к заблокированному ресурсу, контролирует доступ к объект блокировки .

Проблема здесь не "локальная" для блокировки, а скорее глобальная. Предположим, что ваша собственность Frob, где Frob является ссылочным типом. Вы не знаете, блокируется ли какой-либо другой код в вашем процессе тем же Frob, и, следовательно, вы не знаете, какие ограничения порядка блокировки необходимы для предотвращения взаимных блокировок. Является ли программа блокированной или нет, является глобальным свойством программы. Точно так же, как вы можете построить пустотелый дом из сплошных кирпичей, вы можете создать взаимоблокирующую программу из набора замков, которые индивидуально верны. Убедившись, что каждая блокировка снята только с частного объекта, которым вы управляете , вы гарантируете, что никто больше никогда не блокирует один из ваших объектов, и поэтому анализ того, содержит ли ваша программа взаимоблокировку, становится проще.

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

Так что, если бы вы были одержимы этим, какой был бы правильный способ сделать это?

Правильным способом было бы реализовать новую службу: поставщик объектов блокировки . LockProvider<T> должен уметь хеш и сравнивать на равенство два T с. Служба, которую он предоставляет: вы сообщаете ему, что хотите заблокировать объект для определенного значения T, и он возвращает вам канонический объект блокировки для этого T. Когда вы закончите, вы говорите, что все готово. Поставщик ведет подсчет ссылок того, сколько раз он выдал объект блокировки и сколько раз он получил его, и удаляет его из своего словаря, когда счетчик обнуляется, чтобы у нас не было утечки памяти.

Очевидно, что поставщик блокировок должен быть ориентирован на многопотоковое исполнение и требовать крайне низкого уровня конкуренции, поскольку это механизм, разработанный для предотвращения конфликтов , поэтому лучше не вызывать никакого! Если это путь, по которому вы собираетесь идти, вам понадобится эксперт по потокам C # для разработки и реализации этого объекта . Очень легко ошибиться. Как я отметил в комментариях к вашему сообщению, вы пытаетесь использовать параллельную очередь в качестве своего рода плохого поставщика блокировки, и это масса ошибок состояния гонки.

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

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