Есть у меня в проекте некий интерфейс 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 всегда работает.

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


  1. a553
    03.10.2015 13:50
    +2

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

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


    1. andreishe
      03.10.2015 22:14

      Вы открыли для себя оптимизатор

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

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

      Конечно, спасибо за замечение, недоглядел.


      1. a553
        03.10.2015 22:39
        +2

        Да, для работы таких фич как Edit & Continue, Intermediate window, Locals и др.


  1. Bas1l
    03.10.2015 13:53

    У вас либо опечатка в TestWeakRef, либо, в общем-то, он и должен падать: вы пишете «var wa = new WeakReference(new A());», а должно было бы быть, по логике, «var wa = new WeakReference(sa);». То, что после GC удаляется объект «new A()», на который есть только слабая ссылка, вполне нормально, как я понимаю. Не уверен, как это соотносится с вашей исходной проблемой. [Edit: а мне надо обновлять комментарии перед отправкой своего]


    1. Bas1l
      03.10.2015 13:56

      Но в любом случае, после фразы «В Visual Studio выдает «not null» при запуске вне студии выдает «null».» я ожидал самого интересного, а вы уже закончили статью. Даже если бы TestWeakRef был правильным, хотелось бы прочитать больше, с документацией GC, Unity, с объяснением, почему в Visual Studio не падает и т.п.


      1. Nagg
        03.10.2015 13:57

        Даже если будет new WeakReference(sa); — поведение не поменяется ;)


  1. Nagg
    03.10.2015 13:55
    +2

    Какой-то костыль. Вы воспользовались Unity (не самый лучший выбор) только из-за того, что он умеет сам подобрать нужный конструктор, а потом ещё к костылю приделали другой костыль в виде KeepAlive.


    1. andreishe
      03.10.2015 22:13

      Главная мысль — время жизни объектов не соответствует времени жизни переменных, ссылающихся на эти объекты. Об этом стоит помнить, особенно если в проекте есть вызовы unmanaged кода или COM-объектов — в этом случае, ошибки буду куда более слабодиагностируемые.


  1. druss
    04.10.2015 00:42
    +3

    Недавно читал в блоге SergeyT пост на данную тему. Там все подробно объясняется http://sergeyteplyakov.blogspot.nl/2013/08/blog-post_27.html


  1. Mixim333
    04.10.2015 10:43
    -3

    Не знаю, лично я всегда пишу методы со следующей архитектурой:
    public T MyMethod(int A)
    {
    T returnedValue;
    returnedValue=MyClass.Resolve(A);
    return returnedValue;//всегда создаю переменную returnedValue
    }

    — мне так и отлаживать проще и, оказывается, это меня еще и избавляет от описанных проблем


    1. a553
      04.10.2015 10:51

      Нет, не избавляет.


      1. Mixim333
        04.10.2015 14:15

        За что минусуют — не понял. Ни могли бы объяснить более подробно, почему не избавляет?


        1. a553
          04.10.2015 14:39

          Упрощенно проблемный код выглядит так:

          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);
          }
          


          1. qw1
            04.10.2015 15:16

            Есть другое наблюдение, которое не понятно.

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

            Ситуация аналогичная для кода, вызывающего функцию M(a).
            Пока не завершится ф-ция M, вызывающий её код держит ссылку на экземпляр a и он не должен быть уничтожен.


            1. a553
              04.10.2015 17:33

              Вызывающий код мог быть переписан компилятором в M(new A()). А учитывая, что в примере автора вообще Tail Call, оптимизация которого скорее всего отключается при запуске из-под дебаггера, там вообще что угодно могло произойти.


            1. a553
              04.10.2015 17:39

              Ещё: насчет «завершения функции» я несколько неочевидно выразился. Вызывающий метод вполне может потерять ссылку на какой-то объект сразу после вызова метода, если используются хитрости со стеком и регистрами. По конвенции вызовов x64 первые 4 аргумента в стек писать не обязательно. Поэтому теоретически JIT мог переписать вызов M как вызов с поглощением стекового пространства каких-то переменных, оставив аргументы в регистрах. А для Value Types там другая картина, они могут быть «размазаны» по стеку угодным компилятору образом, и там, опять же, чёрт знает что происходит.


              1. qw1
                04.10.2015 21:38

                Если ссылка находится находится в локальной переменной, а оптимизатор переместил её в регистр, GC может уничтожить объект? Это баг.


                1. a553
                  05.10.2015 03:12

                  Нет, в общем случае для GC нет разницы, где находится ссылка — на стеке или в регистре, он отследит и то, и то. Но даже нахождение ссылки на стеке может не спасти инстанс от уничтожения после вызова метода (до завершения) — то есть, после последнего использования.


  1. GrigoryPerepechko
    05.10.2015 10:52
    -1

    Так все правильно. У вас в последнем примере кода sa на момент GC.Collect() уже никем не держится. В методе на нее ссылок нет.

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


    1. andreishe
      07.10.2015 19:37

      Переменная sa существует до конца метода. В моем представлении, объект, на который она ссылается тоже должен был бы существовать до конца метода.

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