Ошибка в WeakAction в случае действия Closure

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

Все работает хорошо, за исключением одного случая - когда действие создается в закрытии. рассмотрим следующий пример:

public class A 
{
     WeakAction action = null;

     private void _register(string msg) 
     {
         action = new WeakAction(() => 
         {
             MessageBox.Show(msg);
         }
     }
}

Поскольку выражение лямбда использует локальную переменную msg, компилятор С# автоматически генерирует вложенный класс для хранения всех переменных замыкания. Целью действия является экземпляр вложенного класса вместо экземпляра A. Действие, переданное конструктору WeakAction, не упоминается после завершения конструктора и поэтому сборщик мусора может немедленно его утилизировать. Позже, если выполняется WeakAction, это не сработает, потому что цель больше не жива, даже если исходный экземпляр A жив.

Теперь я не могу изменить способ вызова WeakAction (поскольку он широко используется), но я могу изменить его реализацию. Я думал о попытке найти способ получить доступ к экземпляру A и заставить экземпляр вложенного класса оставаться в живых, пока экземпляр A еще жив, но я не знаю, как его получить.

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

Экземпляр класса A требует, чтобы экземпляр класса B уведомлял его, когда что-то происходит, поэтому он обеспечивает обратный вызов с использованием объекта Action. A не знает, что B использует слабые действия, он просто предоставляет Action для выполнения обратного вызова. Тот факт, что B использует WeakAction, является деталью реализации, которая не отображается. B необходимо сохранить это действие и использовать его при необходимости. Но B может жить намного дольше, чем A, и удерживая сильную ссылку на нормальное действие (которое само по себе содержит сильную ссылку на экземпляр A, который его создал) заставляет A никогда не собираться мусором, Если A является частью списка элементов, которые больше не являются живыми, мы ожидаем, что A будет собрано мусором, и из-за ссылки, что B содержит действие, которое само указывает на A, у нас есть утечка памяти.

Итак, вместо B, содержащего действие, которое A предоставлено, B обертывает его в WeakAction и сохраняет только слабое действие. Когда придет время называть его, B делает это только в том случае, если WeakAction все еще жив, и он должен быть до тех пор, пока A все еще жив.

A создает это действие внутри метода и не сохраняет ссылку на него самостоятельно - это заданное. Поскольку Action был создан в контексте конкретного экземпляра A, этот экземпляр является объектом A, а когда A умирает, все слабые ссылки на него становятся null, поэтому B знает не называть его и распоряжаться объектом WeakAction.

Но иногда метод, который генерировал Action, использует переменные, определенные локально в этой функции. В этом случае контекст, в котором выполняется действие, включает не только экземпляр A, но также состояние локальных переменных внутри метода (называемого "замыканием" ). Компилятор С# делает это, создавая скрытый вложенный класс для хранения этих переменных (позволяет вызывать его A__closure), а экземпляр, который становится объектом Action, является экземпляром A__closure, а не A. Это то, о чем пользователь не должен знать. За исключением того, что этот экземпляр A__closure ссылается только на объект Action. И так как мы создаем слабую ссылку на цель и не сохраняем ссылку на действие, ссылка на экземпляр A__closure отсутствует, и сборщик мусора может (и обычно делает это) немедленно избавиться от него. Таким образом, A живет, A__closure умирает, и, несмотря на то, что A все еще ожидает вызова обратного вызова, B не может этого сделать.

Это ошибка.

Мой вопрос состоял в том, что если кто-то знает, как конструктор WeakAction, единственный фрагмент кода, который фактически содержит исходный объект Action, временно каким-то магическим способом может извлечь исходный экземпляр A из A__closure экземпляр, который он находит в Target Action. Если это так, я мог бы продлить жизненный цикл A__closure в соответствии с A.

Ответы

Ответ 1

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

Более глубокое исследование объекта Action, переданного конструктору WeakEvent, и особенно свойство Action.Target, показало, что есть фактически два разных случая замыкающих объектов.

В первом случае Lambda использует локальные переменные из области вызывающей функции, но не использует никакой информации из экземпляра класса A. В следующем примере предположим EventAggregator.Register - это метод, который выполняет действие и хранит WeakAction, который его обертывает.

public class A 
{
    public void Listen(int num) 
    {
        EventAggregator.Register<SomeEvent>(_createListenAction(num));
    }

    public Action _createListenAction(int num) 
    {
        return new Action(() => 
        {
            if (num > 10) MessageBox.Show("This is a large number");
        });
    }
}

Созданная здесь лямбда использует переменную num, которая является локальной переменной, определенной в области функции _createListenAction. Поэтому компилятор должен обернуть его классом замыкания, чтобы поддерживать переменные замыкания. Однако, поскольку лямбда не имеет доступа ни к одному из членов класса А, нет необходимости хранить ссылку на А. Таким образом, цель действия не будет содержать ссылки на экземпляр А, и нет никакого способа для конструктора WeakAction чтобы достичь этого.

Второй случай показан в следующем примере:

public class A 
{
   int _num = 10;

    public void Listen() 
    {
        EventAggregator.Register<SomeEvent>(_createListenAction());
    }

    public Action _createListenAction() 
    {
        return new Action(() => 
        {
            if (_num > 10) MessageBox.Show("This is a large number");
        });
    }
}

Теперь _num не предоставляется в качестве параметра функции, он исходит из экземпляра класса A. Использование отражения для изучения структуры объекта Target показывает, что последнее поле, которое определяет компилятор, содержит ссылку на экземпляр класса A. Этот случай также применяется, когда лямбда содержит вызовы методов-членов, как в следующем примере:

public class A 
{
    private void _privateMethod() 
    {
       // do something here
    }        

    public void Listen() 
    {
        EventAggregator.Register<SomeEvent>(_createListenAction());
    }

    public Action _createListenAction() 
    {
        return new Action(() => 
        {
             _privateMethod();
        });
    }
}

_privateMethod является функцией-членом, поэтому она вызывается в контексте экземпляра класса A, поэтому закрытие должно содержать ссылку на нее, чтобы вызвать лямбда в правильном контексте.

Итак, первым случаем является Закрытие, которое содержит только локальную переменную функций, вторая содержит ссылку на родительский экземпляр. В обоих случаях нет жестких ссылок на экземпляр Closure, поэтому, если конструктор WeakAction просто оставляет вещи так, как они есть, WeakAction будет "умирать" мгновенно, несмотря на то, что экземпляр класса A все еще жив.

Мы столкнулись с тремя различными проблемами:

  • Как определить, что цель действия - вложенное закрытие экземпляр класса, а не исходный экземпляр экземпляра?
  • Как получить ссылку на исходный экземпляр класса A?
  • Как продлить срок службы экземпляра закрытия так, чтобы он до тех пор, пока существует экземпляр A, но не выше этого?

Ответ на первый вопрос заключается в том, что мы полагаемся на 3 характеристики экземпляра замыкания: - Он является частным (точнее, он не является "видимым". При использовании компилятора С# отраженный тип имеет IsPrivate, установленный в true, но с VB это не так. Во всех случаях свойство IsVisible является ложным). - Он вложен. - Как заметил в своем ответе @DarkFalcon, он украшен атрибутом [CompilerGenerated].

private static bool _isClosure(Action a)
{
    var typ = a.Target.GetType();
    var isInvisible = !typ.IsVisible;
    var isCompilerGenerated = Attribute.IsDefined(typ, typeof(CompilerGeneratedAttribute));
    var isNested = typ.IsNested && typ.MemberType == MemberTypes.NestedType;


    return isNested && isCompilerGenerated && isInvisible;
}

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

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

Задача 3 также легко разрешима. Как написал @usr в своем ответе, как только мы получим экземпляр класса A, добавив ConditionalWeakTable с единственной записью, в которой экземпляр класса A является ключом, а экземпляр замыкания является целью, решает проблему. Сборщик мусора знает, что он не собирает экземпляр замыкания, пока живет экземпляр класса A. Так что хорошо.

Единственная неразрешимая проблема - вторая, , как получить ссылку на экземпляр класса A?Как я уже сказал, есть два случая закрытия. Там, где компилятор создает элемент, который содержит этот экземпляр, и тот, где он не работает. Во втором случае просто нет способа получить его, поэтому единственное, что мы можем сделать, это создать твердую ссылку на экземпляр закрытия, чтобы сохранить его от моментального сбора мусора. Это означает, что он может проживать экземпляр класса А (на самом деле он будет жить до тех пор, пока живет экземпляр WeakAction, который может быть вечно). Но это не такой ужасный случай. Класс замыкания в этом случае содержит только несколько локальных переменных, а в 99,9% случаев это очень малая структура. Хотя это все еще утечка памяти, она не является существенной.

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

public WeakAction(object target, Action action) {...}

И когда этот конструктор вызывается, мы добавляем запись ConditionalWeakTable, где цель - это ключ, а целью действия является значение. Мы также придерживаемся слабой ссылки как на цель, так и на цель действий, и если кто-либо из них умирает, мы очищаем оба. Таким образом, цель действий не меньше, чем цель. Это в основном позволяет пользователю WeakAction сказать, чтобы он держался за экземпляр замыкания, пока жив мишень. Поэтому новым пользователям будет предложено использовать его, чтобы избежать утечек памяти. Но в существующих проектах, где этот новый конструктор не используется, это, по крайней мере, минимизирует утечки памяти для закрытий, которые не имеют ссылки на экземпляр класса A.

Случай замыканий, которые ссылаются на родителя, более проблематичен, поскольку они влияют на сбор гарбаз. Если мы серьезно относимся к закрытию, мы вызываем гораздо более резкую утечку памяти, потому что экземпляр класса A также никогда не будет очищен. Но этот случай также легче лечить. Поскольку компилятор добавляет последний элемент, который содержит ссылку на экземпляр класса A, мы просто используем рефлексию для ее извлечения и выполняем именно то, что делаем, когда пользователь предоставляет его в конструкторе. Мы идентифицируем этот случай, когда последний член экземпляра замыкания имеет тот же тип, что и тип объявления замыкающего вложенного класса. (Опять же, он не на 100% точнее, но для реальных случаев его достаточно близко).

Подводя итог, решение, представленное здесь, не является 100% -ным запечатанным решением, просто потому, что похоже, что такого решения не существует. Но поскольку мы должны предоставить НЕКОТОРЫЙ ответ на эту досадную ошибку, это решение по крайней мере существенно уменьшает проблему.

Ответ 2

Вы хотите продлить время жизни экземпляра класса закрытия точно так же, как в экземпляре A. CLR имеет специальный тип ручки GC для этого: Ephemeron, реализованный как internal struct DependentHandle.

  • Эта структура отображается только как часть класса ConditionalWeakTable. Вы можете создать одну такую ​​таблицу за WeakAction ровно с одним элементом в ней. Ключ будет экземпляром A, значением будет экземпляр класса закрытия.
  • В качестве альтернативы вы можете открыть DependentHandle с помощью частного отражения.
  • Или вы можете использовать один глобальный общий экземпляр ConditionalWeakTable. Это, вероятно, требует синхронизации. Посмотрите на документы.

Рассмотрите возможность открытия проблемы с подключением, чтобы сделать DependentHandle общедоступным и связать этот вопрос, чтобы обеспечить использование.

Ответ 3

a.Target обеспечивает доступ к объекту, который содержит параметры лямбда. Выполнение GetType на этом возвращает тип, сгенерированный компилятором. Один из вариантов - проверить этот тип для настраиваемого атрибута System.Runtime.CompilerServices.CompilerGeneratedAttribute и сохранить в этом случае сильную ссылку на объект.

Now I can't change the way the WeakAction is called, (since it in wide use) but I can change it implementation. Обратите внимание, что это единственный способ до сих пор сохранить его, не требуя изменений в построении WeakAction. Он также не достигает цели сохранения лямбды в живых до тех пор, пока объект A (он сохранит его до тех пор, пока вместо WeakAction). Я не верю, что это будет достигнуто без изменения способа построения WeakAction, как это сделано в других ответах. Как минимум, WeakAction необходимо получить ссылку на объект A, который вы в настоящее время не предоставляете.