Управление памятью в net для профессионалов

Обновлено: 07.07.2024

Проблема управления ресурсами вообще и памятью в частности является одной из наиболее животрепещущих при разработке программного обеспечения. Сколько раз после успешного завершения работы над очередным шедевром ваше хорошее настроение портилось из-за того, что служба тестирования выявляла утечку памяти (кто её только просил)? Или вдруг заказчик начинал жаловаться на то, что последняя версия вашего редактора через час работы начинает рисовать не то, что ему хочется видеть, и вскоре вообще перестаёт подавать признаки жизни (подумаешь, всего-то забыл удалить кисточку и карандаш). Ещё хуже, когда приходится перелопачивать мегабайты исходного кода в поисках незакрытых соединений, т.к. с увеличением числа рабочих станций сервер базы данных начинает нещадно тормозить из-за нехватки ресурсов. В конце концов, выясняется, что с закрытыми соединениями программа местами вообще отказывается работать, и принимается «мудрое» решение переписать всё заново.

Безусловно, с опытом разработчики вырабатывают свои правила написания программ и приёмы борьбы с утечкой ресурсов. В таких языках, как C++ и Object Pascal, этому способствует наличие конструкторов и, главным образом, деструкторов объектов. Наиболее продвинутые программисты вообще не используют в своих программах выделение памяти или других ресурсов без использования специальных классов-обёрток. В частности, для работы с памятью используются так называемые «умные» указатели (smart pointers), которые управляют временем жизни объектов, созданных в динамической памяти.

ПАМЯТЬ БОЛЬШЕ НЕ РЕСУРС.

Сборщик мусора

Для хранения объектов CLR использует хип, подобный хипу C++, за тем важным исключением, что хип CLR не фрагментирован. Выделение объектов производится всегда один за другим в последовательных адресах памяти, что позволяет весьма существенно повысить производительность всего приложения (рис. 1-1).


Рис.1. Начальное состояние хипа.

Когда память заканчивается, в дело вступает процесс, который мы опишем ниже. Фактически, основная задача этого процесса сводится к тому, чтобы освободить место в хипе путём дефрагментации неиспользуемой памяти.


Рис. 2. Хип после завершения работы с некоторыми объектами.

Первое, что делает GC во время сборки мусора – это принимает решение о том, что все выделенные блоки памяти в вашей программе - это как раз и есть мусор, и они вам больше не нужны ;o) К счастью, на этом GC не прекращает свою работу. Далее начинается утомительный поиск «живых» указателей на объекты по всем закоулкам приложения. Microsoft называет эти указатели «roots». В процессе поиска GC сканирует глобальную память программы (на рисунках обозначено как Global), стек (Stack – локальные переменные) и даже регистры процессора (CPU). Как мы знаем, каждый поток в программе имеет свой собственный стек, и CLR приходится сканировать их все. Кроме того, интересен тот факт, что CLR умеет работать даже с потоками, которые создавались в неуправляемом коде с использованием функций WinAPI. По крайней мере, следующий тест на MC++ отработал правильно, выдав на консоль результат – «212».

Если бы CLR не знал ничего о созданном без его участия потоке и не просканировал бы его стек, то указатель ptr1 был бы не найден и не расценен как «живой». В этом случае деструктор объекта, на который указывает ptr1, так же был бы вызван во время сборки мусора, то есть до вывода на консоль единицы.

Найдя «живой» указатель, сборщик мусора помечает объект, на который этот указатель указывает, как всё ещё используемый программой и запрашивает информацию о его структуре, чтобы в свою очередь просканировать уже этот объект на наличие указателей на другие объекты. И так далее, пока все указатели в программе не будут обработаны.

Теперь GC знает, какие объекты можно удалить, а какие пока не стоит. Сама по себе задача дефрагментации памяти является тривиальной, и все сложности начинаются позже, когда приходит время для корректировки всех указателей, которые существуют в программе, и присвоения им новых значений, получившихся после дефрагментации. Ну что же, GC только что просканировал всю память программы, почему бы не сделать это ещё раз.


Рис. 3. Хип после сборки мусора.

Тест производительности

Наверняка у вас возникли сомнения в эффективности подобного решения, по крайней мере, у меня они точно были, в связи с чем я решил провести небольшой эксперимент и проверить, как будет себя вести GC в сравнении с хипом C++ при выделении большого числа объектов.

Здесь мы создаём 10 миллионов объектов, каждый из которых создаёт в свою очередь массив байт заданного (maxObjSize) размера, и ещё один объект, который также создаёт массив размером 20 байт. В общей сложности мы имеем 40 миллионов создаваемых объектов. Число одновременно живущих базовых (содержащих в своём составе ещё три) объектов задаётся значением переменной maxListSize, и в нашем тесте будет равняться 1,000, 10,000 и 100,000 элементов. Значением maxObjSize мы будем регулировать размер создаваемого в первом объекте массива.

Кроме того, практический интерес представляет только первая диаграмма, так как именно на ней протекает реальная жизнь 99% всех программ. Создание же такого количества одновременно живущих объектов и таких размеров, как на последующих диаграммах вряд ли имеет место в реальной жизни, поэтому эти результаты для нас будут представлять больше познавательный интерес.

Кроме CLR (GC) в тесте будут участвовать хип C++ (C++), хип Windows (Win) и QuickHeap (QH).

Далее приведены результаты тестов для различного количества одновременно живущих объектов. По горизонтали отмечен размер создаваемого в первом объекте массива (maxObjSize), по вертикали – время выполнения теста.


Рис. 4. Тест для 1,000 объектов.

На первой диаграмме хорошо видно, что автоматическое управление памятью в несколько раз опережает хип Windows и C++ при выделении небольших фрагментов. На отметке 700 байт это опережение заканчивается, и далее уверенно лидируют хипы.

ПРИМЕЧАНИЕ

Безусловный лидер QuickHeap в наших тестах учитываться не будет ввиду своей специфики, и приведён здесь лишь для сравнения.


Рис. 5. Тест для 10,000 объектов.

Увеличим число одновременно живущих объектов в 10 раз. В принципе, картина изменилась не сильно, за исключением того, что на отметке в 400 байт GC начал резко сдавать. И это вполне объяснимо.

Дело в том, что в некоторых случаях сборщик мусора может быть не вызван ни разу за всё время работы программы. В том числе и при её завершении. Посудите сами, зачем убирать мусор в доме, предназначенном на снос. Мы в нашем тесте подбирали число создаваемых объектов таким образом, чтобы сборщик мусора вызывался минимум 2-4 раза.

Аномалия на второй диаграмме обусловлена тем, что число вызовов GC резко возрастает почти до 400! на отметке 400 байт, и более чем до 900 раз при размере массива в 1000 байт.

Размер объектаВремя тестаВызовы GC
100:00:063
1000:00:113
2000:00:163
3000:00:224
4000:00:40394
5000:00:53498
6000:01:03584
7000:01:08529
8000:01:19597
9000:01:31665
10000:01:57932

Продолжим наш тест.


Рис. 6. Тест для 100,000 объектов.

При увеличении числа одновременно живущих объектов ещё на порядок ситуация стала ещё более печальной для GC. Теперь выигрыш заканчивается на 75 байтах, и далее GC начинает отставать, проигрывая в десятки раз, а при размере объектов в 5000 байт GC вообще впал в анабиоз часа на три, в то время как хипы уверенно справились с задачей за несколько минут. Хотя, конечно, этот случай уже совсем клинический. Посудите сами, программа за время своей работы выделяет более 50 гигабайт памяти мелкими кусочками, причём в один и тот же момент времени ей необходимо 400,000 объектов с суммарным размером более 500 мегабайт. Возможно, такие задачи и встречаются в жизни, но наверняка в них используется другой, менее универсальный, но более эффективный механизм работы с памятью. В общем, реально это или не реально, но мы теперь знаем, что нас ожидает в подобном случае.

В тоже время, это совсем не означает, что CLR не может работать с объектами большого размера. Очень даже может, вопрос лишь в том, как много таких объектов в вашей программе. Более того, работа с большими объектами в CLR специально оптимизирована. Для объектов размером более 20 килобайт используется отдельный хип, который отличается от обычного тем, что он никогда не дефрагментируется.

Вообще, алгоритмы работы GC построены во многом исходя из правил, полученных статистическим и опытным путём. В частности, одно из таких правил утверждает, что только что созданные «молодые» объекты имеют наиболее короткое время жизни, а живущие уже давно будут жить ещё долго. Именно в соответствии с этим правилом в CLR существуют понятие поколений (generations). Объекты, выжившие после первой сборки мусора и дефрагментации, объявляются первым поколением. В следующий раз, те объекты из первого поколения, которые опять смогли выжить, перемещаются во второе поколение и уже больше не трогаются, выжившие объекты из нулевого поколения перемещаются в первое. Другими словами, объекты, пережившие две сборки мусора, остаются в хипе навсегда и GC не занимается их дефрагментацией .

Теперь, зная этот факт, мы можем легко объяснить причину резкого падения производительности GC на второй и третьей диаграммах. Всё дело в том, что хип просто был перенаселён вторым поколением и для новых объектов оставалось совсем немного места, из-за чего сборщик мусора вызывался всё чаще и чаще.

Освобождение ресурсов в CLR

Давайте теперь разберёмся с тем, как мы подсчитывали количество вызовов GC, а заодно рассмотрим природу деструкторов и механизм освобождения ресурсов, принятый в CLR.

Для подсчёта вызовов GC мы воспользовались следующим классом, объект которого необходимо создать только один раз:

Для работы с объектами, имеющими метод Finalize, CLR использует следующий механизм. При создании объекта, если он содержит метод Finalize, ссылка на него помещается в специальный список, называемый Finalization Queue (рис. 7).


Рис. 7. Управляемый хип и Finalization Queue.


Рис. 8. Хип после завершения работы с объектами A, D, E, H.

После того, как GC определяет, что какой-либо объект можно удалить, ссылка на этот объект ищется в Finalization Queue, и если находится, то объект оставляется в покое до следующей сборки мусора, а ссылка на него из Finalization Queue удаляется и добавляется в другой список, называемый F-reachable Queue.


Рис. 9. Хип после первой сборки мусора.

Далее этим списком занимается специально созданный для этого поток, который по очереди вызывает методы Finalize для объектов из F-reachable Queue, а затем удаляет их и из этого списка.


Рис. 10. GC вызвал методы Finalize для объектов D и H и теперь они просто мусор.


Рис. 11. Хип после второй сборки мусора.

Зачем нужны такие сложности? Дело в том, что по идее деструктор вызывается после того, как объект уже никто не использует, но нам никто не мешает прямо в деструкторе сохранить указатель на наш объект в какой-нибудь глобальной переменной и использовать его в дальнейшем без особых угрызений совести. При описанной выше схеме с объектом ничего не случится, и им можно будет спокойно пользоваться сколь угодно долго. Единственное отличие заключается в том, что после того, как наш объект станет не нужен, метод Finalize для него вызван уже не будет, так как ссылка на наш объект уже отсутствует в Finalization Queue. Но, как вы уже догадались, и эта ситуация исправима. Метод ReRegisterForFinalize, который мы используем в нашем примере, как раз и позволяет вернуть наш объект обратно в Finalization Queue.

Как видите, наличие деструкторов у объектов не предвещает ничего хорошего для вашей программы. Такие объекты не удаляются первой же сборкой мусора и у них больше шансов попасть во второе поколение и остаться в хипе навсегда, бесполезно занимая память, даже если они уже никому больше не нужны.

Всё это, конечно, не означает, что деструкторы – это зло, специально добавленное в CLR, чтобы усложнить нам жизнь. Ими можно и нужно пользоваться для освобождения unmanaged-ресурсов, таких, как файловые дескрипторы, соединения с БД и COM-объекты, но в тоже время нужно чётко понимать, что за этим стоит, и уж тем более не следует добавлять их к классам «просто так на всякий случай».

Интерфейс IDisposable

Думаю, исключение System.IO.IOException вам обеспечено по причине «The process cannot access the file "test.txt" because it is being used by another process.» (Процесс не может достучаться до файла, потому что до него уже достучался другой процесс). В общем-то, ничего удивительного, файлы за собой нужно закрывать, но в данном случае это ещё не всё. Даже написав код закрытия файла, нет никакой гарантии, что он точно будет выполнен. Что будет, если между открытием и закрытием файла произойдёт исключение? Даже если мы и обработаем это исключение позже, то наш файл опять не будет вовремя закрыт. Чтобы в данном случае обеспечить корректную работу, нам нужен примерно следующий код:

Вот теперь всё будет работать как надо. Для этого нам всего лишь понадобилось добавить ещё девять строчек кода к исходным двум. Не много ли для такой простой задачи? Тем более что на C++ такого же результата вполне можно добиться всё теми же двумя строчками, где и закрывать ничего не надо, и никакие исключения не страшны:

Похоже, using появилась в языке перед самым его выходом, по крайней мере, ничего подобного нет больше ни в одном другом языке, поддерживающем CLR. Да и в мегабайтах исходного кода, который предоставила Microsoft как CLI, эта конструкция встречается всего пару раз, в то время как её аналог в виде try/finally сплошь и рядом. К тому же в статьях Джефри Рихтера об алгоритмах работы GC, которые были опубликованы полтора года назад в MSDN Magazine и которые затем аккуратно в полном объёме перекочевали в его книгу, нет никакого упоминания о using. В самой же книге этому уже посвящён целый небольшой раздел. Вряд ли человек, который имеет неограниченный «доступ к телу», включая самые интимные места, не знал об этом ранее.

  • Всё это вселяет определённую надежду на то, что в будущем проблема освобождения ресурсов если и не будет решена так же изящно, как в C++, то, по крайней мере, работы в этом направлении будут вестись.
  • Перепишем наш пример с использованием using:
  • Это уже гораздо лучше. Конструкция using генерирует код, аналогичный нашему примеру с try/finally, за тем исключением, что вместо метода Close она вызывает Dispose. Вполне логично предположить, что объект должен иметь в своём составе такой метод. Так и есть, мы можем использовать с этой конструкцией только те объекты, которые реализуют интерфейс IDisposable, содержащий в своём составе один единственный метод:

Теперь можно просто наследоваться от RSDN.DisposableType и всего лишь перекрывать метод Dispose(bool), в котором можно освобождать ресурсы:

Тем не менее, и этот способ недостаточно хорош для настоящих пуристов от C++. Так в процессе обсуждения этой темы на форумах RSDN родилась идея использовать какой-либо атрибут, с помощью которого можно было бы помечать нужные члены класса и свести процедуру очистки ресурсов к следующему виду:

Это вполне осуществимо. Добавим в наш класс реализацию этой возможности:

Было бы совсем здорово, если бы этот способ был встроен в сам компилятор, который мог бы при необходимости автоматически добавлять наследование от IDisposable и генерировать метод Dispose. В этом случае можно было бы использовать всё тот же атрибут или ключевое слово using:

Слабые ссылки

Следующий пример демонстрирует возможный вариант применения «слабых» ссылок:

Класс System.GC

В заключение разберём класс System.GC, который предоставляет интерфейс к подсистеме сборки мусора. Мы не можем создавать экземпляры этого класса и не можем от него наследоваться, да это и не нужно, так как все методы и свойства этого класса статические и доступны для использования без создания объекта. Рассмотрим их все по порядку.

MaxGeneration

Это свойство возвращает максимальный номер поколения, который поддерживается системой. В настоящий момент это значение равно 2.

Метод Collect

Принудительный вызов сборщика мусора.

Параметр generation задаёт максимальный номер поколения, которое будет обработано при сборке мусора. Первый метод использует в качестве номера поколения максимальный номер и фактически соответсвует следующему коду:

GetGeneration

Возвращает номер поколения, в котором находится заданный объект.

GetTotalMemory

Возвращает число байт, занимаемых в данный момент управляемым хипом.

Если параметр forceFullCollection равен true , то функция возвращает занимаемую хипом память после вызова сборщика мусора. Как мы уже знаем, GC не гарантирует освобождение абсолютно всей возможной памяти. Данная функция вызывает сборщик мусора несколько (до 20) раз до тех пор пока разница в занимаемой памяти до и после сборки не будет составлять 5%.

KeepAlive

Предотвращает удаление объекта при сборке мусора, если даже на него нет уже ссылок.

Подобная возможность может понадобитсься, например, в случае если ссылки на ваш объект уже отсутствуют в управляемом коде, но ваш объект всё ещё необходим unmanaged коду.

ReRegisterForFinalize

Добавляет объект в Finalization Queue.

Мы уже пользовались этим методом для подсчёта числа вызовов GC.

SuppressFinalize

Удаляет объект из Finalization Queue.

WaitForPendingFinalizers

Останавливает текущий поток до завершения выполнения методов Finalize для всех объектов из F-reachable Queue (рис. 9).

Под катом история о том, зачем мы в это ввязались, как был организован процесс и с какими трудностями мы столкнулись.





Об авторе статьи: Евгений Биккинин, участник DotNetRu, один из редакторов перевода.

Мотивация

Многие из вас спросят: зачем вообще переводить техническую книгу на русский язык? Ведь английский давно уже стал основным языком IT-специалиста, настолько, что некоторые фразы наших русскоязычных коллег приходится в уме переводить — так в них много английских слов.

Я бы выделил несколько пунктов:

  • Недостаточный уровень знания языка. Порой знания нужны быстрее, чем изучение курсов английского.
  • Время и место чтения. Это особенно актуально для бумажных книг, когда поблизости нет словаря или интернета, чтобы быстро перевести фразу. Я, например, часто читаю в метро с электронной книги.
  • Скорость потребления контента. Ты вроде бы понимаешь все слова предложения, кроме одного, от которого часто зависит смысл всей фразы. А еще предложения в книгах обычно сложнее, чем в статьях или документации. Ты реально тратишь время и усилия, чтобы понять, что же сказано в одном предложении. В результате сбивается ритм и чтение в целом становится менее эффективным.

Как появился этот проект



Стенд сообщества на TechTrain

Действительно, независимое некоммерческое сообщество — кто сможет осилить эту задачу лучше него? Люди, которые каждый день пишут код, собирают митапы, пишут статьи на хабре, обучают будущих коллег в школах и университетах, настойчиво вкладываются в создание и распространение русскоязычного материала по любимой платформе. Это идеальный симбиоз. Сообществу нужны качественные переводы фундаментальных книг. Издательству нужен спрос на их продукт. Несмотря на то, что для нас это был первый подобный опыт, мы схватились за предложение с азартом.

Прежде всего мы связались с Конрадом Кокосой и получили его благословение на перевод (кстати, в процессе мы не раз обращались к нему за разъяснениями неочевидных мест и всегда получали большую поддержку). Далее мы собрали фанатов творчества автора, людей не понаслышке знакомых с описываемыми процессами. Разбавили их программистами-любителями русского языка и литературы (да, такие есть). И получили отличную команду, готовую на несколько месяцев посвятить себя неизведанным подвигам переводческого дела.

Трудности

Первая же глава показала, что просто техническим корректированием дело не обойдётся. Нам захотелось существенно переделать перевод. И это оказалось не так просто, как кажется. Для издательских домов такая практика отсутствует, потому что они сталкиваются с риском некачественного перевода от непонятных людей. Но нам повезло. После долгих обсуждений издательство ДМК-пресс согласилось устроить эксперимент и отдать перевод в наше полное распоряжение. За что мы им крайне благодарны. Поэтому данный труд — это не только перевод книги, но еще и попытка доказать, что русский вариант может быть не только качественным с технической точки зрения, но и благозвучным со стороны языка.

В большинстве случаев исправления были довольно очевидными. Заменяем «каркас» на «фреймворк», «трассу вызова» на «стек вызова», а «интеллектуальный указатель» на «умный указатель».

Но были и загвоздки. Одним из таких случаев стала парочка plug/gap. С одной стороны, нет какого-то устоявшегося IT-перевода для этих слов. С другой стороны, это не фантазия автора. Эти слова используются непосредственно в коде gc.cpp. Так вот, по смыслу, plug — это непрерывная область в памяти, в которой находятся используемые объекты, а gap — это промежуток между такими областями.


Сперва в качестве перевода этих двух слов использовалась пара «колодка/зазор». Но мы решили, что такой перевод точно не будет восприниматься читателем адекватно. Поэтому нам пришлось пожертвовать краткостью и ввести понятия «заполненный/пустой блок».
Чуть позже в тексте, правда, появились pinned plugs но всё же «закрепленные заполненные блоки», кажется, звучит лучше, чем «закрепленные колодки».

К счастью таких специфических терминов в книге не так уж много. В основном, все же, встречались вполне известные descriptor, handle, value type, reference type и прочие. Казалось бы, все эти термины давно переведены, однако и с ними возникли разногласия.

Descriptor буквально переводится как «описатель». Но и handle тоже часто так переводят. Поэтому пришлось переводить descriptor буквально «дескриптор», а handle стал «описателем» (не переводить же как «ручка» — и такое встречалось в некоторых переводах).

При редактировании книги объемом почти 1000 страниц отдельный вызов — это добиться одинаковых терминов во всех главах. Поскольку редактированием занималась группа, то главы обрабатывались не строго по порядку, и иногда случалось использовать слегка различные термины в разных главах. Несомненно, общий словарь терминов сильно помогал, но это не отменило необходимости финальной вычитки всех глав подряд и сверки всех терминов.



На фото автор книги с одним из переводчиков

Заключение

Спасибо всем тем, благодаря кому этот проект стал реальностью. Конечно, Конраду за оригинальный труд и за помощь в русскоязычной версии книги. Всем, кто поддержал настоящую революцию внутри издательства и готов идти на эксперименты.

Нашей дружной команде, безвозмездно делающей мир лучше, а именно:

  • Игорь Лабутин
  • Ирина Ананьева
  • Максим Шошин
  • Елизавета Голенок
  • Евгений Биккинин
  • Ренат Тазиев
  • Анатолий Кулаков

Промокод на скидку 25%: DotNet25.

А еще мы создали телеграм-чат. Здесь вы можете пообщаться с редакторами перевода, задать свои вопросы, объяснить, в чём и как сильно мы были не правы, либо просто подискутировать на тему переводов и участия сообщества в подобных активностях.



Дата выхода: апрель 2020 года

В книге представлены:

Промокод на скидку в 25%: DotNet25

→ Перейти к книге

Дата выхода: август 2020 года

Эта книга предназначена для разработчиков, стремящихся улучшить свои знания о Boost, и тех, кто хочет упростить процесс разработки приложений. Предполагается, что читатель знаком с C++ и имеет базовые знания стандартной библиотеки.

Начиная с основ C++, читатель перейдёт к изучению того, как библиотеки Boost упрощают разработку приложений. Увидите, какую работу можно выполнить во время компиляции и на что способны контейнеры Boost. Научитесь разрабатывать качественные, быстрые и портативные приложения. От манипулирования изображениями до графов, каталогов, таймеров, файлов и работы в сети – каждый найдет для себя интересную тему. Напишите программу один раз и используйте ее в операционных системах Linux, Windows, macOS и Android. Обратите внимание, что знания, полученные в ходе прочтения этой книги, не устареют, поскольку все больше и больше библиотек Boost становятся частью стандарта C++.

В книге представлены:

  • новые типы данных для повседневного использования;
  • использование умных указателей для управления ресурсами;
  • вычисление времени компиляции;
  • использование библиотеки Boost для многопоточности;
  • параллельном выполнении различных задач;
  • будете выполнять повседневные задачи, связанные со строками, с помощью библиотек Boost;
  • научитесь разбивать все процессы, вычисления и взаимодействия на задачи и обрабатывать их независимо друг от друга;
  • изучите основы работы с графами, трассировкой стека, тестированием и межпроцессным взаимодействием;
  • рассмотрите различные вспомогательные макросы, используемые для обнаружения возможностей компилятора, платформы и функциональности Boost.

Промокод на скидку в 25%: Boost25

→ Перейти к книге

Дата выхода: ноябрь 2018 года

Это издание учит использовать Kubernetes для развертывания распределенных контейнеризированных приложений. Перед тем, как собрать свой первый кластер Kubernetes, вы начнете с обзора систем Docker и Kubernetes. Вы будете постепенно расширять свое начальное приложение, добавляя новые функциональные возможности и углубляя свои знания архитектуры и принципа работы Kubernetes. Также вы изучите такие важные темы, такие как мониторинг, настройка и масштабирование.

В книге представлены:

  • внутреннее устройство Kubernetes;
  • развертывание контейнеров в кластере;
  • обеспечение защиты кластеров;
  • обновление приложений с нулевым временем простоя

Об авторе: Марко Лукша (Marko Luksa)– инженер Red Hat, работающий на Kubernetes и OpenShift.

Промокод на скидку в 15%: OTUS15

→ Перейти к книге

«Python. Книга рецептов»

Дата выхода: июль 2019 года

Если вам нужна помощь в разработке программ на языке Python 3 или вы планируете заняться обновлением старого кода на Python 2, эта книга – ваш счастливый билет. Она наполнена практическими примерами, разработанными и протестированными в среде Python 3.3, поэтому станет уникальным подспорьем для опытных программистов, желающих сконцентрироваться на современных инструментах и идиомах. В книге приведены полноценные рецепты, охватывающие свыше 10 основных тем Python, а также задачи, имеющие широкий спектр областей применения. Каждый рецепт содержит примеры кода, которые вы можете использовать в своих проектах, а также включает обсуждение принципов работы данного решения.

Кратко о темах:

  • структуры данных и алгоритмы;
  • строки и текст;
  • числа, даты и время;
  • итераторы и генераторы;
  • ввод-вывод данных и работа с файлами;
  • преобразование и обработка данных;
  • функции;
  • классы и объекты;
  • метапрограммирование;
  • модули и пакеты;
  • сетевое и веб-программирование;
  • конкурентное программирование;
  • полезные скрипты и системное администрирование;
  • тестирование, отладка и исключения;
  • расширения на языке C.

Об авторах:

Дэвид Бизли (David Beazley) — независимый разработчик программного обеспечения, преподает программирование разработчикам, ученым и инженерам. Он написал книгу «Python. Подробный справочник» и разработал несколько пакетов Python с открытым исходным кодом.

Брайан К. Джонс (Brian K. Jones) — системный администратор на факультете информатики в Принстонском университете.

Промокод на скидку в 15%: OTUS15

→ Перейти к книге

«Go на практике»

Дата выхода: январь 2017 года

Кратко о темах:

  • десятки конкретных практических приемов программирования на Go;
  • использование языка Go для создания обычных и облачных приложений;
  • разработка веб-служб RESTful и микрослужб;
  • практические приемы веб-разработки.

Об авторах:

Мэтт Батчер (Matt Butcher) – архитектор программного обеспечения компании Deis.

Мэтт Фарина (Matt Farina) – ведущий инженер группы передовых технологий в компании Hewlett Packard Enterprise.

Оба автора книги «Go на практике»являются техническими писателями, лекторами и активными участниками проектов с открытым исходным кодом.

Промокод на скидку в 15%: OTUS15

→ Перейти к книге

«Запускаем Ansible»

Дата выхода: апрель 2018 года

Среди множества систем управления конфигурациями Ansible обладает неоспоримыми преимуществами. Он минималистичен, не требует установки программного обеспечения на узлах, а также легок в освоении. Второе издание книги научит вас выстраивать продуктивную работу в кратчайшие сроки, будь вы разработчик, разворачивающий код в производственной среде, или системный администратор в поисках более эффективного решения для автоматизации. Авторы книги расскажут вам, как написать сценарий (скрипт управления конфигурациями Ansible), установить контроль над удаленными серверами, а также задействовать мощный функционал встроенных декларативных модулей. Вы поймете, что Ansible обладает всеми функциональными возможностями, которые вам необходимы, и той простотой, о которой вы мечтаете.

Кратко о темах:

  • узнайте, чем Ansible отличается от других систем управления конфигурациями;
  • используйте формат файлов YAML для написания собственных сценариев;
  • изучите пример полного сценария для развертывания нетривиального приложения;
  • администрируйте машины Windows и автоматизируйте конфигурацию сетевых устройств;
  • производите развертывание приложений на Amazon EC2 и других облачных платформах;
  • используйте Ansible для создания образов Docker и развертывания контейнеров Docker.

Об авторах:

Лорин Хохштейн (Lorin Hochstein) является старшим инженером по программному обеспечению (Senior Software Engineer) команды Chaos в компании Netflix. Он также работал старшим инженером по программному обеспечению в компании SendGrid Labs, был ведущим архитектором облачных сервисов (Lead Architect for Cloud Services) в компании Nimbis Services и занимал должность ученого в области компьютерных наук в Институте информатики Университета Южной Калифорнии (University of Southern California’s Information Sciences Institute).

Рене Мозер (Rene Moser) занимает позицию системного инженера в компании Swiss, является разработчиком ASF CloudStack, автором интеграции CloudStack в Ansible и ключевым членом сообщества Ansible с 2016 года.

Промокод на скидку в 15%: OTUS15

→ Перейти к книге

«Практика реактивного программирования в Spring 5»

Дата выхода: июль 2019 года

Современному бизнесу необходимы программные системы нового типа, способные оставаться отзывчивыми при любых нагрузках. Эту потребность можно удовлетворить с использованием приемов реактивного программирования; однако разработка таких систем – сложная задача, требующая глубокого понимания предметной области. Для разработки отзывчивых систем разработчики Spring Framework придумали и создали проект Project Reactor. Данная книга начинается с основ реактивного программирования в Spring. Вы исследуете многочисленные возможности построения эффективных реактивных систем с помощью Spring 5 и других инструментов, таких как WebFlux и Spring Boot. Познакомитесь с методами реактивного программирования и научитесь использовать их для взаимодействий с базами данных и между серверами. Освоите навыки масштабирования с Spring Cloud Streams и научитесь создавать независимые и высокопроизводительные реактивные микросервисы.

Кратко о темах:

  • откроете разницу между реактивной системой и реактивным программированием;
  • исследуете преимущества реактивных систем и область их применения;
  • освоите приемы реактивного программирования в Spring 5;
  • получите представление о Project Reactor;
  • построите реактивную систему с использованием Spring 5 и Project Reactor;
  • создадите высокоэффективный реактивный микросервис с использованием Spring Cloud;
  • научитесь тестировать, выпускать и осуществлять мониторинг реактивных приложений.

Об авторах:

Олег Докука (Oleh Dokuka) – опытный инженер-программист, обладатель награды Pivotal Champion и один из основных вкладчиков в развитие Project Reactor и Spring Framework. Он хорошо знает, как устроены оба фреймворка, и ежедневно популяризирует идеи реактивного программирования с использованием Project Reactor. Наряду с этим Олег использует Spring Framework и Project Reactor в разработке программного обеспечения, поэтому он не понаслышке знает, как создавать реактивные системы с применением этих технологий.

Игорь Лозинский (Igor Lozynskyi) – старший Java-разработчик, в основном создающий надежные, масштабируемые и невероятно быстрые системы. Имеет за плечами более чем семилетний опыт работы с платформой Java. Увлекается интересными и динамичными проектами как в своей жизни, так и в разработке программного обеспечения.

Промокод на скидку в 15%: OTUS15

→ Перейти к книге

«Использование Doker»

Дата выхода: январь 2017 года

Контейнеры Docker предоставляют простые быстрые и надёжные методы разработки, распространения и запуска программного обеспечения, особенно в динамических и распределённых средах. Из книги вы узнаете, почему контейнеры так важны, какие преимущества вы получите от применения Docker и как сделать Docker частью процесса разработки. Вы последовательно пройдете по всем этапам, необходимым для создания, тестирования и развёртывания любого веб-приложения, использующего Docker. Также вы изучите обширный материал — начиная от основ, необходимых для запуска десятка контейнеров, и заканчивая описанием сопровождения крупной системы с множеством хостов в сетевой среде со сложным режимом планирования. Издание предназначено разработчикам, инженерам по эксплуатации и системным администраторам/

Кратко о темах:

  • начало работы с Docker — создание и развёртывание простого веб-приложения;
  • использование методик непрерывного развёртывания для продвижения вашего приложения к активному промышленному использованию несколько раз в день;
  • изучение различных возможностей и методик для регистрации в системных журналах и наблюдения за многочисленными контейнерами;
  • исследование сетевой среды и сетевых сервисов: как контейнеры находят друг друга и каким образом можно установить соединение между ними;
  • распределение и организация кластеров контейнеров с целью балансировки нагрузки, масштабирования, устранения критических сбоев и планирования;
  • обеспечение безопасности системы, следуя принципам «глубокой или много-уровневой защиты» и минимальных привилегий;
  • применение контейнеров для построения архитектуры микросервисов.

Об авторе:

Промокод на скидку в 15%: OTUS15

→ Перейти к книге

«Глубокое обучение»

Дата выхода: ноябрь 2017 года

Глубокое обучение — это вид машинного обучения, наделяющий компьютеры способностью учиться на опыте. Книга содержит математические и концептуальные основы линейной алгебры, теории вероятностей и теории информации, численных расчетов и машинного обучения в том объеме, который необходим для понимания материала. Описываются приемы глубокого обучения, применяемые на практике, в том числе глубокие сети прямого распространения, регуляризация, алгоритмы оптимизации, сверточные сети, моделирование последовательностей и др. Рассматриваются такие приложения, как обработка естественных языков, распознавание речи, компьютерное зрение, онлайновые рекомендательные системы, биоинформатика и видеоигры. Наконец, описываются перспективные направления исследований: линейные факторные модели, автокодировщики, обучение представлений, структурные вероятностные модели, методы Монте-Карло, статистическая сумма, приближенный вывод и глубокие порождающие модели. Издание будет полезно студентами и аспирантам, а также опытным программистам, которые хотели бы применить глубокое обучение в составе своих продуктов или платформ.

Кратко о темах:

  • базовый математический аппарат;
  • известные алгоритмы глубокого обучения;
  • линейная алгебра, теория вероятностей и фундаментальные концепции машинного обучения;
  • основы машинного обучения;
  • глубокие сети прямого распространения;
  • регуляризация в глубоком обучении;
  • оптимизация в обучении глубоких моделей;
  • сверточные сети;
  • моделирование последовательностей: рекуррентные и рекурсивные сети;
  • выбор алгоритма для конкретного приложения, а также сбор и анализ полученных в данных для улучшения системы машинного обучения.

Об авторах:

Ян Гудфеллоу (Ian Goodfellow), исследователь, работающий в компании OpenAI. Изобрел различные алгоритмы машинного обучения, в т.ч. порождающие состязательные сети, и внес вклад в различные программы машинного обучения, включая библиотеки TensorFlow и Theano.

Иошуа Бенджио (Yoshua Bengio), профессор факультета информатики и исследования операций, директор Монреальского института алгоритмов обучения. Основная цель его исследований – понять те принципы обучения, которые порождают интеллект. Читает курс по машинному обучению и руководит большой группой студентов и аспирантов. Редактор журнала Journal of Machine Learning Research, заместитель редактора журнала Neural Computation. Участвовал в организации различных конференций, семинаров и симпозиумов по машинному обучению.

Аарон Курвилль (Aaron Courville), доцент факультета информатики и исследования операций в Монреальском университете, член Монреальского института алгоритмов обучения (MILA).

Промокод на скидку в 15%: OTUS15

→ Перейти к книге

Дата выхода: декабрь 2019 года

Глубокое обучение поисковых систем решает самые сложные задачи, в частности позволяет получать релевантные результаты при неточных условиях поиска и плохо проиндексированных данных, извлекать изображения с минимальными метаданными. С помощью таких современных инструментов, как DL4J и TensorFlow, вы сможете применять мощные методы глубокого обучения, не обладая специальными знаниями в области науки о данных или обработки естественного языка. Книга покажет вам, как это сделать. Вы узнаете, как глубокое обучение связано с основами поиска, такими как индексация и ранжирование, и изучите подробные примеры, позволяющие улучшить поиск, используя библиотеки Apache Lucene и Deeplearning4j. В ходе чтения вы освоите сложные темы: поиск по изображениям, перевод пользовательских запросов, проектирование поисковых систем, совершенствуемых по мере обучения.

Кратко о темах:

  • генерация синонимов;
  • точное и релевантное ранжирование;
  • поиск по языкам;
  • поиск изображений на базе содержимого;
  • поиск с использованием рекомендательных систем.

Об авторе:

Томмазо Теофили – инженер-программист, работающий с открытым исходным кодом и искусственным интеллектом. Он состоит в организации Apache Software Foundation и участвует в проектах по поиску информации, обработке естественного языка и распределенным вычислениям

Сборщик мусора выделяет сегменты кучи, в которых каждый сегмент является непрерывным диапазоном памяти. Объекты, помещенные в кучу, делятся на три поколения: 0, 1 или 2. Поколение определяет частоту, с которой сборщик мусора пытается освободить память на управляемых объектах, на которые больше не ссылается приложение. Более низкие номера поколений чаще всего являются GC.

Объекты перемещаются из одного поколения в другое в зависимости от времени их существования. Как только объекты проводятся дольше, они перемещаются в более высокое поколение. Как упоминалось ранее, более высокие поколения являются сборщиком мусора менее часто. Кратковременные объекты с краткосрочным временем существования всегда остаются в поколении 0. Например, кратковременно используются объекты, на которые имеются ссылки в течение жизненного цикла веб-запроса. Одноэлементные экземпляры уровня приложения обычно переносятся в поколение 2.

  • Резервирует некоторый объем памяти для начальных сегментов кучи.
  • Фиксирует небольшую часть памяти при загрузке среды выполнения.

Предыдущие выделения памяти выполняются по соображениям производительности. Преимущество производительности состоит из сегментов кучи в непрерывной памяти.

Вызовите GC. Получение

Вызов GC. Собирайте явным образом:

Анализ использования памяти приложением

Специальные средства могут помочь в анализе использования памяти:

  • Подсчет ссылок на объекты
  • Измерение степени влияния сборки мусора на использование ЦП
  • Измерение пространства памяти, используемого для каждого поколения

Используйте следующие средства для анализа использования памяти.

Обнаружение проблем с памятью

Если значение памяти диспетчера задач постоянно увеличивается и никогда не происходит, в приложении возникает утечка памяти. В следующих разделах показаны и описаны некоторые закономерности использования памяти.

Пример приложения для просмотра использования памяти

Пример приложения меморилеак доступен на GitHub. Приложение Меморилеак:

Запустите Меморилеак. Выделенная память медленно увеличивается, пока не произойдет GC. Память увеличивается, поскольку средство выделяет пользовательский объект для записи данных. На следующем рисунке показана страница индекса Меморилеак при возникновении сборки мусора Gen 0. На диаграмме показано 0 RPS (запросов в секунду), так как не были вызваны конечные точки API из контроллера API.

Предыдущая диаграмма

На диаграмме показаны два значения использования памяти:

  • Выделение: объем памяти, занятой управляемыми объектами : набор страниц в виртуальном адресном пространстве процесса, которые в настоящий момент находятся в физической памяти. Отображаемый рабочий набор имеет то же значение, что и диспетчер задач.

Временные объекты

Следующая диаграмма создается с относительно небольшой нагрузкой в, чтобы продемонстрировать, как это влияет на выделение памяти сборщиком мусора.

Предыдущая диаграмма

На предыдущей диаграмме показано следующее:

  • 4 КБ (запросов в секунду).
  • Коллекции GC поколения 0 происходят каждые две секунды.
  • Рабочий набор является константой приблизительно 500 МБ.
  • ЦП равен 12%.
  • Потребление памяти и выпуск (через GC) стабильны.

На следующей диаграмме приведена максимальная пропускная способность, которую может обработать компьютер.

Предыдущая диаграмма

На предыдущей диаграмме показано следующее:

  • 22K RPS
  • Коллекции GC поколения 0 встречаются несколько раз в секунду.
  • Сборки поколения 1 запускаются, так как приложение выделяет значительно больше памяти в секунду.
  • Рабочий набор является константой приблизительно 500 МБ.
  • ЦП равен 33%.
  • Потребление памяти и выпуск (через GC) стабильны.
  • ЦП (33%) не чрезмерно используется, поэтому сборка мусора может повести себя с большим количеством выделений.

Сборщик мусора рабочей станции и сервер GC

Режим GC можно задать явно в файле проекта или в runtimeconfig.js файла опубликованного приложения. В следующей разметке показан параметр ServerGarbageCollection в файле проекта:

ServerGarbageCollection Для изменения в файле проекта требуется Перестроение приложения.

Примечание. Сборка мусора сервера недоступна на компьютерах с одним ядром. Дополнительные сведения см. в разделе IsServerGC.

На следующем рисунке показан профиль памяти в 5 КБ RPS с помощью сборщика мусора рабочей станции.

Предыдущая диаграмма

Различия между этой диаграммой и версией сервера существенны:

  • Рабочий набор удаляется с 500 МБ до 70 МБ.
  • Сборщик мусора выделает коллекции поколения 0 несколько раз в секунду, а не каждые две секунды.
  • GC перепадает с 300 МБ на 10 МБ.

В типичной среде веб-сервера загрузка ЦП важнее, чем память, поэтому сервер GC лучше. Если интенсивность использования памяти высока и загрузка ЦП относительно низкая, то сборщик мусора рабочей станции может быть более производительным. Например, высокая плотность размещения нескольких веб-приложений, где недостаточно памяти.

GC с использованием DOCKER и небольших контейнеров

Если на одном компьютере выполняется несколько контейнерных приложений, сборщик мусора рабочей станции может быть более предустановлен, чем сервер GC. Дополнительные сведения см. в разделе Работа с GC сервера в небольшом контейнере и Работа с GC сервера в небольшом сценарии, часть 1 — жесткое ограничение для кучи сборщика мусора.

Постоянные ссылки на объекты

Сборщику мусора не удается освободить объекты, на которые имеются ссылки. Объекты, на которые имеются ссылки, но которые больше не нужны, приводят к утечке памяти. Если приложение часто выделяет объекты и не может освободить их после того, как они больше не нужны, использование памяти увеличится со временем.

Следующий API создает экземпляр строки размером 10 КБ и возвращает его клиенту. Разница с предыдущим примером заключается в том, что на этот экземпляр ссылается статический член, что означает, что он никогда не доступен для коллекции.

  • — Пример типичной утечки памяти.
  • При частом вызове происходит увеличение объема памяти приложения до аварийного завершения процесса с OutOfMemory исключением.

Предыдущая диаграмма

На предыдущем рисунке:

  • Нагрузочное тестирование /api/staticstring конечной точки вызывает линейное увеличение памяти.
  • Сборщик мусора пытается освободить память по мере роста нехватки памяти путем вызова сборки поколения 2.
  • Сборщику мусора не удается освободить утечку памяти. Увеличение выделенного и рабочего набора с учетом времени.

В некоторых сценариях, таких как кэширование, требуется, чтобы ссылки на объекты удерживались до тех пор, пока не будет принудительно снята нагрузка на память. WeakReferenceКласс может использоваться для этого типа кода кэширования. WeakReference Объект собирается при нехватке памяти. Используемая по умолчанию реализация IMemoryCache WeakReference .

Собственная память

Рассмотрим следующий код.

Фисикалфилепровидер является управляемым классом, поэтому в конце запроса будет собираться любой экземпляр.

На следующем рисунке показан профиль памяти при постоянном вызове fileprovider API.

Предыдущая диаграмма

На предыдущей диаграмме показана очевидная проблема с реализацией этого класса, так как она обеспечивает увеличение использования памяти. Это известная проблема, которая отслеживается в этой проблеме.

Такая же утечка может произойти в пользовательском коде одним из следующих:

  • Не освобождайте класс должным образом.
  • Не Dispose удается вызвать метод зависимых объектов, которые должны быть удалены.

Куча больших объектов

Частые циклы выделения и освобождения памяти могут фрагментировать память, особенно при выделении больших блоков памяти. Объекты выделяются в смежных блоках памяти. Чтобы устранить фрагментацию, когда сборщик мусора освобождает память, он пытается выполнить дефрагментацию. Этот процесс называется сжатием. Сжатие включает в себя перемещение объектов. Перемещение больших объектов накладывает снижение производительности. По этой причине сборщик мусора создает специальную зону памяти для больших объектов, которая называется кучей больших объектов (LOH). Объекты размером более 85 000 байт (примерно 83 КБ):

  • Размещается в LOH.
  • Не сжато.
  • Собраны во время сборки мусора поколения 2.

Когда LOH заполнена, сборщик мусора запускает сборку поколения 2. Коллекции поколения 2:

  • По сути являются медленными.
  • Кроме того, следует взимать затраты на активацию коллекции во всех других поколениях.

Следующий код немедленно сжимает LOH:

LargeObjectHeapCompactionModeДополнительные сведения о сжатии LOH см. в разделе.

Следующий API демонстрирует такое поведение:

На следующей диаграмме показан профиль памяти для вызова /api/loh/84975 конечной точки в разделе максимальная нагрузка:

Предыдущая диаграмма

На следующей диаграмме показан профиль памяти для вызова /api/loh/84976 конечной точки, который выделяет еще один байт:

Предыдущая диаграмма

Примечание. в byte[] структуре содержится дополнительная нагрузка на байты. Вот почему 84 976 байта вызывает ограничение 85 000.

Сравнение двух предыдущих диаграмм:

  • Рабочий набор аналогичен обоим сценариям, примерно 450 МБ.
  • В разделе запросы LOH (84 975 байт) показаны в основном коллекции поколения 0.
  • При чрезмерном запросе LOH генерируются коллекции констант поколения 2. Коллекции поколения 2 являются дорогостоящими. Требуется больше ресурсов ЦП, и пропускная способность падает почти на 50%.

Временные крупные объекты особенно проблематичны, так как они вызывают Gen2 GC.

Дополнительные сведения см. в разделе:

  • Больше, чем память.
  • Более проблематично, если утечка по сравнению с памятью.

Использование пулов объектов

Объединение объектов в пул:

  • Использует шаблон повторного использования.
  • Предназначен для объектов, которые могут создаваться дорого.

Пул — это коллекция предварительно инициализированных объектов, которые можно зарезервировать и освободить в потоках. Пулы могут определять правила распределения, такие как ограничения, предопределенные размеры или скорость роста.

пакет NuGet Microsoft. extensions. обжектпул содержит классы, помогающие управлять такими пулами.

Следующая Конечная точка API создает экземпляр byte буфера, который заполняется случайными числами для каждого запроса:

На следующей диаграмме показан вызов предыдущего API с умеренной нагрузкой:

Предыдущая диаграмма

На приведенной выше диаграмме сборки поколения 0 происходят приблизительно в секунду.

Предыдущий код можно оптимизировать путем объединения byte буфера с помощью <T> аррайпул. Статический экземпляр повторно используется в запросах.

В отличие от этого подхода, объект poold возвращается из API. Это означает:

  • Объект находится вне элемента управления, как только вы вернетесь из метода.
  • Вы не можете освободить объект.

Чтобы настроить удаление объекта, выполните следующие действия.

Применение той же нагрузки, что и версия без пула, приводит к следующей диаграмме:

Предыдущая диаграмма

Основное различие состоит в том, что выделяется байты, и в итоге число сборок поколения 0 будет значительно меньше.



Преимущества:

  • Книга слишком подробна и не годится для быстрого получения справки: нужно читать весь посвященный нужной теме раздел.
  • Устаревание информации: раз в 1 – 2 года технология меняется и книга требует обновления.


Преимущества:

Как использовать параллелизм, как написать асинхронный код или многопоточное приложение? Если вы задаетесь этим вопросами при старте нового проекта, эту книгу вам стоит прочесть.

Эта практическая поваренная книга изменит ваше мнение. В более чем 75 богатых примерами кода рецептах автор демонстрирует методы параллельной обработки и асинхронного программирования.


Преимущества:

В книге обсуждаются действительно сложные и интересные темы

  • async и await для асинхронных операций.
  • Параллельное программирование с помощью библиотеки Task Parallel Library.
  • Библиотека TPL Dataflow для создания конвейеров потоков данных.
  • Возможности, которые Reactive Extensions создает поверх LINQ.
  • Юнит-тестирование с параллельным кодом.
  • Сценарии взаимодействия для комбинирования параллельных подходов.
  • Неизменяемые потокобезопасные коллекции производителей/потребителей.
  • Поддержка отмены в параллельном коде.
  • Асинхронное объектно-ориентированное программирование.
  • Синхронизация потоков для доступа к данным.


Преимущества:

  • Год издания.
  • Книга не подходит новичкам.

Развивайте навыки программирования, изучая такие важные темы, как внедрение TDD и BDD и проектирование API для преодоления неэффективности кода, избыточности и других проблем.


Преимущества:

  • Книга учит писать код, который чисто интегрируется с другими системами.
  • С ее помощью можно понять, как принципы и стандарты кодирования повышают качество программного обеспечения.
  • Автор рассказывает, как избежать распространенных ошибок при реализации параллелизма или многопоточности.


Преимущества:


Преимущества:

  • Описана разработка приложений для серверов на Windows и других ОС.
  • Разобрано конфигурирование приложений.
  • Разобрано создание пользовательских компонентов.
  • Есть информация по ведению журналов, тестированию и безопасности.

8. Steven van Deurser & Mark Seemann, «Dependency Injection Principles, Practices, and Patterns»


Преимущества:

Книга затрагивает важные темы

Понимание внутренней работы и методов управления памятью в .NET поможет вам избежать широкого спектра проблем с производительностью и масштабируемостью программного обеспечения. "Pro .NET Memory Management" – это полное руководство по написанию лучшего программного обеспечения.


Преимущества:

10. David Thomas & Andrew Hunt, «The Pragmatic Programmer: your journey to mastery, 20th Anniversary Edition, 2nd Edition»

Двадцать лет спустя после первого выпуска новое издание рассматривает, что значит быть современным программистом. Темы варьируются от личной ответственности и карьерного роста, до архитектурных приемов, позволяющих сделать ваш гибким, легко адаптируемым и повторно используемым.


Преимущества:

  • Книга обучает борьбе с «гниением» программного обеспечения.
  • Дает возможность постоянно учиться и позволяет избежать ловушки дублирования знаний.
  • Учит писать гибкий, динамичный и адаптируемый код.
  • Учит использовать возможности базовых инструментов.

Читайте также: