Про Garbage Collector, Unity и слабые ссылки

    Есть у меня в проекте некий интерфейс IT и factory метод типа

    interface IT {}
    public IT CreateT(IA a, IB b, IC c, Type concreteType)
    {
        // куча говнокода, который создает объект типа concreteType, определяемого в месте вызова в рантайме.
    }
    


    Классы, реализующие IT, имеют конструкторы с разными сигнатурами, принимающими какую-то комбинацию объектов типов IA, IB или IC, поэтому с ростом количества реализаций IT, код их создания начинал пахнуть все сильнее, и, наконец, было принято решение его выкинуть и заменить простым кодом с Unity, примерно такого содержания:

    private static IT CreateITInternal(IA a, Type targetType)
    {
        using (UnityContainer cont = new UnityContainer())
        {
            cont.RegisterInstance<IA>(a, new ExternallyControlledLifetimeManager());
            cont.RegisterType(typeof(IT), targetType, new ExternallyControlledLifetimeManager());
            return cont.Resolve<IT>();
        }
    }
    

    (для простоты я оставлю только ссылки на IA, чтобы не загромождать код. Unity нам тут нужен только для облегчения инстанциирования объектов и мы не хотим давать ему контролировать время жизни никаких объектов, поэтому использован ExternallyControlledLifetimeManager)
    Код написан, тесты написаны, все зелено, все работает, выкатывам в продакшен. И тут началось…

    Вызывающий код выглядел примерно так:

    private static IT CreateIT()
    {
        var a = new A();
        Type concreteType = typeof(T);
        return CreateITInternal(a, concreteType);
    }
    
    static void Main(string[] args)
    {
        for (int i = 0; i < 1000; ++i)
        {
            CreateIT();
        }
    }
    

    классы и интерфейсы
    public interface IA { };
    public interface IT { };
    
    public class A : IA
    {
    }
    
    public class T : IT
    {
        public T(IA a)
        {
        }
    }
    


    Вроде ничего страшного, в Visual Studio прекрасно работает и debug и release. Но если пойти и запустить .exe-файл, он стабильно падает с таким исключением:

    Unhandled Exception: Microsoft.Practices.Unity.ResolutionFailedException: Resolution of the dependency failed, type = "WeakRefTest.IT", name = "(none)".
    Exception occurred while: while resolving.
    Exception is: InvalidOperationException - The current type, WeakRefTest.IA, is an interface and cannot be constructed. Are you missing a type mapping?
    -----------------------------------------------
    At the time of the exception, the container was:
    
      Resolving WeakRefTest.T,(none) (mapped from WeakRefTest.IT, (none))
      Resolving parameter "a" of constructor WeakRefTest.T(WeakRefTest.IA a)
        Resolving WeakRefTest.IA,(none)
     ---> System.InvalidOperationException: The current type, WeakRefTest.IA, is an interface and cannot be constructed. Are you missing a type mapping?
    

    Stack trace
       at Microsoft.Practices.ObjectBuilder2.DynamicMethodConstructorStrategy.ThrowForAttemptingToConstructInterface(IBuilderContext context)
       at lambda_method(Closure , IBuilderContext )
       at Microsoft.Practices.ObjectBuilder2.DynamicBuildPlanGenerationContext.<>c__DisplayClass1.<GetBuildMethod>b__0(IBuilderContext context)
       at Microsoft.Practices.ObjectBuilder2.DynamicMethodBuildPlan.BuildUp(IBuilderContext context)
       at Microsoft.Practices.ObjectBuilder2.BuildPlanStrategy.PreBuildUp(IBuilderContext context)
       at Microsoft.Practices.ObjectBuilder2.StrategyChain.ExecuteBuildUp(IBuilderContext context)
       at Microsoft.Practices.ObjectBuilder2.BuilderContext.NewBuildUp(NamedTypeBuildKey newBuildKey)
       at Microsoft.Practices.Unity.ObjectBuilder.NamedTypeDependencyResolverPolicy.Resolve(IBuilderContext context)
       at lambda_method(Closure , IBuilderContext )
       at Microsoft.Practices.ObjectBuilder2.DynamicBuildPlanGenerationContext.<>c__DisplayClass1.<GetBuildMethod>b__0(IBuilderContext context)
       at Microsoft.Practices.ObjectBuilder2.DynamicMethodBuildPlan.BuildUp(IBuilderContext context)
       at Microsoft.Practices.ObjectBuilder2.BuildPlanStrategy.PreBuildUp(IBuilderContext context)
       at Microsoft.Practices.ObjectBuilder2.StrategyChain.ExecuteBuildUp(IBuilderContext context)
       at Microsoft.Practices.Unity.UnityContainer.DoBuildUp(Type t, Object existing, String name, IEnumerable`1 resolverOverrides)
       --- End of inner exception stack trace ---
       at Microsoft.Practices.Unity.UnityContainer.DoBuildUp(Type t, Object existing, String name, IEnumerable`1 resolverOverrides)
       at Microsoft.Practices.Unity.UnityContainer.Resolve(Type t, String name, ResolverOverride[] resolverOverrides)
       at Microsoft.Practices.Unity.UnityContainerExtensions.Resolve[T](IUnityContainer container, ResolverOverride[] overrides)
       at WeakRefTest.Program.CreateITInternal(IA a, Type targetType)
       at WeakRefTest.Program.CreateIT()
       at WeakRefTest.Program.Main(String[] args)
    



    wat? Мы же буквально двумя строчками выше добавили объект в контейнер…

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

    Недолгое перечитывание документации дало подозреваемого: ExternallyControlledLifetimeManager, внутри себя он хранит слабые ссылки на объекты, помещаемые в контейнер, и возможно, если между помещением объекта типа IA в контейнер и запросом конструирования объекта типа IT он будет уничтожен, то конструирование должно упасть как раз с подобным исключением. Но с другой стороны, объект типа IA может быть удален только в случае, если слабая ссылка из нашего контейнера является единственной, но в нашем случае это не так! И в CreateITInternal и в CreateIT на стеке есть сильные ссылки на этот объект, что должно продлевать его жизнь как минимум до выхода из CreateIT. Или нет? Проверяем:

            private static IT CreateITInternal(IA a, Type targetType)
            {
                using (UnityContainer cont = new UnityContainer())
                {
                    cont.RegisterInstance<IA>(a, new ExternallyControlledLifetimeManager());
                    cont.RegisterType(typeof(IT), targetType, new ExternallyControlledLifetimeManager());
                    GC.Collect();
                    return cont.Resolve<IT>();
                }
            }
    


    Теперь падает на первой же итерации. Т.е. дело действительно в сборке мусора и слабых ссылках.

    Более простой пример:

            private static void TestWeakRef()
            {
                var sa = new A();
                var wa = new WeakReference<A>(sa);
                GC.Collect();
                A sa2;
                wa.TryGetTarget(out sa2);
                Console.WriteLine("{0}", sa2 == null ? "null" : "not null");
                Console.ReadLine();
            }
    


    В Visual Studio выдает «not null» при запуске вне студии выдает «null».

    Судя по всему, наличие сильных ссылок в коде не продлевает время жизни объекта до выхода этих ссылок из области видимости, а значение имеет реальное разыменование этих ссылок.

    Фикс чрезвычайно прост:
            private static IT CreateITInternal(IA a, Type targetType)
            {
                using (UnityContainer cont = new UnityContainer())
                {
                    cont.RegisterInstance<IA>(a, new ExternallyControlledLifetimeManager());
                    cont.RegisterType(typeof(IT), targetType, new ExternallyControlledLifetimeManager());
                    IT ret = cont.Resolve<IT>();
                    GC.KeepAlive(a);
                    return ret;
                }
            }
    


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

    Все еще непонятно, правда, почему при запуске в VS всегда работает.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 20

      +2
      Все еще непонятно, правда, почему при запуске в VS всегда работает.
      Вы открыли для себя оптимизатор.

      var sa = new A();
      var wa = new WeakReference<A>(new A());
      
      Видимо имелось ввиду new WeakReference<A>(sa)
        0
        Вы открыли для себя оптимизатор

        Visual Studio при запуске подавляет какие-то оптимизации?

        Видимо имелось ввиду new WeakReference<A>(sa)

        Конечно, спасибо за замечение, недоглядел.
          +2
          Да, для работы таких фич как Edit & Continue, Intermediate window, Locals и др.
        0
        У вас либо опечатка в TestWeakRef, либо, в общем-то, он и должен падать: вы пишете «var wa = new WeakReference(new A());», а должно было бы быть, по логике, «var wa = new WeakReference(sa);». То, что после GC удаляется объект «new A()», на который есть только слабая ссылка, вполне нормально, как я понимаю. Не уверен, как это соотносится с вашей исходной проблемой. [Edit: а мне надо обновлять комментарии перед отправкой своего]
          0
          Но в любом случае, после фразы «В Visual Studio выдает «not null» при запуске вне студии выдает «null».» я ожидал самого интересного, а вы уже закончили статью. Даже если бы TestWeakRef был правильным, хотелось бы прочитать больше, с документацией GC, Unity, с объяснением, почему в Visual Studio не падает и т.п.
            0
            Даже если будет new WeakReference(sa); — поведение не поменяется ;)
          +2
          Какой-то костыль. Вы воспользовались Unity (не самый лучший выбор) только из-за того, что он умеет сам подобрать нужный конструктор, а потом ещё к костылю приделали другой костыль в виде KeepAlive.
            0
            Главная мысль — время жизни объектов не соответствует времени жизни переменных, ссылающихся на эти объекты. Об этом стоит помнить, особенно если в проекте есть вызовы unmanaged кода или COM-объектов — в этом случае, ошибки буду куда более слабодиагностируемые.
            +3
            Недавно читал в блоге SergeyT пост на данную тему. Там все подробно объясняется http://sergeyteplyakov.blogspot.nl/2013/08/blog-post_27.html
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Нет, не избавляет.
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    Упрощенно проблемный код выглядит так:
                    void M(A a) {
                        f(a);
                        g();
                    }
                    
                    Здесь существует аргумент-ссылка на инстанс класса А, который передается в функцию f, которая сохраняет исключительно слабую ссылку, и функция g, которая использует эту слабую ссылку. Кроме самой переменной-аргумента «сильных» ссылок на инстанс нет.

                    Проблема в том, что для программиста не всегда очевидно, что данные из f в g передаются слабой ссылкой, а время жизни локальной переменной может быть ограничено её последним использованием.

                    То есть, как только в методе выше завершится вызов функции f инстанс класса может быть уничтожен и более не быть достижимым в функции g.

                    Решается продлением времени жизни переменной-аргумента, а, соответственно, и инстанса, специально созданным для этого костылём в виде GC.KeepAlive:
                    void M(A a) {
                        f(a);
                        g();
                        GC.KeepAlive(a);
                    }
                    
                      0
                      Есть другое наблюдение, которое не понятно.
                      То есть, как только в методе выше завершится вызов функции f инстанс класса может быть уничтожен

                      Ситуация аналогичная для кода, вызывающего функцию M(a).
                      Пока не завершится ф-ция M, вызывающий её код держит ссылку на экземпляр a и он не должен быть уничтожен.
                        0
                        Вызывающий код мог быть переписан компилятором в M(new A()). А учитывая, что в примере автора вообще Tail Call, оптимизация которого скорее всего отключается при запуске из-под дебаггера, там вообще что угодно могло произойти.
                          0
                          Ещё: насчет «завершения функции» я несколько неочевидно выразился. Вызывающий метод вполне может потерять ссылку на какой-то объект сразу после вызова метода, если используются хитрости со стеком и регистрами. По конвенции вызовов x64 первые 4 аргумента в стек писать не обязательно. Поэтому теоретически JIT мог переписать вызов M как вызов с поглощением стекового пространства каких-то переменных, оставив аргументы в регистрах. А для Value Types там другая картина, они могут быть «размазаны» по стеку угодным компилятору образом, и там, опять же, чёрт знает что происходит.
                            0
                            Если ссылка находится находится в локальной переменной, а оптимизатор переместил её в регистр, GC может уничтожить объект? Это баг.
                              0
                              Нет, в общем случае для GC нет разницы, где находится ссылка — на стеке или в регистре, он отследит и то, и то. Но даже нахождение ссылки на стеке может не спасти инстанс от уничтожения после вызова метода (до завершения) — то есть, после последнего использования.
                  –1
                  Так все правильно. У вас в последнем примере кода sa на момент GC.Collect() уже никем не держится. В методе на нее ссылок нет.

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

                    А в Unity ссылки слабые, потому что был использован ExternallyControlledLifetimeManager, другие life time менеджеры их не используют, но в том месте, контейнер короткоживущий, поэтому отдавать ему возможность управлять временем жизни объектов было не нужно.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое