Вопрос

Я собираю на заказ SynchronizedCollection<T> Класс, чтобы я мог иметь синхронизированную наблюдаемую коллекцию для моего приложения WPF. Синхронизация предоставляется через ReaderWriterLockSlim, который, по большей части, было легко применить. Дело, у меня проблемы с тем, как обеспечить безопасное переключение перечисления сбора. Я создал обычай IEnumerator<T> Вложенный класс, который выглядит так:

    private class SynchronizedEnumerator : IEnumerator<T>
    {
        private SynchronizedCollection<T> _collection;
        private int _currentIndex;

        internal SynchronizedEnumerator(SynchronizedCollection<T> collection)
        {
            _collection = collection;
            _collection._lock.EnterReadLock();
            _currentIndex = -1;
        }

        #region IEnumerator<T> Members

        public T Current { get; private set;}

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            var collection = _collection;
            if (collection != null)
                collection._lock.ExitReadLock();

            _collection = null;
        }

        #endregion

        #region IEnumerator Members

        object System.Collections.IEnumerator.Current
        {
            get { return Current; }
        }

        public bool MoveNext()
        {
            var collection = _collection;
            if (collection == null)
                throw new ObjectDisposedException("SynchronizedEnumerator");

            _currentIndex++;
            if (_currentIndex >= collection.Count)
            {
                Current = default(T);
                return false;
            }

            Current = collection[_currentIndex];
            return true;
        }

        public void Reset()
        {
            if (_collection == null)
                throw new ObjectDisposedException("SynchronizedEnumerator");

            _currentIndex = -1;
            Current = default(T);
        }

        #endregion
    }

Моя проблема, однако, заключается в том, что если перечислетель не располагается, замок никогда не будет выпущен. В большинстве случаев использования это не проблема, поскольку Foreach должен правильно вызовить утилизацию. Это может быть проблемой, однако, если потребитель извлекает явный экземпляр перечисления. Является ли мой единственный вариант для документирования класса с помощью реализатора предостережения, напоминающий потребителю, чтобы позвонить с утилизацией, если использование перечисления явно или есть способ безопасно освободить блокировку во время доработки? Я не думаю, что я думаю, так как финализатор даже не бежит в том же ниве, но мне было любопытно, если там другие способы улучшить это.


РЕДАКТИРОВАТЬ

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

Это было полезно?

Решение

Вы правы, это проблема. Финализатор бесполезный, он будет бежать слишком поздно, чтобы иметь любое использование. Код должен иметь в любом случае, код не заблокирован до этого. К сожалению, для вас нет никакого способа сказать разницу между Foreach, вызывающим ваши члены MoveEnext / quie или Client Code, используя их явно.

Нет исправления, не делай этого. Майкрософт тоже этого не делал, у них было много разума в .NET 1.x. Единственным реальным потоком безопасным итератором, который вы можете сделать, является то, что создает копию объекта коллекции в методе GetEnumerator (). Итератор выходит из синхронизации с коллекцией, не так ли не радость.

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

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

Обычно это хорошая идея, чтобы дублировать общие узоры - представляют собой перечисленную коллекцию с 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!
}

Это будет всегда бросить, а не загадость иногда исходя из синхронизации.

Используя метод, который принимает следующий делегат, такой как это общее метод управления жизненным временем ресурсов, обычно используемых в LISP и других динамических языках, и пока оно менее гибкое, чем реализация IDisposable, что пониженная гибкость часто является благословением: он удаляет беспокойство по поводу клиентов «забыть о том, чтобы выбрать».

Обновлять

С вашего комментариев я вижу, что вам нужно иметь возможность передавать ссылку на коллекцию в существующую структуру интерфейса, которая, следовательно, ожидает, что сможет использовать обычный интерфейс для коллекции, то есть непосредственно получить IEnumerable<T> От него и доверять, чтобы быстро очистить его. В каком случае зачем беспокоиться? Доверьтесь Framework UI, чтобы обновить пользовательский интерфейс и быстро утилизируйте коллекцию.

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

Это заманчиво (примерно на наносекунде), чтобы предположить, что вы используете простое правило: если коллекция меньше некоторого порога, сделайте копию, в противном случае сделайте его оригинал; Выберите реализацию динамически. Таким образом, вы получаете оптимальную производительность - установите порог (экспериментом), так что копия дешевле, чем удерживая замок. Тем не менее, я всегда буду думать дважды (или миллиард раз) о таких «умных» идеях в резьбовом коде, потому что, если где-то есть злоупотребление перечислетелем? Если вы забудете утилизировать его, вы не увидите проблему Если это не большая коллекция... Рецепт для скрытых ошибок. Не ходите туда!

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

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

Я должен был сделать это недавно. То, как я сделал это, это было, чтобы аннотация его, так что есть внутренний объект (ссылка), который содержит обе Актуальный список / массив а также счет (и GetEnumerator() реализация; Тогда я могу сделать безблокировать, безопасное переключение, имея:

public IEnumerator<T> GetEnumerator() { return inner.GetEnumerator();}

То Add и т. Д. Нужно синхронизировать, но они изменять то inner Ссылка (поскольку ссылочные обновления являются атомными, вы нет нужно синхронизировать GetEnumerator()). Это означает, что любой перечислетель вернется как можно больше предметов, сколько было Когда перечислетель был создан.

Конечно, это помогает, чтобы мой сценарий был простым, и мой список был Add Только ... Если вам нужно поддерживать мутиру / удалить, то это намного сложнее.

В использовании должен IDisposable Реализация, вы создаете защищенные Dispose(bool managed) Метод, который всегда располагает неуправляемыми ресурсами, которые вы используете. Призывая к защиту Dispose(false) Метод от вашего финаризатора, Вы утили замок по мере необходимости. Отказ Замок управляется, вы утили ли вы только тогда, когда Dispose(true) называется, где true означает, что управляемые объекты должны быть расположены. В противном случае, когда публика Dispose() называется явно, он называет защищенный Dispose(true) а также GC.SuppressFinalize(this) Для предотвращения работы финализатора (потому что больше нечего распоряжаться больше).

Поскольку вы никогда не знаете, когда пользователь сделан с помощью перечисления, у вас нет другого варианта, чем документирование того, что пользователь должен выбрать объект. Возможно, вы захотите предложить, чтобы пользователь использует using(){ ... } Строительство, которое автоматически располагает объектом при совершении.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top