Предположим, нам нужно синхронизировать доступ для чтения/записи к общим ресурсам. Несколько потоков будут обращаться к этому ресурсу как при чтении, так и в записи (чаще всего для чтения, иногда для записи). Предположим также, что каждая запись всегда вызывает операцию чтения (объект является наблюдаемым).
В этом примере я представлю себе такой класс (простить синтаксис и стиль, это просто для иллюстрации):
class Container {
public ObservableCollection<Operand> Operands;
public ObservableCollection<Result> Results;
}
Я пытаюсь использовать ReadWriterLockSlim
для этой цели, кроме того, я бы поставил его на уровне Container
(представьте, что объект не так прост, и одна операция чтения/записи может включать несколько объектов):
public ReadWriterLockSlim Lock;
Реализация Operand
и Result
не имеет смысла для этого примера.
Теперь представьте себе какой-то код, который соблюдает Operands
и произведет результат, чтобы вставить Results
:
void AddNewOperand(Operand operand) {
try {
_container.Lock.EnterWriteLock();
_container.Operands.Add(operand);
}
finally {
_container.ExitReadLock();
}
}
Наш гипотонический наблюдатель сделает что-то подобное, но будет потреблять новый элемент, и он будет блокироваться с помощью EnterReadLock()
для получения операндов, а затем EnterWriteLock()
для добавления результата (позвольте мне пропустить код для этого). Это приведет к исключению из-за рекурсии, но если я установил LockRecursionPolicy.SupportsRecursion
, тогда я просто открою свой код для мертвых блокировок (из MSDN):
По умолчанию новые экземпляры ReaderWriterLockSlim создаются с помощью флага LockRecursionPolicy.NoRecursion и не позволяют рекурсии. Эта политика по умолчанию рекомендуется для всех новых разработок, потому что рекурсия вводит ненужные сложности, а делает ваш код более склонным к взаимоблокировкам.
Я повторяю соответствующую часть для ясности:
Рекурсия [...] делает ваш код более склонным к взаимоблокировкам.
Если я не ошибаюсь в LockRecursionPolicy.SupportsRecursion
, если из того же потока я спрашиваю, допустим, блокировку чтения, тогда кто-то еще спрашивает о блокировке записи, тогда у меня будет мертвый замок, а то, что говорит MSDN, имеет смысл. Более того, рекурсия также ухудшит производительность также измеримым образом (и это не то, что я хочу, если я использую ReadWriterLockSlim
вместо ReadWriterLock
или Monitor
).
Вопрос (ы)
Наконец, мои вопросы (обратите внимание, что я не ищу обсуждения общих механизмов синхронизации, я бы знал, что неправильно для этого сценария производителя/наблюдаемого/наблюдателя):
- Что лучше в этой ситуации? Чтобы избежать
ReadWriterLockSlim
в пользуMonitor
(даже если в реальном мире чтение кода будет намного больше, чем пишет)? - Откажитесь от такой грубой синхронизации? Это может даже повысить производительность, но это сделает код намного сложнее (конечно, не в этом примере, а в реальном мире).
- Должен ли я просто делать уведомления (из наблюдаемой коллекции) асинхронными?
- Что-то еще я не вижу?
Я знаю, что нет лучшего механизма синхронизации, поэтому инструмент, который мы используем, должен быть правильным для нашего случая, но мне интересно, есть ли какая-то лучшая практика или я просто игнорирую что-то очень важное между потоками и наблюдателями (представьте, что используйте Microsoft Reactive Extensions, но вопрос является общим, не привязанным к этой структуре).
Возможные решения?
Я бы попытался сделать события (как-то) отложенными:
1-е решение
Каждое изменение не запускает событие CollectionChanged
, оно хранится в очереди. Когда поставщик (объект, который выталкивает данные) завершил, он вручную заставит очередь быть сброшенной (последовательно поднимая каждое событие). Это может быть сделано в другом потоке или даже в потоке вызывающего (но вне блокировки).
Он может работать, но он сделает все менее "автоматическим" (каждое уведомление об изменении должно запускаться вручную самим производителем, больше кода для записи, больше ошибок).
2-е решение
Другое решение может заключаться в предоставлении ссылки на наш замок на наблюдаемую коллекцию. Если я обернул ReadWriterLockSlim
в пользовательский объект (полезно скрыть его в удобном для использования объекте IDisposable
), я могу добавить ManualResetEvent
, чтобы уведомить, что все блокировки были выпущены таким образом, сама коллекция может вызывать события (снова в том же потоке или в другом потоке).
3-е решение
Еще одна идея - просто сделать события асинхронными. Если обработчику событий потребуется блокировка, он будет остановлен, чтобы подождать его временного интервала. Для этого я беспокоюсь о большом количестве потоков, которое может быть использовано (особенно если из пула потоков).
Честно говоря, я не знаю, применимо ли какое-либо из них в приложении реального мира (лично - с точки зрения пользователей - я предпочитаю второй вариант, но он подразумевает коллекцию для всего, и он делает коллекцию осведомленной о потоковом режиме, и я бы избегал это, если возможно). Я не хотел бы делать код более сложным, чем необходимо.