#1. Мантра про 3 поколения в любой ситуации
Это скорее неточность, чем заблуждение. Вопрос про «сборщик мусора в C#» для разработчика стал классикой и уже мало кто не начнет на него бойко отвечать про концепцию поколений. Однако, почему-то, мало кто обращает внимание на то, что великий и ужасный сборщик мусора — часть рантайма. Соответственно, я бы дал понять, что не пальцем пихан, и спросил бы про какую среду исполнения идет речь. По запросу «сборщик мусора в c#» в интернетах можно найти более, чем много похожих сведений. Однако мало кто упоминает, что данная информация относится к CLR/CoreCLR (как правило). Но не стоит забывать и про Mono, легковесный, гибкий и встраиваемый рантайм, который занял свою нишу в мобильной разработке (Unity, Xamarin) и используется в Blazor. И для соответствующих разработчиков я бы посоветовал поинтересоваться подробностями устройства сборщика в Mono. Например, по запросу «mono garbage collector generations», можно увидеть, что поколения всего два — nursery и old generation (в новеньком и модном сборщике мусора — SGen).
#2. Мантра про 2 стадии сборки мусора в любой ситуации
Еще не так давно исходники сборщика мусора были скрыты от всех. Однако интерес к внутреннему устройству платформы был всегда. Поэтому информация извлекалась разными путями. И некоторые неточности при реверс-инжиниринге сборщика привели к мифу о том, что сборщик работает в 2 стадии: маркировка и чистка. Или и того хуже, 3 стадии — маркировка, чистка, сжатие.
Однако все изменилось,
На этапе маркировки выясняется, какие объекты не стоит собирать сборщику.
На этапе планирования производятся расчет различных показателей текущего состояния памяти и сбор данных, необходимых на этапе чистки. Благодаря информации, полученной на этом этапе, выносится решение о необходимости компактинга (дефрагментации), там же просчитывается, на сколько необходимо двигать объекты и т.д.
И на этапе чистки в зависимости от необходимости компактинга может производится обновление ссылок и компактинг или удаление без перемещений.
#3. Выделение памяти на куче так же быстро, как и на стеке
Опять же, скорее неточность, чем абсолютная неправда. В общем случае, конечно, разница в скорости выделения памяти минимальна. Действительно, в лучшем случае, при bump pointer allocation, выделение памяти — лишь сдвиг указателя, как и на стеке. Однако на выделение памяти в куче могут повлиять такие факторы, как присваивание нового объекта в поле старого (что затронет write barrier, обновляющий card table — механизм, позволяющий отслеживать ссылки из старшего поколения в младшее), наличие финализатора(необходимо добавить тип в соответствующую очередь) и др. Так же, возможно, объект будет записан в одну из свободных дыр в куче (после сборки без дефрагментации). А нахождение такой дыры происходить хоть и быстро, но, очевидно, медленнее, чем простой сдвиг указателя. Ну и разумеется каждый созданный объект приближает очередную сборку мусора. И на очередной процедуре выделения памяти она может случится. Что, естественно, займет некоторое время.
#4. Определение ссылочного, значимого типов и упаковки через понятия стека и кучи
Прямо классика, которая, к счастью, уже не так часто встречается.
Ссылочный тип располагается в куче. Значимый на стеке. Наверняка многие слышали эти определения очень часто. Но мало того, что это лишь частичная правда, так и определять понятия через протекшую абстракцию — не лучшая идея. За всеми определениями предлагаю обращаться к стандарту CLI — ECMA 335. Для начала стоит уточнить, что типы описывают значения. Так, ссылочный тип определяется следующим образом — значение, описываемое ссылочным типом (ссылка) указывает на расположение другого значения. Для значимого типа значение им описываемое является автономным(самосодержащим). Про то, где располагаются те или иные типы ни слова. Это является протекшей абстракцией, которую все же следует знать.
Значимый тип может располагаться:
- В динамической памяти (куче), если он является частью объекта, расположенного в куче, или в случае упаковки;
- На стеке, если он является локальной переменной/аргументом/возвращаемым значением метода;
- В регистрах, если то позволяет размер значимого типа и другие условия.
Ссылочный тип, а именно, значение на которое указывает ссылка, на текущий момент располагается в куче.
Сама же ссылка может располагаться там же, где и значимый тип.
Упаковка также не определяется через места хранения. Рассмотрим краткий пример.
public struct MyStruct
{
public int justField;
}
public class MyClass
{
public MyStruct justStruct;
}
public static void Main()
{
MyClass instance = new MyClass();
object boxed = instance.justStruct;
}
И соответственный IL код для метода Main
1: nop
2: newobj instance void C/MyClass::.ctor()
3: stloc.0
4: ldloc.0
5: ldfld valuetype C/MyStruct C/MyClass::justStruct
6: box C/MyStruct
7: stloc.1
8: ret
Так как значимый тип является частью ссылочного очевидно, что располагаться он будет в куче. И шестая строка дает ясно понять, что мы имеем дело с упаковкой. Соответственно, типичное определение «копирование из стека в кучу» дает сбой.
Чтобы определить, что есть упаковка, для начала стоит сказать, что для каждого значимого типа CTS(common type system) определяет ссылочный тип, который называется упакованным типом. Так, упаковка — операция над значимым типом, создающая значение соответствующего упакованного типа, содержащего побитовую копию оригинального значения.
#4. События — отдельный механизм
События существуют с первой версии языка и вопросы о них встречаются гораздо чаще, чем сами события. Однако понимать и знать, что это такое, стоит, ведь этот механизм позволяет писать очень слабо связанный код, что иногда бывает полезно.
К сожалению, зачастую под событием понимается некий отдельный инструмент, тип, механизм. Особенно этому способствует тип из BCL EventHandler, имя которого наталкивает на мысли о том, что это что-то отдельное.
Определение события стоит начать с определения свойств. Я уже давно провел для себя такую аналогию, а недавно увидел, что она проведена и в спецификации CLI.
Свойство определяет именованное значение и методы, которые обращаются к нему. Звучит довольно очевидно. Переходим к событиям. CTS поддерживает события так же, как и свойства, НО методы для доступа отличаются и включают методы для подписки и отписки от события. Из спецификации языка C# — класс определяет событие… что напоминает объявление поля с добавлением ключевого слова event. Тип этого объявления должен быть типом делегата. Спасибо стандарту CLI за определения.
Итак, это означает, что событие — ничто иное, как делегат, который выставляет наружу лишь часть функциональности делегатов — добавление очередного делегата в список для выполнения, удаление из этого списка. Внутри же класса событие ничем не отличается от простого поля типа делегата.
#5. Управляемые и неуправляемые ресурсы. Финализаторы и IDisposable
Абсолютная путаница существует при работе с этими ресурсами. Этому во многом способствует интернет с тысячей статей о правильной реализации паттерна Dispose. Собственно, в этом паттерне ничего криминального нет — модифицированный шаблонный метод под конкретный случай. Но вопрос в другом — нужен ли он вообще. Почему-то, у части людей возникает непреодолимое желание реализовать финализатор на каждый чих. Скорее всего причиной этого является не полное понимание, что есть «неуправляемый ресурс». И строчки про то, что в финализаторах, как правило, освобождаются неуправляемые ресурсы из-за этого неполного понимания проходят мимо и не остаются в голове.
Неуправляемый ресурс — ресурс, который не является управляемым (как бы это странно ни было). А управляемый ресурс, в свою очередь, тот, который выделяется и высвобождается CLI автоматически через процесс который называется сборка мусора. Это определение я нагло содрал из стандарта CLI. Но если попытаться объяснить проще, то неуправляемые ресурсы — те, про которые не знает сборщик мусора. (Строго говоря мы можем давать сборщику немного информации про такие ресурсы с помощью GC.AddMemoryPressure и GC.RemoveMemoryPressure, это может оказать влияние на внутренние самотьюнинги сборщика). Соответственно, он не сможет сам позаботиться об их освобождении и поэтому мы должны сделать это за него. И подходов к этому может быть много. И чтобы код не пестрил от разнообразия воображения разработчиков, используются 2 общепринятых подхода.
- Интерфейс IDisposable (и его асинхронная версия IAsyncDisposable). Мониторится всеми анализаторами кода, так что забыть про его вызов сложно. Предоставляет единственный метод — Dispose. И поддержку компилятора — оператор using. Отличный кандидат на тело метода Dispose — вызов аналогичного метода одного из полей класса или освобождение неуправляемого ресурса. Вызывается явно пользователем класса. Наличие данного интерфейса у класса подразумевает, что по окончании работы с экземпляром, нужно вызвать этот метод.
- Финализатор. По своей сути является страховкой. Вызывается неявно, в неопределенное время, во время сборки мусора. Замедляет выделение памяти, работу сборщика мусора, продлевает время жизни объектов минимум до следующей сборки, а то и дольше, но зато вызывается сам, даже если его никто не вызывал. Из-за своей недетерминированной природы, в нем должны освобождаться только неуправляемые ресурсы. Также можно встретить примеры, в которых финализатор применялся для воскрешения(resurrection) объекта и организации пула объектов таким образом. Однако такая имплементация пула объектов — однозначно плохая идея. Как и пытаться логировать, кидать исключения, обращаться к базе и тысячи подобных действий.
И можно легко представить себе ситуацию при написании критической к производительности библиотеки, которая внутри использует неуправляемые ресурсы, что она обходится просто грамотным обращением с этим ресурсом, освобождая память аккуратно вручную. При написании таких высокопроизводительных библиотек, ООП, поддерживаемость и иже с ним уходит на второй план.
И вопреки утверждению, что Dispose нарушает концепцию, в которой CLR сделает все за нас, заставляет делать что-то самим, о чем-то помнить и тд, скажу следующее. При работе с неуправляемыми ресурсами, надо быть готовым, что они никем, кроме вас, не управляются. И вообще, ситуации, при которых данные ресурсы будут использоваться в ентерпрайсе, почти не встречаются. И в большинстве случаев можно обойтись замечательными классами обертками, вроде SafeHandle, который обеспечивает критическую финализацию ресурсов, предотвращая их преждевременную сборку.
Если же в вашем приложении по тем или иным причинам много ресурсов, требующих дополнительных действий для освобождения, то стоит взглянуть на отличный паттер компании JetBrains — Lifetime. Но не стоит его применять при виде первого же IDisposable объекта.
#6. Стек потока, стек вызовов, вычислительный стек и Stack <T>
Последний пункт добавил смеха ради, не думаю, что есть те, кто относит последнее к предыдущим двум. Однако путаницы с тем, что такое стек потока, стек вызовов и вычислительный стек довольно много.
Стек вызовов — структура данных, а именно стек, для хранения адресов возврата, для возвращения из функций. Стек вызовов — понятие больше логическое. Оно не регламентирует где и как должна храниться информация для возврата. Получается, что стек вызовов — самый обычный и родной нам стек [т.е.
Stack<T>:trollface:]. В нем же хранятся локальные переменные, через него передаются параметры и в нем же сохраняются адреса возврата при вызове инструкции CALL и прерываний, которые впоследствии используются инструкцией RET для возврата из функции/прерывания. Идем дальше. Одним из основных приколов потока является указатель на инструкцию, которая выполняется далее. Поток поочереди выполняет инструкции, объединяющиеся в функции. Соответственно у каждого потока есть стек вызовов. Таким образом получается, что стек потока и есть стек вызовов. То есть стек вызовов данного потока. Вообще, он также упоминается и под другими именами: программный стек, машинный стек.
Подробно рассматривался в предыдущей статье.
Также, определение стек вызовов используется для обозначения цепочки вызовов конкретных методов в каком-либо языке.
Вычислительный стек (evaluation stack). Как известно, код C# компилируется в IL код, который входит в состав результирующих DLL (в самом общем случае). И как раз в основе среды выполнения, поглощающей наши DLL и выполняющей IL код лежит стек-машина. Почти все IL инструкции оперируют неким стеком. Например, ldloc загружает локальную переменную под определенным индексом на стек. Здесь под стеком понимается некий виртуальный стек, ведь в итоге эта переменная может с высокой вероятностью оказаться и в регистрах. Арифметические, логические и др IL инструкции оперируют с переменными со стека и кладут результат туда же. То бишь вычисления производятся через этот стек. Таким образом получается, что вычислительных стек — абстракция в рантайме. К слову, многие виртуальные машины основаны на стек-машине.
#7. Больше потоков — быстрее код
Интуитивно кажется, что обрабатывать данные параллельно будет быстрее, чем поочередно. Поэтому вооружившись знаниями о работе с потоками, многие пытаются запараллелить любой цикл и вычисление. Почти все уже знают про оверхед, который вносит создание потока, поэтому лихо используют потоки из ThreadPool и Task. Но оверхед создания потока — далеко не конец. Здесь мы имеем дело с еще одной протекшей абстракцией, механизмом, который используется процессором для повышения производительности — кеш. И как часто это бывает, кеш является обоюдоострым клинком. С одной стороны он значительно ускоряет работу при последовательном доступе к данным из одного потока. Но с другой стороны, при работе нескольких потоков, даже без необходимости их синхронизации, кеш не только не помогает, но еще и замедляет работу. Дополнительное время тратится на инвалидацию кэша, т.е. поддержание актуальных данных. И не стоит недооценивать эту проблему, которая по началу кажется пустяком. Эффективный с точки зрения кэша алгоритм будет выполняться одним потоком быстрее, чем многопоточный, в котором кэш используется неэффективно.
Также пытаться работать с диском из многих потоков — самоубийство. Диск и без того является тормозящим фактором многих программ, которые с ним работают. Если пытаться работать с ним из многих потоков, то надо забыть о быстродействии.
За всеми определениями рекомендую обращаться сюда:
C# Language Specification — ECMA-334
Просто хорошие источники:
Konrad Kokosa — Pro .NET Memory Management
CLI specification — ECMA-335
CoreCLR developers about runtime — Book Of The Runtime
От Станислава Сидристого про финализацию и прочее — .NET Platform Architecture
Комментарии (56)
dimaaan
12.08.2019 21:33+6Среди зеленых юниоров распространено мракобесие по поводу очевидных (только что прочитавшему книгу про память) вещей
Зачем вообще спрашивать джуна про количество поколений CG?
Naglec
12.08.2019 21:43+7Потому что собеседующие не знают что спрашивать
UnclShura
13.08.2019 15:42Такие вопросы задают чтобы выявить хоть кого-то, кто интересуется языком да и программированием вообще. Не важно, что отверят — главное что-б не «а я учииил...» и глаза такие большие большие… у дядьки весом под центнер и седой бородой.
Naglec
13.08.2019 17:57Нет, это вопрос из списка «10 банальных вопросов для собеседования программиста на языке C#». Это совсем не про любознательность и интерес к программированию.
Чтобы определить интерес к программированию надо смотреть как и что человек рассказывает про прошлую работу/опыт/проекты/дипломы и как он рассказывает про то, чем хотел бы заниматься. Для этого интервьюиру нужно иметь богатый опыт разработки и кругозор в технологиях, коим большая часть 23-летних тимлидов, разумеется, не обладает.UnclShura
13.08.2019 19:31А вот и нет. Врать про прошлые проекты, где в реальности на совещаниях сидел да одну фичу годами точил может (почти) каждый. А спросишь его элементарный синтаксис — всё поплыл. Вы удивитесь, но примерно 80% на вопрос о garbage collector или GetHashCode не отвечают вообще ничего.
Кровавый энтерпрайз за дофига зарплаты.
Naglec
13.08.2019 19:37Я не очень понимаю как можно врать про нюансы и сложности использования тех или иных технологических/архитектурных решений на прошлых проектах. Если человек легко может рассказать что, почему и как было использовано, то все ок.
AndreyDmitriev
12.08.2019 22:02+2Ну как сказать… Я вот почти сразу на грабли наступил. Дело было так. Детишки попросили соорудить игрушечный камин для кукольного домика, чтоб огонь горел как настоящий. У меня под рукой была платка Raspberry Pi да трехдюймовый мониторчик. Делов на вечер, но дело там осложнялось тем, что мониторчик был на шине SPI со специальным API и библиотекой. Короче, для упражнения я написал программку на C# и она там под mono крутилась, показывая в цикле фреймы из видео. Где-то кадров 15 в секунду я получил. Все работало, но падало через некоторое время с переполнением памяти, хотя утечек там на вид не было. Там вся программа-то полсотни строк была от силы. И решилось, как ни странно, вставкой небольшой паузы в несколько миллисекунд в цикл. Судя по всему, сборщик мусора банально не получал процессорного времени.
Dima_Sharihin
13.08.2019 09:46Если мониторчик типа ili9341, то для него есть fbtft и больше 15 кадров в секунду с него получить проблематично
Programmierus
12.08.2019 23:14+1Все гуд, только Вам очень повезло с зелеными джунами — те которые попадались мне (а последнее время ещё чаще стали попадаться) на подобные темы в таком ракурсе точно общаться не готовы… Справедливости ради я от них подобных нюансов, наверное, и не жду даже…
DarkGenius
13.08.2019 07:52В статье вопросы не для джуна. А детали имплементации сборщика мусора в разных рантаймах могут не знать и синьоры.
MonkAlex
13.08.2019 00:10Ни один из рассмотренных вопросов начинающим нафиг не сдался.
А вы точно специалист?
Varim
13.08.2019 00:37Стек вызовов — структура данных, а именно стек, для хранения адресов возврата, для возвращения из функций. Стек вызовов — понятие больше логическое. Оно не регламентирует где и как должна храниться информация для возврата. Получается, что стек вызовов — самый обычный и родной нам стек т.е. Stack T
Каждое предложение, на мой взгляд, ложное.
Стек поддерживаемый CPU хранит еще значения регистров до изменения этих регистров новой функцией.
Может до JIT оно больше логическое, но вряд ли, но после JIT наверняка используется машино-зависимый стек поддерживаемый CPU, то есть точно не Stack T, то есть именно регламентируется где и как должна хранится информация для возврата, что бы CPU это понял.
Конечно если Stack T — вдруг стал виртуальной оберткой над поддерживаемым CPU стеком, то конечно это могло бы быть правдой, но нет.a-tk
13.08.2019 10:23А есть ещё архитектуры, отличные от x86, которые не хранят адреса возвратов. Например ARM, на котором .NET тоже уже вроде как работает.
Varim
14.08.2019 09:08Я не знаком с асемблером ARM. Там что приходится самому выдумывать где хранить адреса возвратов и текущий контекст состояния CPU? На ARM может тогда и подойдет Stack T, но может и на ARM проще указатель стека «двигать», что бы не использовать Stack T потому что наверняка у Stack T всякие излишние аллокации памяти есть?
a-tk
14.08.2019 09:24Нет, там есть специальный регистр — LR (Link Register) — куда записывается адрес возврата. А его уже можно куда-нибудь потом переложить.
mayorovp
16.08.2019 13:19Иными словами, адрес возврата всё равно хранится в стеке, просто конкретное место хранения в кадре определяется не архитектурой процессора, а компилятором.
skany
13.08.2019 07:19Когда я устраивался в качестве джуниора, на собеседовании были вопросы про сборку мусора (в т.ч. я рассказал про 3 поколения, о которых вы упомянули), как работает GC, boxing/unboxing и т.д. Я бы не сказал, что это что-то супер сложное… Обычно, такие вопросы не вызывают проблем у тех, кто дочитал книгу Рихтера :)
Matisumi
13.08.2019 11:21+1Не уверен что джун вообще должен знать о поколениях сборщика мусора. Да и Рихтера даже не обязательно читать и помнить все прочитанное. Вот дальше да, будет нужно.
Free_ze
13.08.2019 12:13Джуны джунам рознь. Никто не говорит, что это знать прямо необходимо и пригодится в работе уже завтра. Эти вопросы вполне можно провалить и успешно устроиться на работу. Задача интервьюера — найти «дно» знаний у кандидата, оценить его кругозор, найти области, в которых он «плавает» и наоборот прокачан сильнее необходимого. Для этого диапазон вопросов должен быть достаточно широк. Особенно это ценно при получении первой коммерческой работы.
skany
13.08.2019 13:35Честно слово, на предыдущем собеседовании (в другой компании) меня даже спросили, читал ли я Рихтера. После чего, я серьезно взялся за книги, и… Но об этом я, в скором времени, напишу во второй части своей статьи :)
Matisumi
13.08.2019 14:18Мне гораздо интереснее другое — многим из тех, кто «читал Рихтера потому что все говорят надо его читать» вообще пригодились эти знания в повседневной работе?
skany
13.08.2019 15:45Не скажу вам за тех многих, но, мне кажется, любая (адекватная) прочитанная книга в этой области, по крайней мере, позволяет немного меньше ошибаться, и тратить рабочее время
vasyan
13.08.2019 19:49Да. Инфа там полезная, но подача довольно тяжёлая. Я не думаю, что читать рихтера джунам — это хороший план.
skany
14.08.2019 07:23Согласен с вами насчет полезности и тяжелой подачи для начинающих. Но ведь никто не говорит, что его книга должна быть первой прочитанной по .NET :) В моем случае, я сначала читал Троелсена и информацию на сайте metanit
Kanut
13.08.2019 07:43Пункты конечно интересные и занимательные. Но на мой взгляд человеку, только начинающему писать на С#, знать про них не то чтобы надо. Там на мой взгляд другие вещи гораздо важнее.
Да и не для начинающих часть из пунктов интересна только в определённых ситуациях/проектах. И в этих случаях на мой взгляд всё равно надо регулярно поглядывать не меняется ли ситуация с новыми версиями и/или библиотекамиhunroll
13.08.2019 09:46человеку, только начинающему писать на С#
это не джун, это школьник/студент. Максимальная позиция — интерн. Джун — это человек, который пишет код, который потом попадёт в продакшн. И этот код должен быть предсказуемым и поддерживаемым. Когда «джун» случайно увидит в интернетах слово «рефлексия» и начнёт пихать её во все щели в проекте — тогда будут проблемы. Это основы. Это одна несчастная книга, которую можно осилить за полгода и писать код сознательно а не «скомпилилось? ничёси я крутой, коммичу»Kanut
13.08.2019 09:50+1Во первых давайте посмотрим на название статьи и увидим что там не ни слова про «джунов»/«миддлов»/«сениоров», а стоит «Заблуждения начинающих C# разработчиков».
А во вторых если взять конкретно те пункты, которые автор описал в своей статье, то они не особо нужны ни джунам, ни даже миддлам. И уж точно они менее важны чем куча других вещей, которые полезно знать людям пишущим на С# и о которых в статье нет ни слова.hunroll
13.08.2019 09:57Ну я согласен, некоторые пункты из этой статьи не нужны даже мидлам, да и синьёры используют эти познания только на собеседованиях)
Но вот пункты про IDisposable и про количество потоков — прям наболевшее. Кто-то где-то пишет using на общий ресурс, и всё, ищи потом, почему конекшен закрылся внезапно. Ну а с потоками доходило до того что ресурсов на их создание уходило на несколько порядков больше, чем на саму задачу.
Mikluho
13.08.2019 08:04+1Это скорее не заблуждения джунов, а укоренившиеся вредные вопросы с собеседований.
Именно там это любят спрашивать и именно там кочуют неправильные ответы, которые тиражируются на всяких форумах и бложиках. Мне порой казалось, что собеседующие как раз с бложиков и понатягивали эти вопросы.
Как-то раз интервьюер мне спрашивал про разницу между значимыми и ссылочными типами. Я ему рассказал про разницу синтаксиса, про конструкторы, про ссылки на кучу, про способы передачи и упаковку… А он мне, «есть ещё одна разница»… Я чуть потупил, потом говорю про выделение на стеке и в куче. Он радостно подтвердил, что именно это хотел услышать, но я сломал ему систему — рассказал про то, что локальная переменная всегда на стеке (хотя иногда и в регистре сразу), а вот её содержимое может и в куче, если это ссылочный тип. Хуже того, если поле значимого типа часть ссылочного типа, то значение окажется сразу в куче. А если в локальной переменной значимого типа поле ссылочного типа, то значение оного будет снова в куче. А совсем худо от того, что компилятору/оптимизатору может хватить ума весь ссылочный тип уложить на стек, если он коротко живёт.
И чтобы уж совсем его добить, я спросил «Как думаете, а поле типа int в классе претерпевает boxin/unboxing»? (да, это был не первый тупой вопрос на этом собеседовании и я был уже немного злой).
Мораль: всё зло от соцсетей :)
Точнее от того, что джуны не книжки читают, а их краткие пересказы (и это касается не только программирования), не пишут экспериментальный код, а смотрят в ютубе, как это делает кто-то другой :(nbytes
13.08.2019 17:45А что бы вы советовали почитать? Часто спрашивают, но я в свое время по С# кроме Шилдта и доки ничего не читал.
Mikluho
13.08.2019 18:20Что читать — зависит от специализации. Лично моё мнение — в программировании читать книжки по языкам почти бесполезно, если не писать код. Хорошо, когда есть конкретная задача или pet-project.
Если пока ещё нет конкретной цели, но интересен C#, можно начать c официальных туториалов, или с этой подборки.
Я на .net пишу с 2002. Тогда книжек не было — я шерстил msdn, rsdn. Писал, ковырял много чего. Потом избранные главы из разных авторитетных книжек, по мере выявления потребности.
Но я вообще по книжкам учил только Basic (89-й), C по Кернигану и Ритчи (90-й), да C++ по Страуструпу (92-й). Всё остальное — хэлпы, мануалы, доки и метод научного тыка… Да, блин, я английский выучил по докам :)
vasyan
13.08.2019 18:46Да в наше время даже сам Рихтер говорит: «зачем вам книжки писать, если вы их не читаете». Кстати, Рихтер, по сравнеию с Шилдтом пишет ужасно: что на русском, что в оригинале реально снотворное.
Самое лучшее, пока что есть, это статьи и официальная дока. Microsoft очень по этому поводу запарились и привели её в божеский вид.
Стоит читать framework design guidelines.Free_ze
13.08.2019 19:06Рихтер же пишет то, чего в доках нет — про хрупкую внутреннюю кухню.
… реально снотворное.
Самое лучшее, пока что есть, это статьи и официальная дока.
…
Стоит читать framework design guidelines.
Просытня советов — это разве не скучно?vasyan
14.08.2019 15:39Нет framework design guidelines это zero bullshit и очень близко к «земле».
Людям скучно читать что-то не про них (Сильмариллион какой-нибудь), а framework design guidelines — это именно про то с чем мы сталкиваемся каждый день.
Ну вот это например: docs.microsoft.com/en-US/dotnet/standard/design-guidelines/choosing-between-class-and-struct
чудо же.Free_ze
14.08.2019 16:21Близость к «земле» — это просто выжимка best practice, с минимумом ответов на вопрос: «Почему?», хотя темы поднимаются те же.
Тоже неплохо (для того, кому нужно завтра проект поддерживать, а технологию он впервые видит и нет времени читать тысячестраничные талмуды), но я бы не сказал, что это даже сейчас может быть достойным заменителем более подробным книгам.
Как мне кажется, не столько времени уходит на само чтение книг, сколько на усвоение информации. Много взаимосвязей, которые бывают неочевидны даже после первого прочтения. Шилдт же проще потому, что у него процент воды к информации выше (и это не плохо, ибо позволяет мозгу выиграть время на усваивание) и материал проще. А Рихтер рискует стать настольной книгой, хотя бы на какое-то время, где даже после первого прочтения придется возвращаться к отдельным главам за вновь появившимися вопросами, когда очередной пласт информации уляжется.
Odrin
13.08.2019 12:46Финализатор. По своей сути является страховкой. Вызывается неявно, в неопределенное время, во время сборки мусора.
Если точнее, то GC только помещает объект в очередь финализации. Из этой очереди объекты забирает отдельный поток финализации, который и вызывает финализатор.
e_fail
13.08.2019 14:54Также пытаться работать с диском из многих потоков — самоубийство. Диск и без того является тормозящим фактором многих программ, которые с ним работают. Если пытаться работать с ним из многих потоков, то надо забыть о быстродействии.
Для SSD проблема становится гораздо менее острой, в большинстве случаев можно не заморачиваться.
DistortNeo
13.08.2019 18:21Также пытаться работать с диском из многих потоков — самоубийство. Диск и без того является тормозящим фактором многих программ, которые с ним работают. Если пытаться работать с ним из многих потоков, то надо забыть о быстродействии.
С одной стороны хорошо, а с другой — не всегда. Можно на выходе получить софт, который тормозит, потому что форматируется дискета.
Реальный пример: однопоточный SMB-сервер. У меня была проблема с умирающим хардом, который мог несколько секунд отвечать на запрос. И пока он отвечал на запрос, работа с другими дисками была невозможна. В итоге подвисали все SMB-соединения.
ad1Dima
13.08.2019 19:05Если пытаться работать с ним из многих потоков, то надо забыть о быстродействии.
Можно работать с одни диском из одного не UI потока, например. Данная цитата вовсе не означает, что вся программа должна быть однопоточной
druss
13.08.2019 19:43+1Самое большое заблуждение начинающих C# разработчиков, это думать что знание всех нюансов работы GC, памяти, финализатора и прочих «низкоуровневых штук» рантайма автоматом сделает из них «не начинающих» C# разработчиков.
force
13.08.2019 19:52Однако такая имплементация пула объектов — однозначно плохая идея. Как и пытаться логировать, кидать исключения, обращаться к базе и тысячи подобных действий.
Вообще, в финализаторе логировать часто хорошая идея. Потому что вызов финализатора свидетельствует о том, что разработчик забыл Dispose (Естественно, там не забыть GC.SuppressFinalize или вляпать флажок).scumware
13.08.2019 22:06+1Это черевато непредсказуемыми падениями: порядок вызова финализаторов недетерминирован, равно как и порядок убийства объектов. Закрываешь окно приложения, а оно тебе НА! CurrentDomain_UnhandledException.
Чтобы гарантировать, что Dispose() таки вызвали, нужно тесты писать.force
13.08.2019 22:10+1Если вызвали Dispose, то финалайзер не должен вызываться. А если его забыли — то явный крэш приложения лучше, чем тихая утечкая ресурсов, которую потом отлавливать по обрывочным сведениям.
Я как-то из-за забытого диспоза при установке приложения клиенту, поехал не на поезд, а в гостиницу. И да, конечно же приложение не говорило, что кто-то забыл диспоз на 78-ой строчке в конкретном файле, и то, что забыт именно диспоз выяснилось потом.
mayorovp
16.08.2019 13:25Тем не менее, гарантируется что у обычных объектов финализаторы вызываются раньше чем у наследников CriticalFinalizerObject.
Что означает, в том числе, безопасность обращения к любым SafeHandle в "логирующем" финализаторе.
Ну и между SafeHandle отношения владения можно выстраивать при помощи подсчёта ссылок (DangerousAddRef/DangerousRelease), но тут уже надо внимательно всё проверять.
CrazyElf
13.08.2019 19:56Господи, вот хоть бы раз за мою долгую карьеру мне как-то пригодилось знание про поколения GC. Кроме как на собеседованиях — вообще ни разу не нужно было. И ещё туча таких же вопросов чисто собеседовательных.
В реальной жизни самое важное это вообще умение декомпозировать задачи, про которое ни на одном собеседовании не спросили и не спросят, потому что оно «не про язык программирования».force
13.08.2019 22:13Ну, мне поколения часто пригождались, правда больше встрявал в LOH, но и другие ситуации, связанные с «утечками» памяти тоже приходилось расследовать. Но большинству программистов нафиг не надо, да. Вопросы про это бестолковые.
rrust
13.08.2019 21:00Также пытаться работать с диском из многих потоков — самоубийство. Диск и без того является тормозящим фактором многих программ, которые с ним работают. Если пытаться работать с ним из многих потоков, то надо забыть о быстродействии.
Это может быть два физически разных диска, тогда одним потоком читаем, другим пишем. Это быстрее чем одним потоком оба действия.
Еще бывает и внутреннее распараллеливание потоков записи или чтения (обычно для SSD), тогда последовательная запись данных одним потоком может оказаться еще и медленнее 2-4 потоков.
В третьих у дисков иногда бывает кэш и множество коротких операций чтения/записи работают сильно быстрее в многопоточном режиме, если укладываются в размер кэша.
Если кто в курсе про файловые системы, то у каждого файла есть метаданные (имя, размер, атрибуты, время создания, изменения, доступа, и данные распределения на диске). ОС с целью обеспечения сохранности структур файловой системы все время фиксирует на диск их изменения для каждого файла. То есть один поток вынужден ждать когда ОС зафиксирует очередное изменение на диск, а много потоков могут разделять это время.
Конечно если паттерн обращений на диск не может параллелиться, а зачастую это так, то лучше это делать одним потоком.
vasyan
По-моему это почти всегда вопрос чисто для собеседования. Мне кажется, потому что Рихтера дальше сборщика мусора большинство не осилили.
В C# очень мало способов работать со сборщиком мусора и реально редки кейсы, когда знание про то, что он есть хоть как-то помогает. Другими словами, раньше ваш проект закроют, чем вы дойдёте до тонкой оптимизации кода под сборщик мусора.
scumware
Тут даже не про тонкую оптимизацию речь. Такие знания наиболее полезны, когда от пользователей повалили дампы с жалобами на «повисшую» софтиную. Раскопать, кто же наинвочил в UI'ный поток, без знания о сборке мусора практически невозможно, особенно если QA не может воспроизвести проблему, а напрямую отдебажить не представляется возможным ввиду объёма кода и кол-ва потоков.
vasyan
Специфика конкретного UI-фреймворка. Следуйте гайдлайнам и не будет таких проблем. Понятно, что у разных UI-фреймворков свои тонкости.
Я никогда не работал с WPF и Unity, а в asp.net таких проблем не было и нет, там проблемы свои, но я за 10 лет ни разу не встречался с проблемами от GC в самых разных проектах.
scumware
Это не проблема фрэймворка — это проблема кода приложения. Многие фрэймворки требуют обращения к UI'ным объектам только в UI'ном потоке, просто потому что STAThread — тяжкое наследие COM'а. Во многих фрэймворках и библиотеках есть Invoke() и BeginInvoke()
DistortNeo
Сейчас с async-await стало чуть легче писать подобный код.
ad1Dima