Потокобезопасный ленивый класс

У меня есть класс Lazy, который лениво оценивает выражение:

public sealed class Lazy<T>
{
    Func<T> getValue;
    T value;

    public Lazy(Func<T> f)
    {
        getValue = () =>
            {
                lock (getValue)
                {
                    value = f();
                    getValue = () => value;
                }
                return value;
            };
    }

    public T Force()
    {
        return getValue();
    }
}

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

Это очевидно работает в моем тестировании, но я не могу знать, взорвется ли это в производственной среде.

Является ли мой класс потокобезопасным? Если нет, что можно сделать, чтобы гарантировать потокобезопасность?


person Juliet    schedule 02.12.2009    source источник


Ответы (4)


Разве вы не можете просто полностью отказаться от повторной оценки функции, используя либо флаг, либо защитное значение для реального значения? То есть:

public sealed class Lazy<T>
{
    Func<T> f;
    T value;
    volatile bool computed = false;
    void GetValue() { lock(LockObject) { value = f();  computed = true; } }

    public Lazy(Func<T> f)
    {
        this.f = f;
    }

    public T Force()
    {
        if (!computed) GetValue();
        return value;
    }
}
person Konrad Rudolph    schedule 02.12.2009
comment
Ваш код не является потокобезопасным, оптимизатор может изменить порядок операций записи или чтения в вашем коде. Чтобы сделать ваш код безопасным, вам придется пометить bool, вычисляемый как volatile. В общем, всякий раз, когда вы читаете переменную без блокировки, которая может быть установлена ​​из другого потока, вам нужно подумать о модели памяти и о том, где вам нужны барьеры или непостоянные операции чтения/записи. - person Daniel; 02.12.2009
comment
+1, +ответ: у меня плохая привычка носить перчатки усложнителя, и именно по этой причине мне никогда не следует разрешать прикасаться к коду :) Как отмечалось выше, я думаю, что пометка флага как изменчивого даст желаемые результаты с большим количеством меньше магии. - person Juliet; 02.12.2009
comment
@Daniel: зачем вычисляемая переменная должна быть помечена как volatile, если она уже содержится в операторе блокировки? Нужно ли по тем же причинам помечать значение переменной как volatile? - person Juliet; 02.12.2009
comment
вычисленный должен быть помечен как volatile, потому что доступ к нему осуществляется без надлежащей блокировки: блокировка при чтении переменной отсутствует. Пометка вычисляемого как volatile гарантирует, что запись в него будет выполнена после предыдущих операций записи, выполненных этим потоком, и что он будет прочитан до того, как читающий поток выполнит другие операции чтения. Дополнительные сведения см. на странице albahari.com/threading/part4.aspx#_NonBlockingSynch. value не нуждается в volatile, потому что оно считывается после того, как вычисленное значение уже считается истинным. - person Daniel; 03.12.2009

В вашем коде есть несколько проблем:

  1. Вам нужен один объект для блокировки. Не блокируйте изменяемую переменную — блокировки всегда имеют дело с объектами, поэтому, если getValue изменится, несколько потоков могут одновременно войти в заблокированный раздел.

  2. Если несколько потоков ожидают блокировки, все они будут вычислять функцию f() друг за другом. Вам нужно будет проверить внутри блокировки, что функция еще не была оценена.

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

Однако вместо этого я бы использовал подход с флагом от Конрада Рудольфа (только убедитесь, что вы не забыли о «изменчивости», необходимой для этого). Таким образом, вам не нужно вызывать делегата всякий раз, когда значение извлекается (вызовы делегата выполняются довольно быстро, но не так быстро, как простая проверка логического значения).

person Daniel    schedule 02.12.2009

Я не совсем понимаю, что вы пытаетесь сделать с этим кодом, но я просто опубликовал статью в The Code Project о создании своего рода "ленивого" класса, который автоматически асинхронно вызывает рабочую функцию и сохраняет ее значение.

person Adam Maras    schedule 02.12.2009
comment
Очень интересная статья :) Но это не лень в том смысле, который интересует большинство людей. Выделяются два варианта использования: 1) большинство ORM используют ленивую загрузку, чтобы предотвратить одновременное создание экземпляра всего графа объектов базы данных в памяти. 2) Целый класс неизменяемых структур данных (примечательные потоки и некоторые реализации очередей) зависит от лени для некоторых амортизированных характеристик производительности. - person Juliet; 02.12.2009

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

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

Thread 1
Thread 2
Thread 1 completes

Поток 2 никогда не завершится, потому что Поток 1 снимет блокировку с ссылки, отличной от той, которая использовалась для получения блокировки (точнее, он снимет несуществующую блокировку, поскольку вновь созданная ссылка никогда не была заблокирована с самого начала). ) и не снимать исходную блокировку, блокирующую поток 2.

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

public sealed class Lazy<T>
{
    Func<T> getValue;
    T value;
    object lockValue = new object();

    public Lazy(Func<T> f)
    {
        getValue = () =>
            {
                lock (lockValue)
                {
                    value = f();
                    getValue = () => value;
                }
                return value;
            };
    }

    public T Force()
    {
        return getValue();
    }
}
person Adam Robinson    schedule 02.12.2009
comment
Теперь это гарантирует, что f() не вызывается одновременно; но его все равно можно вызывать несколько раз. - person Daniel; 02.12.2009
comment
@Daniel: Да, ты прав. Блокировку нужно будет переместить в метод Force, чтобы предотвратить это. - person Adam Robinson; 02.12.2009