- PVSM.RU - https://www.pvsm.ru -
Некоторые из читателей, которые когда-либо сталкивались с проблемой, описанной в названии статьи, наверняка оставались на работе до поздна и проводили много часов в отладчике. Для других это может быть не более чем игрой слов и жаргонными словечками. Однако, давайте отойдем от жаргона в сторону и раскроем понятия:
Ну и небольшой пример, чтобы показать, о чем идет речь:
using System;
class Test
{
static void Main()
{
Console.WriteLine(First.Beta);
}
}
class First
{
public static readonly int Alpha = 5;
public static readonly int Beta = Second.Gamma;
}
class Second
{
public static readonly int Gamma = First.Alpha;
}
Конечно же, если не смотреть в спецификацию, то любые ожидания как это будет работать не более чем предположения. Потому посмотрим в спецификацию (section 10.5.5.1 of the C# 4 version ):
The static field variable initializers of a class correspond to a sequence of assignments that are executed in the textual order in which they appear in the class declaration. If a static constructor (§10.12) exists in the class, execution of the static field initializers occurs immediately prior to executing that static constructor. Otherwise, the static field initializers are executed at an implementation-dependent time prior to the first use of a static field of that class.
Перевод:
Порядок инициализации статических полей класса соответствует порядку их расположения в исходном тексте класса. Если в классе присутствует статический конструктор, выполнение кода инициализации статических полей класса располагается прямо перед вызовом статического конструктора. В противном случае, если статического конструктора не существует, инициализация статических полей выполняется в месте, зависящем от конкретной реализации: это происходит перед первым использованием статического поля.
В дополнение к специикации языка, можно привести выдержку из спецификации CLI
, которая раскрывает больше деталей о инициализации типов, особенно о циклических зависимостях и многопоточной работе. Однако, я не буду этого делать, а только напишу пару коротких выдержек:
CLI
замечает, что тип A необходимо проинициализировать и при этом он же находится в процессе инициализации в том же самом потоке, CLI
продолжает работу таким образом, как будто тип A уже проинициализирован.Итак, что бы могло случиться по вашему мнению:
Test
: никаких дальнейших действий не требуетсяMain
First
(так как нам необходим First.Beta
)First.Alpha
в 5Second
(так как нам необходим Second.Gamma
)Second.Gamma
в First.Alpha
(5)Second
First.Beta
в Second.Gamma
(5)First
А здесь описано то, что происходит в реальности — на моем компьютере, с установленным .Net Framework 4.5 beta (я знаю, что инициализация типов была изменена в .NET 4 [1]. Я не знаю, были ли изменения в .Net 4.5, но я не буду утверждать, что это не возможно)
Test
: никаких дальнейших действий не требуетсяMain
First
(так как нам необходим First.Beta
)Second
(нам понадобится Second.Gamma
)Second.Gamma
в First.Alpha
(0)Second
First.Alpha
в 5First.Beta
в Second.Gamma
(0)First
Шаг (5) очень интересен. Мы знаем, что нам необходимо проинициализировать First
чтобы получить далее First.Alpha
. Однако этот поток уже инициализирует First
, потому мы пропускам инициализацию, надеясь что все в порядке. Однако в этой точке инициализации переменной еще не произошло. Упс…
(Существует одна тонкость, которая позволит избежать всех описанных проблем: использование ключевого слова const)
Надеюсь, мой пример прояснил для вас, почему использование циклических зависимостей при инициализации типов — дело которое вам сильно подпортит жизнь. Такие места очень трудно отлавливать и отлаживать. И по сути это классический Гайзенбаг [2]. В нашем примере важно понимать, что если так случится что программа проинициализирует первым Second
(например, чтобы получить доступ к другой переменной), то мы получим совершенно другой результат. И, на практике, можно получить такую ситуацию, когда запуск всех юнит-тестов приведет к тому что все они завалятся. Но если при этом запускать их по отдельности, они сработают (вполне возможно, кроме одного).
Один из путей, чтобы избегать подобных ситуаций — это отказаться от инициализации типов вообще. В большинстве случаев это как раз то что надо. Однако, обычно мы используем хорошо известные вещи. Такие как Encoding.Utf8
, или TimeZoneInfo.Utc
. Заметьте, что в обоих случаях это статические свойства, но мне кажется, они за собой несут использование статических полей. На первый взгляд кажется, что использование public static readonl
y
полей и public static get-only свойств одинаково, однако, как мы увиим позже, использование свойств дайт свои приемущества.
Моя библиотека Noda Time [3] имеет несколько похожих на наш, моментов. И все потому что многие типы этой библиотеки immutable, т.е. неизменяемые. Это имеет смысл, когда необходимо создать свою временную зону UTC
, или ISO calendar system
. Причем в дополнении к публично-видимым значениям, мы имеем множество статических переменных, используемых внутри библиотеки (в основном для задач кэширования). Все это делает библиотеку сложнее и трудной для тестирования, однако бенефиты производительности в данном случае очень и очень значительные.
К сожалению, огромное количество этих полей и свойств имеют циклические зависимости. Как я упоминал ранее, когда мы добавляем новое статическое поле, это может привести к самым различным поломкам в программе. Я могу исправить непосредственную причину, однако это оставит во мне чувство обеспокоенности о целостности кода. Ведь если я устранил одну проблему, это не дает никаких гарантй что нет других.
Один из главных вопросов при инициализаии типов — это чувствительность к порядку инициализации в комбинации с гарантией того что тип в пределах AppDomain
будет проинициализирован лишь однажды. Как я показал ранее, возможно, что при одном порядке инициализации это вызовет ошибку, а при каком-либо другом никакой ошибки не возникнет.
Для себя я решил что при разработке Noda Time, я хочу быть абсолютно уверенным что циклические зависимости не создадут для меня никаких проблем. Т.о. я хочу удостовериться что при инициализации типов не образуется циклов, причем независимо от того, в каком порядке они инициализируются. Если размышлять логически, мы можем определить циклическую зависимость, которая начинается с какого-то одного типа, начиная инициализацию с других типов, находящихся в этом же самом цикле. Я очень беспокоюсь чтобы не пропустить ни каких крайних случаев, и чтобы перебрать все варианты, которые возможны и не выпустить ничего из виду. Потому я применил метод грубой силы — полный перебор.
Вот наш грубый план:
Прошу заметить что у нас никогда не выйдет ситуации, когда мы сможем определить циклическую зависимость за одну загрузку домена приложения. Для этого необходимо обойти все типы и выявить циклы, анализируя результаты.
Описание того как работает код выйдет намного большим чем сам код и на самом деле он очень легок для понимания, так что я расположу его в конце статьи.
Это решение не является очень хорошим по нескольким причинам:
NCrunch
.И я уверен, что если я это исправлю, остальные системы юнит-тестирования все равно будут ломать мою программу.private static readonly int TypeInitializationChecking = NodaTime.Utility.TypeInitializationChecker.RecordInitializationStart();
И, учитывая все эти оговорки… Стоит ли это использовать? Однозначно, да. Эта методика помогла мне найти множество багов, которые были исправлены.
В прошлом, я «исправлял» порядок инициализации типов просто перемещая по коду поля. Циклы все еще существовали, однако я вычислил как сделать их безвредными. Я могу сказать что этот подход не масштабируемый и стоит гораздо больших усилий чем кажется. Код становится трудным… И если вы однажды получите цикл в более чем из двух зависимостей, это будет задачка для ума, как сделать его безопасным. На данный момент я использую очень простую технику чтобы осущиствить отложенную инициализацию статических переменных.
Так что вместо того чтобы искать что за static readonly field
создает вам циклическую зависимость, вы используете static readonly property
, которое возвращает internal static readonly field
, во вложенном, приватном статическом классе. Мы все еще имеем потоко-безопасную инициализацию с гарантией единичного вызова, однако nested
тип не будет проинициализирован, пока в этом не появится нужда.
Таким образом, вместо этого:
// Requires Bar to be initialized - if Bar also requires Foo to be
// initialized, we have a problem...
public static readonly Foo SimpleFoo = new Foo(Bar.Zero);
Мы напишем:
public static readonly Foo SimpleFoo { get { return Constants.SimpleFoo; } }
private static class Constants
{
private static readonly int TypeInitializationChecking = NodaTime.Utility.TypeInitializationChecker.RecordInitializationStart();
// This requires both Foo and Bar to be initialized, but that's okay
// so long as neither of them require Foo.Constants to be initialized.
// (The unit test would spot that.)
internal static readonly Foo SimpleFoo = new Foo(Bar.Zero);
}
На данный момент я не могу определить, включать статические конструкторы в этих классах чтобы добиться ленивой инициализации или нет. Если инициализатор типа Foo вызовет инициализатор типа Foo.Constants, мы вернемся к исходной точке. Но добавление статических конструкторов в каждых из nested классов звучит ужасно.
Я хочу вам сказать, что какая-то часть меня в реальности не любит писать тестирование кода или делать обходные пути и городить костыли. И определенно, стоит задуматься, можно ли на самом деле избавиться от инициализации типов (или его части), избегая хранения только в статических полях. Было бы совсем замечательно, если бы можно было найти все эти зависимости измегая запуска программы или юнит-тестов. Чтобы это можно было сделать при помощи статического анализатора. Когда у меня будет шанс, я попробую выяснить, может ли мне в этом помочь NDepend
.
Тем не менее, в то время как этот подход выглядит как некоторое хакерство, он все же лучше чем альтернатива — код, полных ошибок. И… Мне стыдно сказать, но я не думаю что в Noda Time
я зашел все циклические зависимости. Ее стоит опробовать на своем собственном коде — посмотрите, где у вас могут быть скрытые проблемы
internal sealed class TypeInitializationChecker : MarshalByRefObject
{
private static List<Dependency> dependencies = null;
private static readonly MethodInfo EntryMethod = typeof(TypeInitializationChecker).GetMethod("FindDependencies");
internal static int RecordInitializationStart()
{
if (dependencies == null)
{
return 0;
}
Type previousType = null;
foreach (var frame in new StackTrace().GetFrames())
{
var method = frame.GetMethod();
if (method == EntryMethod)
{
break;
}
var declaringType = method.DeclaringType;
if (method == declaringType.TypeInitializer)
{
if (previousType != null)
{
dependencies.Add(new Dependency(declaringType, previousType));
}
previousType = declaringType;
}
}
return 0;
}
/// <summary>
/// Invoked from the unit tests, this finds the dependency chain for a single type
/// by invoking its type initializer.
/// </summary>
public Dependency[] FindDependencies(string name)
{
dependencies = new List<Dependency>();
Type type = typeof(TypeInitializationChecker).Assembly.GetType(name, true);
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
return dependencies.ToArray();
}
/// <summary>
/// A simple from/to tuple, which can be marshaled across AppDomains.
/// </summary>
internal sealed class Dependency : MarshalByRefObject
{
public string From { get; private set; }
public string To { get; private set; }
internal Dependency(Type from, Type to)
{
From = from.FullName;
To = to.FullName;
}
}
}
[TestFixture]
public class TypeInitializationTest
{
[Test]
public void BuildInitializerLoops()
{
Assembly assembly = typeof(TypeInitializationChecker).Assembly;
var dependencies = new List<TypeInitializationChecker.Dependency>();
// Test each type in a new AppDomain - we want to see what happens where each type is initialized first.
// Note: Namespace prefix check is present to get this to survive in test runners which
// inject extra types. (Seen with JetBrains.Profiler.Core.Instrumentation.DataOnStack.)
foreach (var type in assembly.GetTypes().Where(t => t.FullName.StartsWith("NodaTime")))
{
// Note: this won't be enough to load the assembly in all test runners. In particular, it fails in
// NCrunch at the moment.
AppDomainSetup setup = new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory };
AppDomain domain = AppDomain.CreateDomain("InitializationTest" + type.Name, AppDomain.CurrentDomain.Evidence, setup);
var helper = (TypeInitializationChecker)domain.CreateInstanceAndUnwrap(assembly.FullName,
typeof(TypeInitializationChecker).FullName);
dependencies.AddRange(helper.FindDependencies(type.FullName));
}
var lookup = dependencies.ToLookup(d => d.From, d => d.To);
// This is less efficient than it might be, but I'm aiming for simplicity: starting at each type
// which has a dependency, can we make a cycle?
// See Tarjan's Algorithm in Wikipedia for ways this could be made more efficient.
// http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
foreach (var group in lookup)
{
Stack<string> path = new Stack<string>();
CheckForCycles(group.Key, path, lookup);
}
}
private static void CheckForCycles(string next, Stack<string> path, ILookup<string, string> dependencyLookup)
{
if (path.Contains(next))
{
Assert.Fail("Type initializer cycle: {0}-{1}", string.Join("-", path.Reverse().ToArray()), next);
}
path.Push(next);
foreach (var candidate in dependencyLookup[next].Distinct())
{
CheckForCycles(candidate, path, dependencyLookup);
}
path.Pop();
}
}
Автор: SunexDevelopment
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/7632
Ссылки в тексте:
[1] была изменена в .NET 4: https://www.pvsm.ruhttp://link:https://msmvps.com/blogs/jon_skeet/archive/2010/01/26/type-initialization-changes-in-net-4-0.aspx
[2] Гайзенбаг: http://ru.wikipedia.org/wiki/%D0%93%D0%B5%D0%B9%D0%B7%D0%B5%D0%BD%D0%B1%D0%B0%D0%B3
[3] Noda Time: http://code.google.com/p/noda-time/
Нажмите здесь для печати.