Это кажется мне слишком подверженным ошибкам. Он поощряет ситуации, в которых блокировка неявно / негласно снимается, способом, который не понятен читателю кода, и повышает вероятность того, что важный факт об интерфейсе будет неправильно понят.
Обычно хорошей идеей является дублирование общих шаблонов - представляйте перечисляемую коллекцию с IEnumerable<T>
, которая удаляется, когда вы закончите с ней, - но дополнительный ингредиент для снятия блокировки делает большим Разница, к сожалению.
Я бы предположил, что идеальным подходом было бы вообще не предлагать перечисления для коллекций, разделяемых между потоками. Попробуйте спроектировать всю систему так, чтобы она не была нужна. Очевидно, это иногда будет сумасшедшей несбыточной мечтой.
Таким образом, следующая лучшая вещь должна была бы определить контекст, в котором IEnumerable<T>
временно доступен, в то время как блокировка существует:
public class SomeCollection<T>
{
// ...
public void EnumerateInLock(Action<IEnumerable<T>> action) ...
// ...
}
То есть, когда пользователь этой коллекции хочет ее перечислить, он делает это:
someCollection.EnumerateInLock(e =>
{
foreach (var item in e)
{
// blah
}
});
Это делает время жизни блокировки, явно выраженное областью видимости (представленной лямбда-телом, работающим во многом как оператор lock
), невозможным для случайного продления, если забыть удалить. Нельзя злоупотреблять этим интерфейсом.
Реализация метода EnumerateInLock
будет выглядеть следующим образом:
public void EnumerateInLock(Action<IEnumerable<T>> action)
{
var e = new EnumeratorImpl(this);
try
{
_lock.EnterReadLock();
action(e);
}
finally
{
e.Dispose();
_lock.ExitReadLock();
}
}
Обратите внимание, как EnumeratorImpl
(который не нуждается в отдельном коде блокировки) всегда располагается до выхода из блокировки. После удаления он выдает ObjectDisposedException
в ответ на любой вызов метода (кроме Dispose
, который игнорируется.)
Это означает, что даже при попытке злоупотребления интерфейсом:
IEnumerable<C> keepForLater = null;
someCollection.EnumerateInLock(e => keepForLater = e);
foreach (var item in keepForLater)
{
// aha!
}
Это будет всегда бросать, а не таинственно проваливаться иногда в зависимости от времени.
Использование метода, который принимает делегата, подобного этому, является общей техникой для управления временем жизни ресурса, обычно используемым в Лиспе и других динамических языках, и, хотя он менее гибок, чем реализация IDisposable
, эта пониженная гибкость часто является благословением: снимает беспокойство о клиентах, "забывающих избавиться".
Обновление
Из вашего комментария я вижу, что вам нужно иметь возможность передать ссылку на коллекцию в существующую структуру пользовательского интерфейса, которая, следовательно, будет иметь возможность использовать обычный интерфейс для коллекции, то есть непосредственно получить IEnumerable<T>
от этого и можно доверять, чтобы убрать это быстро. В каком случае зачем волноваться? Доверьтесь инфраструктуре пользовательского интерфейса для обновления пользовательского интерфейса и быстрого удаления коллекции.
Ваш единственный другой реалистичный вариант - просто сделать копию коллекции, когда запрашивается счетчик. Таким образом, блокировку необходимо удерживать только во время копирования. Как только он готов, замок снимается. Это может быть более эффективным, если коллекции обычно небольшие, поэтому накладные расходы на копирование меньше, чем экономия производительности из-за более коротких блокировок.
Заманчиво (примерно на наносекунду) предложить вам использовать простое правило: если коллекция меньше некоторого порога, сделайте копию, в противном случае сделайте это в своем первоначальном виде; выберите реализацию динамически. Таким образом, вы получите оптимальную производительность - установите порог (экспериментально) так, чтобы копия была дешевле, чем удержание блокировки. Однако я бы всегда дважды (или миллиард раз) думал о таких «умных» идеях в многопоточном коде, потому что что, если где-то есть злоупотребление перечислителем? Если вы забудете утилизировать его, вы не увидите проблемы , если это не большая коллекция ... Рецепт скрытых ошибок. Не ходи туда!
Еще один потенциальный недостаток подхода «раскрыть копию» заключается в том, что клиенты, несомненно, будут подчиняться предположению о том, что если элемент находится в коллекции, он открывается миру, но как только он удаляется из коллекции, он становится безопасно спрятан. Теперь это будет неправильно! Поток пользовательского интерфейса получит перечислитель, затем мой фоновый поток удалит из него последний элемент, а затем начнет изменять его, ошибочно полагая, что, поскольку он был удален, никто больше его не увидит.
Таким образом, подход к копированию требует, чтобы у каждого элемента в коллекции была эффективная собственная синхронизация, при этом большинство кодировщиков предполагают, что они могут сократить это, используя вместо этого синхронизацию коллекции.