おきしみみ (oxij) wrote,
おきしみみ
oxij

Страшное программирование: управление ресурсами

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

Вообще, я считаю, что все утверждения бывают либо ложными, либо очевидными (либо я ещё не понял, что же тут написано, ок). В внутрискобочном случае, если сегодня я не понимаю почему прочитанное не ложно, но и не очевидно, то я добавляю текст в группу закладок браузера, которые я перечитываю round-robinом. Эта группа закладок сейчас содержит ~400 элементов, а повторное чтение через полгода-год обычно приносит таки ощущение очевидности и утверждение из начала параграфа также остаётся очевидным.

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

Мне как-то особенно не с кем делиться своими сумасшедшими математико-функциональными идеями. Было бы клёво иметь брата-близнеца, который бы интересовался какой-то сильно смежной темой, и которому я не стеснялся бы вываливать все свои глупые недо-мысли, и который понимал бы, что я имею ввиду до того, как форма мысли упростится до капитанского состояния, от неё отвалится вся абстрактная бредятина, и она перестанет быть такой захватывающей, как в первые секунды после прихода в голову. Ха-ха. У меня роль своего брата-близнеца пока получается так-себе, но я стараюсь.

Зато существует некто ivansorokin, с которым мы частенько встречаемся на ддр-паде или в метро по дороге к таковому. И, видимо, кроме того, что эту светлую голову иногда посещают какие-то очень похожие (только в императивном базисе) на иногда посещающие меня мысли. Эта светлая голова ещё и решает более прикладные задачи, чем моя, потому поболтать о всяких инженерных вещах бывает очень весело. И, да, голова всё пишет на хорошем C++ (с бустом и гимназистками), а моя для себя — на Haskell, а остальное — на чём придётся.
Результат обмена опытом обычно попадает в одну из следующих категорий:
* идея, дующая из очередного драфта C++xx, после преобразования базиса, превращается в знакомые функциональные алгебраические структуры (что не удивительно, но весело. например, сравните r-value ссылки с move-семантикой и линейные типы, концепты и тайпклассы);
* я узнаю что-то интересное из истории развития фич в плюсах;
* находится что-то, аналогов чему в знакомых теориях я не знаю, но о подходах к реализации чего можно поспорить.
Обмен опытом в обратную сторону идёт с большим трудом, ибо рассказывать о том куда ещё можно воткнуть себе какие-нибудь комонады, чтобы стало веселее, как-то грустновато, зато я могу рассказать о куче глупостей из области операционных систем.

Один из самых частых топиков болтовни — всякие частные случаи, обработка исключительных ситуаций, «правильный и красивый» менеджмент ресурсов и тому подобное.
На этой почве я тут внезапно осознал, что программирование — это черезвычайно хитровывернутая область. Сейчас я хорошенько покапитаню.
* Мы (люди) не умеем хорошо программировать синхронные системы, а замахиваемся на асинхронные.
* Не можем поработить одноядерный ЦПУ, а замахиваемся на многоядерные. Рассуждаем о масштабируемости различных классов задач на «более 48 ядер», а даже для одного ядра написать компилятор, адекватно минимизирующий кеш-миссы не способны.
* Не можем оседлать самый простой метод менеджмента ресурсов — RAII, а вовсю используем сборщики мусора и прочие «высокоуровневые» механизмы.
* И так далее, и тому подобное.

По поводу предпоследнего мне пришёл в голову простой, капитанский, но забавный пример.

RAII придумали, в том числе, чтобы явно не вызывать деструкторы, освобождающие ресурсы. Так? Ну логично, сконструировал — занял у системы чего-то, собрался помирать — отдай.
Мега кусок а-ля C++/Python кода:
foo = connectSocket("tcp:127.0.0.1:1234");
/* вторая строчка */
bar = socket2Iterator(foo);
foreach (elem in bar) {
    cout << elem;
}


Пусть тут socket2Iterator берёт сокет, и делает из него что-то итератороподобное (например, генерирует ленивый стрим строк, считанных из сокета).
Очевидно, что RAII тут — крутая тема. Если при конструировании что-то сломалось, то объекты будут деструктурированы в нужном порядке. Все счастливы.

А что, если в цикле, при выковыривании очередного элемента из bar, сокету foo станет плохо и система отберёт от него этот ресурс (принудительно закроет сокет)? Адекватных варианта два:
* эксепшн, закатили стек вверх до обработчика, он, если ему надо, повторил операцию с начала, или сделал что-то более адекватное на своё усмотрение;
* делимитед континюейшн до обработчика, обработчик пытается восставновить валидное состояние сокета (реконнект или что там надо), чтобы код мог ехать дальше, не заметив ошибки, получилось — вернули управление назад, едем дальше, не получилось — сворачиваемся.

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

Аналогичная ситуация _всегда_ возникает в деструкторе bar (bar всегда деструктурируют до foo), если foo стало плохо. Не вызывать деструктор bar вроде как нельзя, оно может само занимать какие-то динамически выделенные ресурсы, но что делать, если этот деструктор обратится к невалидному foo?

А что, если освобождение ресурса может свалиться с ошибкой «попробуйте позже»? А оставлять ресурсы занятыми нельзя.
А что, если объекты можно создавать не только последовательно, а у них там какие-то весёлые графы зависимостей друг от друга?

И это я умалчиваю о всяких интересных проблемах, связанных с сохранением инвариантов при эксепшонах/континюейшонах, которые, впрочем, можно решать более-менее адекватными методами.

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

Не то, чтобы я утверждал, что всё описанное нерешабельно или черезвычайно сложно. К чему это я капитаню? Забавно же. Хотели очень простую ресурсную управлялку, где у объектов бывает только валидное состояние или вываливаемся в обработчик ошибок, вроде всё хорошо делали, инварианты сохранять научились, а при первой же работе с внешним ресурсом обломались.
«Ладно», — говорим мы и выбираем способ менеджмента покруче. Но тут оказывается регионы и сборщики мусора вносят недетерминизм во время жизни объектов, что не всегда можно делать (потому, например в Java, приходится, по сути, явно вызвать деструкторы у всяких там сокетов, что ещё веселее: объект в памяти ещё зачем-то есть, а ресурса системы уже нет). Остаётся ручное управление? Привет, 70-тые года.

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

Тут одни умные дядьки подумали, и решили, что надо всё верифицировать, а другие решили, что срочно пора изобретать Haskell и явно разруливать побочные эффекты. Окей, теперь с «чистым» кодом проблем нет, да ещё можно автоматом его параллелить. Только вот, если потерей производительности на иммутабельных структурах данных можно пожертвовать, винчестер можно превратить в придаток оперативной памяти, а на монитор выводить последовательность проекций чистой функции, рисующей UI, то, клавиатура и сетевая карта всё-равно никуда не делись, и управлять ими как-то надо (если только не начать считать всю сеть однородным компьютером, что, по очевидным причинам, вряд ли произойдёт).

Для каждого конкретного случая съедобное решение придумать можно, общего никто не знает. Известный учёный муж Conal Elliott много пишет, придумывает функциональное-реактивное программирование, об описанной проблеме говорит, но ничего конкретного не предлагает. Все чего-то такое говорят, что всё сложно, а ничего конкретного не предлагают. Я в этом усматриваю какую-то несправедливость.
Жизнь такая очень интересная штука (если P ≠ NP, иначе — не такая уж и интересная). Где даже в самых мутных областях есть хоть какой-то заметный прогресс. Например, говорят, что продуктивность программистов удваивается каждые шесть лет (я, например, где-то читал, что первый компилятор Фортрана съел аж 50 человеколет, а сегодня подобный проект дают в качестве (не самой сложной) курсовой работы). Инструменты всё круче, мощнее, веселее. А управление ресурсами в каменном веке. Печаль.
Tags: programming
Subscribe
  • Post a new comment

    Error

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 7 comments