Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров21K
Есть у меня в проекте некий интерфейс 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 всегда работает.
Теги:
Хабы:
+10
Комментарии20

Публикации

Истории

Работа

.NET разработчик
75 вакансий

Ближайшие события