Кэширование, ленивая загрузка и слабые ссылки на C#


Кэширование повышает производительность при низкой скорости получения данных. Отложенная инициализация (ленивая загрузка) может повысить производительность и сэкономить ресурсы для неиспользуемых объектов. Слабые ссылки позволяют производить раннюю сборку мусора. В этой статье я поясню подробнее и создам класс, демонстрирующий данные понятия.

Кэширование

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

Ленивая инициализация

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

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

Слабые cсылки

Обычно объекты, созданные в приложении .NET, используют строгие ссылки. Если имеются какие-либо доступные, надежные ссылки на объект, объект находится в управляемой куче. Как только все надежные ссылки на объект становятся недоступными, объект становится кандидатом на сборку мусора.

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

Общий кэш с отложенной загрузкой со слабыми ссылками

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

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

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

Создание класса

Мы начнем с создания нового класса для кэша. Добавьте класс с именем "LazyCache". Поскольку класс должен иметь возможность хранить любой объект, мы изменим определение, чтобы создать универсальный класс. Мы будем использовать экземпляр WeakReference<T> для хранения кэшированного объекта после его первой инициализации. Поскольку это разрешает только слабые ссылки на ссылочные типы, нам необходимо изменить наш параметр универсального типа, чтобы тип LazyCache<T> также ограничивался ссылочными типами.

Обновите стандартное определение класса, чтобы оно соответствовало следующему:

public class LazyCache<T> where T : class
{
}
Добавление полей

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

Func<T> _generateFunction;
WeakReference<T> _cache;
object _lock = new object();
Создание конструктора

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

Добавление свойства объекта

Заключительной частью класса является свойство объекта. Это свойство только для чтения, которое возвращает строгую ссылку на кэшированный объект. Свойство сначала проверяет, является ли поле _cache нулевым. Если это так, необходимо создать экземпляр слабой ссылки. Если объект не равен нулю, вызов метода TryGetTarget пытается создать надежную ссылку. Если это удастся, будет возвращена новая ссылка. Если нет, слабую ссылку необходимо повторно инициализировать.

В блоке кода оператора if мы выполняем функцию генерации для создания нового объекта. Это сохраняется в поле _cache, завернуто в слабую ссылку<T> и возвращается.

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

Добавьте свойство, как показано ниже:

public T Object
{
    get
    {
        lock (_lock)
        {
            T obj;
            if (_cache == null || !_cache.TryGetTarget(out obj))
            {
                obj = _generateFunction();
                _cache = new WeakReference<T>(obj);
            }
            return obj;
        }
    }
} 
Тестирование LazyCache<T>

Чтобы протестировать новый класс на простом примере, мы будем использовать функцию, которая медленно генерирует массив из десяти целых чисел. Мы добавим искусственную задержку в процесс, используя метод Sleep класса Thread. Включите в код следующую директиву using:

using System.Threading;
Пример кода для основного метода и двух вспомогательных методов показан ниже. GenerateIntegerArray создает и возвращает массив целых чисел, который мы будем кэшировать. Это занимает примерно десять секунд из-за вызовов в режиме ожидания. Чтобы показать прогресс, метод выводит символы на консоль. Метод ShowIntegerArray показывает содержимое массива целых чисел путем вывода чисел на консоль.

Метод Main инициализирует объект LazyCache параметром типа, который указывает, что он содержит массив целых чисел. Предоставленная функция вызывает метод GenerateIntegerArray, поэтому при первом доступе к кэшированному объекту или при чтении его после сборки мусора мы увидим десятисекундную задержку при построении массива.

Попробуйте запустить код. Из сообщений вы должны видеть, что кэшированный объект создается только один раз.

static void Main(string[] args)
{
    LazyCache<int[]> values = new LazyCache<int[]>(GenerateIntegerArray);
 
    Console.WriteLine("Cache Instantiated");
 
    ShowIntegerArray(values.Object);
    ShowIntegerArray(values.Object);
}
 
static int[] GenerateIntegerArray()
{
    Console.Write("Generating");
 
    int[] array = new int[10];
    for (int i = 0; i < 10; i++)
    {
        array[i] = i * i;
        Thread.Sleep(1000);
        Console.Write(".");
    }
    Console.WriteLine();
    return array;
}
 
static void ShowIntegerArray(int[] array)
{
    Console.Write("Array > ");
    for (int i = 0; i < array.Length; i++)
    {
        Console.Write(" {0}", array[i]);
    }
    Console.WriteLine();
}
 
/* 
 
Cache Instantiated
Generating..........
Array >  0 1 4 9 16 25 36 49 64 81
Array >  0 1 4 9 16 25 36 49 64 81
 
*/ 
Чтобы показать, что кэш может перестроиться после того, как слабая ссылка будет собрана в мусор, измените содержимое основного метода, как показано ниже. Новая версия выводит массив дважды перед началом сборки мусора. При третьем доступе вы должны увидеть сообщение "Генерация" и связанную с ним задержку по мере восстановления массива.

LazyCache<int[]> values = new LazyCache<int[]>(GenerateIntegerArray);
 
Console.WriteLine("Cache Instantiated");
 
ShowIntegerArray(values.Object);
ShowIntegerArray(values.Object);
 
GC.Collect();
 
ShowIntegerArray(values.Object);
 
/* 
 
Cache Instantiated
Generating..........
Array >  0 1 4 9 16 25 36 49 64 81
Array >  0 1 4 9 16 25 36 49 64 81
Generating..........
Array >  0 1 4 9 16 25 36 49 64 81
 
*/
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, си шарп




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



Урок 3. Добавление Vue в проект и создание приложения
Программа для тестов: подключаем Firebird
Статьи IT