Комментарии 77
И какие штатные средства есть в C# для того что бы не потерять контроль над GC, как бывает иногда в java, которая через како-то время съедает всю доступную память и еще немного и бодро всё вешает.
ps: Для чего в реализации Dispose столько костылей?
pps: После прочтения стало еще больше непонятно.
ps: Для чего в реализации Dispose столько костылей?это со старых версий, нынче статья не особо актуальна, имхо.
Автор писал статью на хабре про него ( [DotNetBook] Реализация IDisposable: правильное использование ), и я с ним был сильно несогласен, привожу всё ту же ссылку на хороший разбор паттерна, если вам вдруг интересно — SO
public void Dispose()
{
if (isDisposed)
return;
resource2.Dispose();
resource1.Dispose();
isDisposed = true;
}
Del
Если мы откроем файл в конструкторе класса C#, и попытаемся закрыть его в деструкторе, получится плохо: деструктор вызывается лишь тогда, когда сборщик мусора убирает объект, то есть, непонятно когда, и может быть даже вообще никогда
Потому что в C# нет деструктора. Деструктор вызывается "вручную" при ручном освобождении памяти. Финализатор да, вызывается когда угодно. Терминология важна: она держит нас в рамках правил и понимания
Пускай у вас есть в классе поле (ну или свойство), которое имплементирует IDisposable. В этом случае ваш класс тоже должен имплементировать IDisposable, чтобы во время своего Dispose вызвать Dispose и для внутреннего объекта.
В общем случае, но не всегда.
Ресурсы делятся на управляемые (те, которые являются по сути .NET-объектами) и неуправляемые (они обычно являются хэндлами системы и хранятся в IntPtr, но в принципе могут быть любым объектом вне данного рантайма .NET, или даже просто чисто логической сущностью, наподобие права на показ нотификации пользователю).
Неуправляемый объект — тот, который создан и менеджится вне подсистемы .NET. Нотификация — какое-то запутывание
Но в целом написано хорошо, живенько. Противоречий более не заметил в том числе и с книгой :)
using System;
using System.Threading;
class Program {
static volatile int iter = 0;
static volatile int count = 0;
class A : IDisposable {
public A() {
//Thread.MemoryBarrier(); // (1)
++count;
Thread.Sleep(0);
}
public void Dispose() {
--count;
}
//~A() {} // (2)
}
static void Main(string[] args) {
for(int i=0;i<2;i++) {
var t=new Thread(()=>{
for(;;) using(var a=new A()) iter++;
});
t.Start();
for(var it=iter;it==iter;) {}
t.Abort();
t.Join();
}
Console.WriteLine("count={0}",count);
if (count > 0) Console.Error.WriteLine("something wrong");
else Console.WriteLine("looks fine");
}
}
output:
count=2
something wrong
Если объявить деструктор явно:
count=0
looks fine
Тем более с абортами треда, сомнительно, что это типичный кейс использования, обычно класс или thread-safe и там такое предусмотрено, или ровно наоборот, как у вас и получилось.
Вопрос интересный, так как логически ни финализатор (~A()), ни барьер не должны менять поведения в данном случая.
- Финализатор не вызывает Dispose. Фактически, Dispose и финализатор не связаны ничем, кроме рекомендаций и здравого смысла.
- MemoryBarrier не должен влиять, так как count объявлена как volatile и JIT сам добавит нужные инструкции.
Видимо, причина различия в поведении на практике в следующем. Когда происходит создание объекта с финализатором на управляемой куче, он должен быть поставлен в очередь финализации. Поскольку надо выполнить больше действий, то больше вероятность, что прерывание выполнения потока из-за Thread.Abort произойдёт не между выполнением конструктора и Dispose, а где-то ещё. Можно в этом убедиться, немного изменив цикл и вывод в вашем примере:
int i;
for (i = 0; i < 200 && count == 0; i++)
{
var t = new Thread(() =>
{
for (; ; ) using (var a = new A()) iter++;
});
t.Start();
for (var it = iter; it == iter;) { }
t.Abort();
t.Join();
}
Console.WriteLine("count={0}, iter={1}, i={2}", count, iter, i);
У меня в варианте с раскомментироваными строками (1) и (2) точно так же count становится не 0, просто это происходит не каждый раз, а спустя 3-20 итераций.
В C# есть механизм запрета на Abort
try {} finally { abort_safe_code(); }
Но в C# на это забили и using этот механизм не использует.
Вместо этого просто пишут в документации «The Thread.Abort method should be used with caution»
А барьер и деструктор просто смещают точку гонки, т.к. они довольно длительные операции.
Но в C# на это забили и using этот механизм не использует.
using (ResourceType resource = expression) statement эквивалентен следующему (если ResourceType — ссылочный тип и не dynamic):
{
ResourceType resource = expression;
try {
statement;
}
finally {
if (resource != null) ((IDisposable)resource).Dispose();
}
}
Это требование спецификации языка, и это выполняется, если выполнение дошло до finally, то Dispose выполняется полностью.
А барьер и деструктор просто смещают точку гонки, т.к. они довольно длительные операции.
Барьер — согласен. Финализатор же вызывается асинхронно сборщиком мусора, и смещение происходит не за счёт вызова как такового, а за счёт постановки объекта в очередь финализации. То есть это эффект не на уровне C# или IL, а на уровне среды выполнения, и ещё не факт, что будет проявляться во всех реализациях .NET.
{
ResourceType resource = null;
try {
try {} finally { resource=expression; }
statement;
}
finally {
if (resource != null) ((IDisposable)resource).Dispose();
}
}
expression
может вычисляться неопределённо долго.Типичный
using (var file = new StreamReader("\\NetworkShare...
Кстати если приложение запросило не существующий сетевой путь — это один из способов запретить закрытие приложения, ибо будет висеть до потери пульса.
Простите, я совершенно не из мира C# разработки. Однако, предложение поспорить пригласило меня прочитать статью.
Поясните, пожалуйста, как из предложения разделить все объекты по размерам следует, что делить надо именно на две независимые группы из маленьких и больших объектов? Почему этим надо управлять используя именно термины поколений? И, вопрос от дилетанта, почему бы не сделать несколько пулов под объекты разного размера и аллоцировать запись из наиболее подходящего пула? Карту использования элементов конкретного пула держать в его заголовке в битовой карте. Это, в принципе, несколько упрощает вопросы фрагментации памяти. Остаются только вопросы выделения памяти под большие объекты и под объекты с заранее неизвестным размером.
Может быть статью надо было начинать с того как устроено управление памятью, прежде чем рассказывать про сборщик мусора?
Поясните, пожалуйста, как из предложения разделить все объекты по размерам следует, что делить надо именно на две независимые группы из маленьких и больших объектов?
Если почитать литературу, было проведено множество исследований за многие годы (сборка мусора очень старая концепция) и два поколения это более менее оптимальный вариант деления для реальных нагрузок. Естественно люди пробовали самые разные способы деления, метрики, количество поколений и т.д. и т.п. Как всегда все упирается в компромиссы.
Почему этим надо управлять используя именно термины поколений?
Опять же, после многочисленных исследований реальных нагрузок образовалась закономерность, которую мы везде и слышим. Вероятность того, что объект умрет, тем выше, чем меньше возраст объекта. Чем объект старше, тем больше вероятность, что он и продолжит жить. Поэтому и появилась концепция молодного и старого поколения, к которым применяются сильно разные алгоритмы.
И, вопрос от дилетанта, почему бы не сделать несколько пулов под объекты разного размера и аллоцировать запись из наиболее подходящего пула?
Да полно таких аллокаторов. Только такие хитрости всегда требуют чем-то платить. Обычно это пропускная способность. Все эти пулы нужно держать в консистентном состоянии, особенно в условиях многопоточности, а с современными параллельными конкурентными сборщиками это означает сильную нагрузку на барьеры, которые и так далеко не дешевые. Да и сама аллокация станет дороже. В итоге сильно просядет пропускная способность, которая у конкурентных сборщиков и так не очень высокая. В итоге бывает проще сделать тупой абсолютно аллокатор, который делает процедуру выделения памяти практически бесплатной (у многих сборщиков оно именно так, выделение памяти намного дешевле типичного malloc), вплоть до тупого сдвига указателя. Естественно за это приходится платить в других местах, той же фрагментацией и необходимость компактить объекты.
Тоже самое касается большего количества поколений. Перенос объектов между поколениями процедура не бесплатная. Еще более неприятным является тот факт, что сборщику надо отслеживать ссылки на объекты сквозь поколения. Объект может лежать в одном поколении, а ссылаться на объект в другом. Я не помню уже деталей, но это создает очень большие проблемы. Естественно, чем больше поколений, тем больше ссылок подобных.
Почему этим надо управлять используя именно термины поколений?
В какое поколение попадёт объект зависит от его возраста.
От размера объекта зависит в какую кучу он попадет.
В .net есть как минимум две управляемых кучи(и несколько неуправляемых) — обычная куча(GC heap или ephemeral heap) и куча для больших объектов(large objects heap).
Может быть статью надо было начинать с того как устроено управление памятью, прежде чем рассказывать про сборщик мусора?Да не мешало бы:
42. Memory management
43. Implementing Tracing Garbage Collectors
44. Copying Garbage Collection
45. Generational Garbage Collection
Карту использования элементов конкретного пула держать в его заголовке в битовой карте
Это необязательно. Посмотрите как устроен SLUB аллокатор в ядре линукса.
Остаются только вопросы выделения памяти под большие объекты и под объекты с заранее неизвестным размером.
Юзермодные аллокаторы для больших размеров вызывают маппинг памяти средствами ОС, в обход основной логики. Ядерные аллокаторы поступают аналогично, запрашивая сразу пачку страниц.
Про "неищвесный размер" не понял. В момент вызова он обязан быть известным, иначе что аллоцируем ?
Обычно, они освобождаются в хаотичном порядке. И вообще не лучше ли представить обьекты как структуры примитивных величин (массивы, списки, хеш таблица) и определить весь функционал векторной обработки обьектов неким аналогом Numpy а вышеупомянутый подход годится для небольшой кучи непредсказуемых обьектов.
которая будет грузиться при доступе к одной переменной счётчика
Да, это одна из больших проблем подсчета ссылок. Да и не только, в сборке мусора тоже бывают добавляют какие-то метаданные в объекты и начинается экономия на page fault'ах. И даже если страница загружена, то словишь промах кэша. Собственно, поэтому счетчики можно упаковать в специальную структуру линейную отдельно от объектов. Вроде ObjC хранит счетчики в отдельной структуре. Swift, судя по комментариям в исходниках, хранит счетчик внутри объекта, но может при определенных условиях вынести его в отдельную таблицу.
А уж mark sweep сборщики мусора так точно все хранят в одном месте, а не помечают какие-то биты внутри каждого объекта. Вроде card table всяких.
да тогда лучше сразу в обьекте хранить.
кстати, ещё, как происходит наблюдение и удаление обьекта в случае когда счётчик достигает нуля? И кошмар — можно ли случайно написать так, чтобы что то нарушить, например ложно увеличить счётчик
как происходит наблюдение и удаление обьекта в случае когда счётчик достигает нуля? И кошмар — можно ли случайно написать так, чтобы что то нарушить, например ложно увеличить счётчик
Если речь про ObjC/Swift, то там такое невозможно. Операции со счетчиком компилятор вставляет автоматически. Поэтому и ARC, automatic reference counting
Мобильные девайсы не такие уж медленные, зато у них мало памяти. Тут, я думаю, основной профит от подсчета ссылок. В памяти просто не болтается ничего лишнего. Объект если больше не нужен, то сразу удаляется. mark sweep сборщики мало того, что не сразу все удаляют, так еще требуют обычно много свободного места в куче, чтобы комфортно работать.
Операции со счетчиком компилятор вставляет автоматически
куда и как вставляет? Предположим там что то вроде
if not obj.counter: obj.destructor()
В каких местах кода это прописывается, во всех где затрагивается обьект что ли?
А если знать место в памяти где счётчики или если они в самом обьекте, можно специально влесть туда и подменить, сделав шикарный лик памяти…
Так же есть weak ссылки, которые автоматически превращаются в null, когда объект удаляется. Так циклические ссылки разруливаются и нет проблемы с dangling pointer.
Это конечно не все, но все остальное это детали для частных хитрых случаев. В общем все работает именно так. Если дизассемблировать код на Objc или swift, то код будет буквально усеян вызовами retain/release. Как можно представить, это далеко не бесплатно. Особенно, если учесть, что все операции со ссылками потокобезопасны. retain/release сильно полагается на атомарные инструкции.
Работает это безусловно, объект и все его поля исчезают моментально. Вплоть до того, что объект не доживает даже до конца функции — компилятор может вставить release вызов ровно в том месте, с которого переменная более не используется.
А стоп, ты имеешь ввиду это разрешается в компайлтайме?
Тогда не настолько плачевно. А как разрулится рекурсия в компайлтайме с неизвестным количеством вложений?
Тогда не настолько плачевно
Ну как, если учесть, что retain/release вставляются пачками на каждый вызов функции, то получается накладно. Совсем не удивительно, что mark-sweep сборщик может работать намного быстрее.
А как разрулится рекурсия в компайлтайме с неизвестным количеством вложений?
А конкретнее? Не вижу проблемы.
А конкретнее? Не вижу проблемы.
в компайл тайме не известно сколько вложений в рекурсию. То есть компилятор не может вставить retain/release без проверки условия что счётчик упал до нуля. И после падения до нуля, обьект удаляется но далее в теле рекурсии опять происходит эта проверка, компилятор то не знает как и что, то есть там даже
if obj. counter == 0 and obj.not_destroyed(): object.destroy()
func foo(obj: ObjectType) {
swiftRetain(obj)
if obj.isDone() {
swiftRelease(obj)
return
}
swiftRelease(obj)
swiftRetain(obj)
foo(obj)
swiftRelease(obj)
}
let obj = ObjectType() //тут аллокация, счетчик изначально единице равен
swiftRetain(obj)
foo(obj)
swiftRelease(obj)
swiftRelease(obj) //объект больше не используется, можно удалить
Опять же, в чем проблема то? Поведение этого кода полностью детерминировано, компилятору все равно, рекурсия тут или нет.
if obj.isDone() {
swiftRelease(obj)
return
}
тут нельзя return если несколько обьектов, а так да этих retain/release будет больше чем собственно других операторов =)
if obj.isDone() {
swiftRelease(obj)
return
}
Тут, если что, вызов release нужен, потому что isDone вызвано и нам надо быть уверенным, что на момент вызова объект будет жив. Для этого retain был, который мы тут и компенсируем.
func foo(obj: ObjectType, obj2: ObjectType,) {
разве тут не:
if obj.isDone() and obj2.isDone() {
return
}
func foo(obj: ObjectType) {
if obj.isDone() {
return
}
foo(obj)
}
let obj = ObjectType()
foo(obj)
Это корректный Swift код. Программисту и думать не надо ни о каких ссылках.
func foo(obj: ObjectType, obj2: ObjectType,)
и вообще, если там массив из 10000 обьектов?
func foo(obj: ObjectType, obj2: ObjectType) {
if obj.isDone() {
return
}
obj2.mutate()
foo(obj: obj, obj2: obj2)
}
let obj = ObjectType()
let obj2 = ObjectType()
foo(obj: obj, obj2: obj2)
После вставок компилятора
func foo(obj: ObjectType, obj2: ObjectType) {
swiftRetain(obj)
if obj.isDone() {
swiftRelease(obj)
return
}
swiftRelease(obj)
swiftRetain(obj2)
obj2.mutate()
swiftRelease(obj2)
swiftRetain(obj)
swiftRetain(obj2)
foo(obj: obj, obj2: obj2)
swiftRelease(obj)
swiftRelease(obj2)
}
let obj = ObjectType()
let obj2 = ObjectType()
swiftRetain(obj)
swiftRetain(obj2)
foo(obj: obj, obj2: obj2)
swiftRelease(obj)
swiftRelease(obj2)
swiftRelease(obj)
swiftRelease(obj2)
(поправил синтаксис вызова функций, чтобы было действительно как в Swift)
и вообще, если там массив из 10000 обьектов?
Тут мы начинаем лезть в дебри языка. В Swift есть структуры, и они передаются по значению, т.е. копируются. Счетчика ссылок, соответственно, у них нет. Массив в swift реализован как раз как структура.
Но если мы говорим об Objective-C, то там есть реализация массива NSArray, который ссылочный тип. Количество объектов в нем не имеет значения. При передаче в функцию будет вставлен один вызов retain, чтобы увеличить счетчик ссылок экземпляра NSArray. Объектов внутри это никак не касается. Их счетчик ссылок это дело самого объекта NSArray — когда внутрь него кладешь объект, то он делает ему retain один раз и все. При удалении объекта из массива делаем один вызов release.
имхо, разве корректно
if obj.isDone() {
swiftRelease(obj)
return
}
если для foo(obj: obj, obj2: obj2) может быть нормально что один обьект уже null (не вызовет NPE)
а если foo получает массив, для всех значений каждый раз вызывается retain/release, но для метода нормально если некоторые из значений уже null?
а если foo получает массив, для всех значений каждый раз вызывается retain/release
Если речь об NSArray, т.е. ссылочном типе, то нет, при передаче его экземпляра в функцию у его элементов retain/release не будет вызываться. Это не имеет смысла и пустая трата ресурсов.
Если речь о swift массивах, которые структуры, то работать будет явно иначе. При передаче в функцию будет создана копия массива, что скорее всего приведет к инкременту счетчика ссылок каждого элемента в нем, если эти элементы ссылочные. Я это, если честно, не проверял, но по логике должно быть именно так. Ведь у нас теперь два массива и оба имеют ссылки на одни и теже элементы. Счетчик у всех как минимум 2 должен быть.
И про копии это на самом деле семантика языка. Компилятор волен делать, что ему вздумается. Если копия не имеет смысла, то код будет оптимизирован. В godbolt это я похоже смог увидеть. У swift иммутабельность на уровне языка поддерживается, поэтому у компилятора много свободы для оптимизации.
При передаче в функцию будет создана копия массива, что скорее всего приведет к инкременту счетчика ссылок каждого элемента в нем, если эти элементы ссылочные.
запомнил — не писать тяжёлый рекурсивный алгоритм типа сортировки на Swift. Или вообще не писать слишком много требовательного… функциями
запомнил — не писать тяжёлый рекурсивный алгоритм типа сортировки на Swift. Или вообще не писать слишком много требовательного… функциями
Запоминать это не надо, это неправильное предположение. Стоит все таки про язык еще почитать. Swift очень навороченный, но и очень интересный язык. Если речь о сортировке, то она, мало того, что уже реализована для встроенного типа массивов, а он тут дженерики использует. Так еще пользуется особенностью — специальный модификатор mutating у методов структур говорит о том, что метод будет структуру менять, а значит this/self должен быть ссылочным и мутабельным. Никаких копий, никаких проблем. По-умолчанию, методы структур не могут менять себя, для них this/self иммутабельный.
Если хочется свою сортировку написать, то для этого есть extension'ы
Если речь о сортировке, то она, мало того, что уже реализована для встроенного типа массивов, а он тут дженерики использует.
ну я то не сомневаюсь что операции высокой производительности реализованы нативно, иначе б так вообще ничего не фурычило.
Запоминать это не надо, это неправильное предположение.
ну представь себе какой нибудь qsort. Рекурсивный. Каждый раз с проверками всех счётчиков.
ну представь себе какой нибудь qsort. Рекурсивный. Каждый раз с проверками всех счётчиков.
Почему? В swift есть inout модификатор аргумента, что заставит передать массив по ссылке. Никаких копирований, а значит и элементы с их счетчиками трогать не надо. Вот тебе и быстрый qsort.
ну я то не сомневаюсь что операции низкого уровня реализованы нативно, иначе б так вообще ничего не фурычило.
И реализовано оно все на самом Swift. Внутри timsort и как раз использует inout модификаторы + небезопасные указатели. Если надо, swift позволяет опуститься довольно низко.
В ObjC не похоже, чтобы анализ какой-то делался. Все предельно прямолинейно из того, что я видел. Но там и язык очень динамический и непредсказуемый. У компилятора вообще нет никакой свободы что-то делать с кодом, который оперирует объектами.
В конечном итоге, главное тут семантика для программиста. Что там под капотом дело уже десятое. Если у тебя есть strong ссылка, то ты уверен, что объект у тебя под ногами не исчезнет.
Ну и не знаю, при чем тут цитата про деструктор объекта. Объект владеет своими полями, т.е. счетчик у них как минимум равен 1, пока родитель жив. Они никогда не удалятся преждевременно.
При счетчике ссылок «платится» за каждую передачу объекта (присваивание, передачу аргументом, и т.п.), тогда как при GC «платится» когда объект переживает сборку мусора (плата, правда, побольше, и зависит от того сколько внутри объекта указателей)
Объекты присваиваются и передаются аргументом намного чаще чем переживают GC.
Плюс, счетчик ссылок очень медленный в случае наличия больше чем одного потока.
А можно объяснить следующую ситуацию: вот я создал объект и передал ссылку на него трем разным потокам.- разве GC не нужен атомарный счетчик для определения что объект все еще используется?
Честно говоря, статья не очень помогла в этом вопросе.
Да, детерминированность это слабое место GC, но на практике оно не имеет значения. Для всех ресурсов, для которых это важно, вроде файлов, сокетов, есть отдельные вызовы Close/Disconnect или конструкции uses/IDisposable как в C#. Если речь просто о памяти под объекты, то не важно вообще, когда оно там освободится. Иначе бы GC языки не правили миром.
Если пишете на С++, то тем более должно быть понятно, что ссылка на объект это всего лишь адрес в памяти. Что в С++, что C#, хоть где. Сборщику на mark фазе нужно эти адреса и найти. Смотрит он на регистры процессора, стек, поля объектов — это все просто числа в памяти. Среди них надо найти адреса и пометить у себя, что по такому адресу объект жив. Самим объектам знать ничего про это не нужно. И количество ссылок тоже считать не нужно. Сборщик мусора строит граф живых объектов, начиная от root set. В этом как раз отличие от подсчета ссылок, где об иерархии объектов ничего неизвестно. Поэтому и циклы не разрулить никак, что даже С++ касается с его shared_ptr. Если что-то в графе живых объектов отсутствует, то это смело можно удалять. Сборщикам не надо даже знать, какие там объекты не живые. Многие просто чистят память большими блоками и пофиг что там было. Проблемы создают разве что финализаторы, которые надо заранее вызвать.
А почитать есть одна замечательная книга. Можно сказать энциклопедия по теме сборки мусора во всех ее формах и проявлениях The Garbage Collection Handbook: The Art of Automatic Memory Management, Richard Jones, Antony Hosking, Eliot Moss www.amazon.com/dp/B01MRDA69B
объем памяти — скорость выделения — скорость освобождения
Все несколько сложнее. Это метрики аллокатора, который является всего лишь частью системы сборки мусора. Конкурентные сборщики вставляют еще барьеры так называемые (компилятор это делает) — при чтении и/или изменении какой-либо ссылки выполняет маленький кусок кода. Это нужно, чтобы параллельно работающий юзерский код и сборщик мусора не конфликтовали между собой и не нужно было stop the world делать. От этого падает throughput — ресурсы процессора банально тратятся на эти барьеры. Зато выигрывает еще одна метрика латентность — ведь если stop the world нет, то и пауз нет, которые так любят припоминать сборщикам мусора. Современные сборщик имеют жалкие микросекундные паузы, т.к. практически всю работу они делают параллельно с пользовательским кодом. В книге выше, кстати, в том числе и о таких штуках есть. Go, Java (два новых экспериментальных сборщика), C# наверное тоже — все они такое умеют нынче.
Хм, а если по ссылке всего лишь число, то как рекурсивно искать всего его ссылки на другие объекты (числа)? Нужно же понимать структуру этого объекта, смещения полей…
Кстати, может быть помните — недавно была статья на Хабре, где бы странная бага в V8, и выяснилось, что сборщик мусора принял за объект какие-то данные. Не могу ее найти...
V8 скорее всего precise сборщик имеет и для него такая бага действительно проблема. Есть conservative сборщики, которые могут использовать другие упомянутые мной механизмы. Они могут принять за объект то, что таковым не является. Но это не особо страшно, если так подумать. Просто будет в памяти мусор лежать и не освобождаться. Если писать сборщик для какого-нить С++, где помощи от компилятора никакой, указатели не выравнены, все куда зря и как хочет может указывать, то там только conservative сборщик и получится сделать.
Хм, а если по ссылке всего лишь числоВ языках типа java/c# такого быть не может. Число будет завёрнуто в объект System.Int32 — наследника System.Object, поэтому тут будут все обычные заголовки объекта.
Но сути не меняет. Насколько я знаю, синтаксически нельзя изобразить в объекте ссылку на Value Type. Можно ссылку на Object, и тут будет боксинг, и ссылка будет на объект с заголовком, а не на просто число.
Проблему детерминированности зато решает IDisposable паттерн в C#. Чрезвычайно удобная и полезная штука.
Только тогда запускается цикл сборки, который в современных сборщиках будет работать параллельно с кодом какое-то время.Можно еще вопрос: а как она производится параллельно, если, например, уплотняется память и все ссылки начинают плыть? Что-то тут противоречит логике.
Проблему детерминированности зато решает IDisposable паттерн в C#.Разговор об автоматическом вызове, а IDisposable это какой-то эразц, уж простите. Я вообще не пойму как строить архитектуру с таким подходом. Вот например был у меня класс Object и в нем не было управляемых ресурсов. Пришли месяцы, проект разросся и вдруг этому классу понадобился IDisposable. Как такое решается на практике: везде где используется Object вставляется using и везде где используются классы которые используют Object и т.д., или как?
Можно еще вопрос: а как она производится параллельно, если, например, уплотняется память и все ссылки начинают плыть? Что-то тут противоречит логике.
Для этого барьеры (если что, барьер в GC и барьер в многопоточном коде это совершенно разные вещи), в том числе, и нужны, чтобы ссылки обновлялись корректно в условиях перемещения объектов. Одна из идей это резервировать в начале объекта поле для forwarding pointer, который будет указывать на новое местоположение объекта, если он перемещен. Там много хитростей и тут лучше податься в упомянутую книжку или посмотреть презентации про конкретные реализации. Про shenandoah в Java есть хороший доклад на русском даже. Он как раз умеет параллельно с пользовательским кодом и помечать, и перемещать объекты.
Разговор об автоматическом вызове, а IDisposable это какой-то эразц, уж простите. Я вообще не пойму как строить архитектуру с таким подходом. Вот например был у меня класс Object и в нем не было управляемых ресурсов. Пришли месяцы, проект разросся и вдруг этому классу понадобился IDisposable. Как такое решается на практике: везде где используется Object вставляется using и везде где используются классы которые используют Object и т.д., или как?
Пример явно притянутый за уши. Не то что на практике, мне такая проблема даже в голову не приходила. Не говоря о том, что IDisposable нужен довольно редко. Я бы даже сказал, что это не техническая проблема, а проблема организации самого процесса разработки.
А так, да. Везде, где используется класс, нужно или вставить using, если это возможно, либо реализовать у использующего класса IDisposable. Рекомендация такова, что если у класса есть поле, реализующее IDisposable, то твой класс тоже должен реализовать этот интерфейс, чтобы дернуть Dispose у полей. Но это всего лишь рекомендация, т.к. сборщик мусора в любом случае все почистит. Главный вопрос, важно ли это делать конкретно здесь и сейчас, что встречается крайне редко.
Пример явно притянутый за уши.Ну уж ладно вам, ну какие уши. Вот мы и имеем ситуацию когда управляемые языки съедают все ресурсы системы за милую душу.
Не то что на практике, мне такая проблема даже в голову не приходила.Это говорит лишь о сложности решаемых проблем.
Не говоря о том, что IDisposable нужен довольно редко. Я бы даже сказал, что это не техническая проблема, а проблема организации самого процесса разработки.Понятия не имею, т.к. очень мало приходилось программировать на управляемых языках.
А так, да. Везде, где используется класс, нужно или вставить using, если это возможно, либо реализовать у использующего класса IDisposable.Ну это вот и является большим недостатком для той ниши применения где используется С++, где все ресурсы нужно беречь и освобождать их как можно быстрее, как только они перестали быть нужны. Потом using решает только часть проблем, которые помогает решить автоматический детерминированный вызов деструктора.
Есть еще куча парных вызовов: Open/Close, Lock/Unlock, Acquire/Release, и т.д. которые также автоматизируются в случае исключения или ошибки.
В С++ я один раз проектирую класс. В какое окружение не помести этот класс, в случае чего он гарантированно очистит всю свою память и ресурсы. Если я меняю класс, то должен контролировать только его код. Мне не нужно бегать по коду и в зависимости от изменения логики руками вставлять/убирать using и finally.
Вот мы и имеем ситуацию когда управляемые языки съедают все ресурсы системы за милую душу.
Язык здесь практически не при чем. Сборщик мусора — может быть, он сам по себе требует больше памяти чисто для комфортной работы себе. Хром вон на С++ написан, а все ресурсы тоже съедает. Так уж получилось, что именно на управляемых языках, а не с++, пишут огромные энтерпрайзы с фреймворками на миллионы строк. Сложно в таких условиях использование памяти контролировать. С++ тут тоже будет жрать непомерно.
Это говорит лишь о сложности решаемых проблем.
Какой сложности? IDisposable паттерн это базовое понятие языка. Он встречается повсеместно в .Net. Такой проблемы просто нет на практике, вот и все.
Ну это вот и является большим недостатком для той ниши применения где используется С++, где все ресурсы нужно беречь и освобождать их как можно быстрее, как только они перестали быть нужны
Эта ниша чрезвычайно мала даже для С++ и там уже больше С используют.
Есть еще куча парных вызовов: Open/Close, Lock/Unlock, Acquire/Release, и т.д. которые также автоматизируются в случае исключения или ошибки
Все это намного проще решается в C# в силу того, что сам язык намного проще и предсказуемый. Есть using, lock, async/await, finally и еще куча всего. Опять же, все эти проблемы притянуты за уши и на практике чрезвычайно редко хотя как-то заставляют о них задумываться, хоть я и постоянно используют сокеты, файлы, таймеры и все то, что IDisposable требует и не терпит отсрочивания освобождения ресурсов.
В С++ я один раз проектирую класс.
С++ ничем выгодно не отличается в этом плане.
В какое окружение не помести этот класс, в случае чего он гарантированно очистит всю свою память и ресурсы
Я более чем уверен, что очень много кода в мире совершенно не exception-safe на С++. В том числе такой код скорее всего есть и у вас. И это реальная проблема, про которую много написано. IDisposable же просто есть, с ним все просто и понятно.
Если я меняю класс, то должен контролировать только его код.
Язык здесь не при чем.
Мне не нужно бегать по коду и в зависимости от изменения логики руками вставлять/убирать using и finally.
Мне тоже не нужно, потому что ваш пример надуманный.
На спор: прочитав до конца, вы поймёте, как и почему именно так работает GC