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?
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)
Bas1l
03.10.2015 13:53У вас либо опечатка в TestWeakRef, либо, в общем-то, он и должен падать: вы пишете «var wa = new WeakReference(new A());», а должно было бы быть, по логике, «var wa = new WeakReference(sa);». То, что после GC удаляется объект «new A()», на который есть только слабая ссылка, вполне нормально, как я понимаю. Не уверен, как это соотносится с вашей исходной проблемой. [Edit: а мне надо обновлять комментарии перед отправкой своего]
Bas1l
03.10.2015 13:56Но в любом случае, после фразы «В Visual Studio выдает «not null» при запуске вне студии выдает «null».» я ожидал самого интересного, а вы уже закончили статью. Даже если бы TestWeakRef был правильным, хотелось бы прочитать больше, с документацией GC, Unity, с объяснением, почему в Visual Studio не падает и т.п.
Nagg
03.10.2015 13:55+2Какой-то костыль. Вы воспользовались Unity (не самый лучший выбор) только из-за того, что он умеет сам подобрать нужный конструктор, а потом ещё к костылю приделали другой костыль в виде KeepAlive.
andreishe
03.10.2015 22:13Главная мысль — время жизни объектов не соответствует времени жизни переменных, ссылающихся на эти объекты. Об этом стоит помнить, особенно если в проекте есть вызовы unmanaged кода или COM-объектов — в этом случае, ошибки буду куда более слабодиагностируемые.
Mixim333
04.10.2015 10:43-3Не знаю, лично я всегда пишу методы со следующей архитектурой:
public T MyMethod(int A)
{
T returnedValue;
returnedValue=MyClass.Resolve(A);
return returnedValue;//всегда создаю переменную returnedValue
}
— мне так и отлаживать проще и, оказывается, это меня еще и избавляет от описанных проблемa553
04.10.2015 10:51Нет, не избавляет.
Mixim333
04.10.2015 14:15За что минусуют — не понял. Ни могли бы объяснить более подробно, почему не избавляет?
a553
04.10.2015 14:39Упрощенно проблемный код выглядит так:
Здесь существует аргумент-ссылка на инстанс класса А, который передается в функцию f, которая сохраняет исключительно слабую ссылку, и функция g, которая использует эту слабую ссылку. Кроме самой переменной-аргумента «сильных» ссылок на инстанс нет.void M(A a) { f(a); g(); }
Проблема в том, что для программиста не всегда очевидно, что данные из f в g передаются слабой ссылкой, а время жизни локальной переменной может быть ограничено её последним использованием.
То есть, как только в методе выше завершится вызов функции f инстанс класса может быть уничтожен и более не быть достижимым в функции g.
Решается продлением времени жизни переменной-аргумента, а, соответственно, и инстанса, специально созданным для этого костылём в виде GC.KeepAlive:
void M(A a) { f(a); g(); GC.KeepAlive(a); }
qw1
04.10.2015 15:16Есть другое наблюдение, которое не понятно.
То есть, как только в методе выше завершится вызов функции f инстанс класса может быть уничтожен
Ситуация аналогичная для кода, вызывающего функцию M(a).
Пока не завершится ф-ция M, вызывающий её код держит ссылку на экземпляр a и он не должен быть уничтожен.a553
04.10.2015 17:33Вызывающий код мог быть переписан компилятором в
M(new A())
. А учитывая, что в примере автора вообще Tail Call, оптимизация которого скорее всего отключается при запуске из-под дебаггера, там вообще что угодно могло произойти.
a553
04.10.2015 17:39Ещё: насчет «завершения функции» я несколько неочевидно выразился. Вызывающий метод вполне может потерять ссылку на какой-то объект сразу после вызова метода, если используются хитрости со стеком и регистрами. По конвенции вызовов x64 первые 4 аргумента в стек писать не обязательно. Поэтому теоретически JIT мог переписать вызов M как вызов с поглощением стекового пространства каких-то переменных, оставив аргументы в регистрах. А для Value Types там другая картина, они могут быть «размазаны» по стеку угодным компилятору образом, и там, опять же, чёрт знает что происходит.
qw1
04.10.2015 21:38Если ссылка находится находится в локальной переменной, а оптимизатор переместил её в регистр, GC может уничтожить объект? Это баг.
a553
05.10.2015 03:12Нет, в общем случае для GC нет разницы, где находится ссылка — на стеке или в регистре, он отследит и то, и то. Но даже нахождение ссылки на стеке может не спасти инстанс от уничтожения после вызова метода (до завершения) — то есть, после последнего использования.
GrigoryPerepechko
05.10.2015 10:52-1Так все правильно. У вас в последнем примере кода sa на момент GC.Collect() уже никем не держится. В методе на нее ссылок нет.
Другой вопрос зачем в юнити используются слабые ссылки. Это жутко неэффективно. Их же периодически удалять нужно, когда объекты умирают, иначе разрастется внутренняя структура данных.andreishe
07.10.2015 19:37Переменная
sa
существует до конца метода. В моем представлении, объект, на который она ссылается тоже должен был бы существовать до конца метода.
А в Unity ссылки слабые, потому что был использованExternallyControlledLifetimeManager
, другие life time менеджеры их не используют, но в том месте, контейнер короткоживущий, поэтому отдавать ему возможность управлять временем жизни объектов было не нужно.
a553
Видимо имелось ввиду
new WeakReference<A>(sa)
andreishe
Visual Studio при запуске подавляет какие-то оптимизации?
Конечно, спасибо за замечение, недоглядел.
a553
Да, для работы таких фич как Edit & Continue, Intermediate window, Locals и др.