Русский
Русский
English
Статистика
Реклама

Программирование

Перевод Загрузка операционной системы с виниловой пластинки

25.11.2020 12:06:49 | Автор: admin
Большинство компьютеров загружаются с встроенного накопителя. Это может быть обычный жёсткий диск или SSD. Иногда они загружают ОС из сети, или, в крайнем случае, если загружаться больше неоткуда, с USB-флешки или с DVD. Как по мне так всё это скука смертная. Как насчёт загрузки ОС с виниловой пластинки?


10-дюймовая пластинка, время проигрывания которой составляет 6 минут 10 секунд при скорости 45 оборотов в минуту это загрузочный диск DOS размером 64512 байт

Для проведения этого необычного эксперимента персональный компьютер (а точнее IBM PC) подключён к проигрывателю виниловых пластинок через усилитель. Тут имеется маленький ROM-загрузчик, управляющий встроенным кассетным интерфейсом PC (который, пожалуй, никогда и никем не используется). Этот загрузчик вызывается BIOS в том случае, если все остальные способы загрузки не сработали (то есть загрузка с дискеты и с жёсткого диска). Проигрыватель воспроизводит аналоговую запись содержимого небольшого RAM-диска, предназначенную только для чтения, размер которой составляет 64 Кб. В этой записи имеется ядро FreeDOS, модифицированное мной так, чтобы его размер уложился бы в существующие ограничения. Здесь же есть компактный вариант COMMAND.COM и пропатченная версия INTERLNK, которая позволяет передавать файлы по принтерному кабелю и переделана так, чтобы она работала бы в FreeDOS. Загрузчик читает образ диска с пластинки через кассетный модем, записывает образ в память и загружает с его использованием ОС. Полагаю, не так уж всё это и сложно.


Виниловый загрузчик в ROM (он ещё может быть записан на жёсткий диск или на дискету, но это уже будет нечестно)

Если немного углубиться в технические детали, то окажется, что перед нами некий симбиоз BootLPT/86 и 5150CAXX без поддержки порта принтера. Он тоже хранится в ROM, в слоте расширения BIOS, но это необязательно. Для подключения усилителя к компьютеру используется кабель, аналогичный тому, что применяется в 5150CAXX, но тут не используется передача данных от компьютера к подключённому к нему устройству.

Кассетный интерфейс это всего лишь выход, представленный каналом 2 таймера динамика PC и вход, который представлен 4 каналом порта C 8255A-5 PPI (PC4, I/O-порт 62h, бит 4). Для программной (де)модуляции используются возможности BIOS INT 15h.

Загрузочный образ это тот же 64-килобайтный образ RAM-диска BOOTDISK.IMG, ссылку на загрузку которого можно найти здесь. Данные образа, с использованием 5150CAXX, преобразуются в вид, совместимый с протоколом IBM cassette tape, а получаемый аудиосигнал уходит прямо в систему записи виниловых пластинок.

Запись осуществляется с использованием кривой выравнивания RIAA, которую предварительный усилитель обычно обращает в процессе воспроизведения звука. Но делает он это не идеально. А значит на усилителе нужно выполнить коррекцию сигнала. Именно поэтому я и воспользовался усилителем, так как мне не удалось получить нужный сигнал, подав звук на компьютер сразу от предусилителя. В моём случае, используя винтажный усилитель Harman&Kardon 6300 и интегрированный предусилитель MM Phono, мне пришлось убавить высокие частоты (-10дБ/10кГц), поднять басы (+6дБ/50Гц) и уменьшить уровень громкости до получения пиков примерно в 0,7 вольта, что позволило предотвратить искажения звука. Всё это делалось, конечно, при отключённой коррекции фазы и громкости.

Безусловно, кассетному модему совершенно наплевать на то, откуда именно приходит сигнал. При этом, конечно, важно, чтобы запись была бы чистой, не содержала бы щелчков и треска (винил) или недостатков, связанных с модуляцией или частотой сигнала (магнитная лента). Всё это может прервать поток данных. Правда, звук вполне может немного плавать, скорость воспроизведения может варьироваться в пределах 2-3%. Это не мешает правильной передаче данных.


EPROM-модуль с загрузчиком

Итоги



Загрузка компьютера с проигрывателя виниловых пластинок

Вот и всё! Если кому-то нужен загрузчик, сделанный для чипа 2364 (через адаптер можно использовать и чипы 2764), то его код можно найти здесь. Он рассчитан на работу с IBM 5150 с монохромным дисплеем и с как минимум 512 Кб RAM, что (вот уж совпадение) напоминает компьютер, с которым экспериментирую я. Ссылку на образ загрузочного диска можно найти в этом материале. А вот тот же образ, но уже в звуковом виде.

Приходилось ли вам загружать компьютеры с использованием каких-нибудь необычных способов?



Подробнее..

Внедрение CICD в чем основная задача пайплайна и как сделать лучше жизнь разработчиков

26.11.2020 04:22:27 | Автор: admin


О своём опыте построения пайплайнов, правильных и неправильных подходах к CI/CD, здоровых профессиональных конфликтах и реализации GitOps в неидеальном мире рассказывают спикеры курса Слёрма по CI/CD Тимофей Ларкин и Александр Швалов.


Кто говорит


Тимофей Ларкин
Руководил направлением автоматизации в дирекции BigData компании X5 Retail Group, строил платформу для разработки и хостинга продуктов. Сейчас работает платформенным инженером Тинькофф.


Александр Швалов
Инженер Southbridge, настраивает и сопровождает проекты на Kubernetes. Помогал настраивать CI/CD как для небольших, так и для крупных компаний. Certified Kubernetes Administrator.


Почему тема настройки CI/CD до сих пор остаётся актуальна? Почему до сих пор все не научились это делать?


Т: Я думаю, по той же причине, по которой уже одиннадцать лет существует слово DevOps, но некоторые компании и коллективы только-только запускают так называемую DevOps-трансформацию. По той же причине, что всегда есть отстающие, которые пока в группе Low Performеrs. Не все ещё научились. Не всем это вдруг стало нужно, не все так быстро разрабатывали внутри себя технологическую экспертизу.


То есть дело не в том, что тема сама по себе невозможна для реализации, а в том, что просто кто-то ещё последовательно до неё не дошёл?


Т: Да, всё так.


А: Я добавлю, что есть некоторые компании, стартапы, где два человека, допустим, сидят, и им пока не нужен CI/CD. Может быть, они о нём знают, но пока без него обходятся. И внедрение это должно быть на каком-то этапе, когда проект вырос до точки, где это необходимо. Тогда это принесёт больше плюсов, чем минусов. Когда есть только один талантливый программист, ему намного быстрее будет разрабатывать без CI/CD. Ну, и да, отсталость некоторых проектов. Я сам работаю с клиентами, и у них есть сайты с большой посещаемостью, куда они заходят и в середине рабочего дня вешают заглушку и начинают править код. Прямо на продакшене.


Какие это проекты?


А: Да просто интернет-магазины. Небольшие, естественно. В больших очень критичны простои. Там используют серьёзные технологии. А небольшие может, не слышали, может, им это неинтересно. Мы иногда рассказываем, что и как можно настроить, но без особого энтузиазма воспринимают. Мол, старое работает, и ладно.


До вопроса внедрения мы ещё дойдём. Расскажите о своём опыте построения CI/CD, что это были за проекты?


Т: Я около двух лет назад пришёл в X5 Retail Group как, на тот момент, единственный сотрудник нового подразделения, и на мне висела задача построить платформу для разработки, чтобы разработчикам дирекции больших данных было, где собирать свой код и где его запускать. Там достаточно много разных проектов. Какие-то более будничные: прогнозирование оптимальных цен, оптимальных промо-акций (вроде скучное, но приносит прибыль). И что-то более хайповое, вплоть до проектов по компьютерному зрению.


Технологии были разные. В основном для бэкендеров это Java, для фронтендеров это React. Ну, и для дата-сайентистов Python (Anaconda, Jupyter Notebook и тому подобное). Я должен был создать для них эту самую платформу разработки. То есть, CI/CD-серверы (у нас был GitLab), Kubernetes заставить всё это работать в связке и помочь продуктовым командам начать этим эффективно пользоваться.


Два года назад это началось и? Процесс завершился?


Т: На сегодняшний день я уже в X5 не работаю, но там ребята уже активно развились, очень хорошо освоились. Естественно, дирекция больших данных сейчас и то, что было два года назад это совершенно разные вещи. По количеству рост сотрудников, наверное, раз в 5-6. Ну, это была очень молодая дирекция на тот момент. Она очень быстро росла. И по компетенции, по парку серверов на этой рантайм платформе, по всему прочему рост был на порядки.


И люди тоже очень сильно компетенций набрались. Как я говорил, тогда я был единственным сотрудником отдела, а сейчас там примерно 10 человек. И были какие-то измеряемые успехи. Я воспроизводил исследование Ускоряйся, которое лежит в основе методики ежегодных отчётов State of DevOps, и по результатам этого исследования в масштабах одной только дирекции успехи были.


Александр, какой у вас опыт?


А: У меня опыт построения CI/CD первый был на третьем или на четвёртом потоке Слёрма, где я участвовал в качестве студента. Ведь Слёрм изначально задумывался как средство обучения сотрудников Southbridge, а я работал в Southbridge и как раз на одном из потоков плотно познакомился с CI/CD. До этого у нас было несколько клиентов с не совсем правильными подходами к CI/CD, а на обучении я увидел такой конкретный, цельный пример, и потом он мне очень пригодился.


У нас пришел клиент, ему нужно было мигрировать из Docker Swarm в Kubernetes некий стек и плюс распилить монолит. Там были уже несколько контейнеризованных микросервисов и плюс был монолит, который разработчики пилили и добавляли микросервисы, и вот это все мы заворачивали в Kubernetes. Поэтому туда я взял пример нашего CI/CD из Слёрма. Он простенький, но вполне себе рабочий. И в итоге мы дошли до того, что последние микросервисы разработчики деплоили уже сами, без нашего участия. Мы всё построили и отладили, а дальше они уже сами всё по шаблонам делали.


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


А: Сначала мы адаптировали практически вручную то, что уже было в Docker Swarm микросервисы. Потребовался небольшой допил конфигурации. Плюс, helm-чарты написали первые. Использовался Kubernetes. Helm 2 на тот момент ещё был популярен. Ну и GitLab CI. После этого разработчики задавали множество вопросов, иногда делали не так, как мы задумывали. Мы поправляли их. У нас не было прямо тесной связи, мы иногда приходили и советовали, где как лучше сделать, чтобы работало. Таким путем проб и ошибок пришли к тому, что они теперь без нас отлично справляются.


До этого вы упомянули неправильные подходы. Что это было?


А: В целом неправильный подход для мелких проектов, наверное, оправдан. Потому что там не было CI-инструмента. У нас в курсе будут описаны некоторые уже устоявшиеся инструменты (online, self-hosted-решения). А там было проще некуда: задание по расписанию, git pull из репозитория с кодом. Буквально каждые две минуты он делает git pull. И когда разработчик в нужную ветку пушит изменения, он понимает, что это попадёт на продакшн. Т.е. через вот этот промежуток времени 1-2 минуты скрипт сработал, и всё попало на продакшн. Разработчику не нужно было самому ходить. Это такой небольшой пример CI/CD. Естественно, о тестах говорить не приходилось, всё было на совести разработчика.


Какие проблемы могут возникнуть при таком подходе? Ошибки?


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


Т: Тут еще, понимаете, это очень здорово работает, пока там действительно стартап из двух человек. И один только финансами занимается, а кодит только второй. Зачем ему тогда сильно следить за качеством своего кода? Зачем ему выстроенный CI/CD процесс? Он и так отлично знает, что у него где лежит. Проблемы начинаются, когда такую модель работы переносят на командную разработку, и там есть 2-3-4 сеньора, которые всё отлично знают, но никто не может начать с ними работать, потому что они не столь ответственно относились к качеству кода, не запускали тесты. Вроде и так всё понятно, но всегда тяжело учесть, что придёт человек со стороны а в больших компаниях это постоянно случается которому будет сложно объяснить, что тут вообще происходит. Цель CI/CD не просто автоматизировано допихать код до продакшена, но и следить за его качеством.


Мне кажется, это даже не основная цель пайплайна собрать какой-то артефакт, Docker-образ и задеплоить. В любом случае, пайплайн должен уметь делать это безупречно. Потому что это такая типовая задача, которую мы должны легко делать каждый день с нашим кодом. Но вообще, в процессе разработки продукта полезно иметь здоровый профессиональный конфликт. Это конфликт между разработчиками и инженерами по обеспечению качества. И пайплайн тоже должен стоять на страже качества кода. Чтобы тыкать разработчика носом и говорить: Так, вот тут у тебя код не по стандартам отформатирован. Вот тут линтер ругается, тут юнит-тесты не проходят. Давай откатывай и переделывай.

Если пайплайн просто собирает как-нибудь код и потом деплоит его, мы не знаем, что, собственно, задеплоили. Нормально или фигня. Может, там десять релизов уже, и каждый всё больше ломал наш код.


А как построить такую систему? Нужно, чтобы разработчики договорились о стандартах: мы все согласны, что вот так делаем, а вот так не делаем?


Т: Да, нужно прийти к соглашению, что мы считаем плохим/хорошим, как мы будем делать. Но в целом, это спор ни о чем. Не так важно, до чего вы договоритесь у вас будут табы или пробелы. Это все второстепенно. Выберите уже что-нибудь. Это лучше, чем ничего. В этом плане Golang такой интересный пример. Он не стал давать разработчикам волю самим выбирать, какое будет соглашение о качестве. Он просто сказал: Ок, у нас отступы табами, табы шириной 8 символов. И всё, не колышет. Это встроено прямо в утилиты самого Golang. Есть такое высказывание: Все его ненавидят, но при этом он самый лучший. Потому что хоть какой-то стандарт есть. Встроить проверку соответствия кода выбранным стандартам в пайплайн это буквально две строчки.


Как обеспечить контроль выполнения стандартов?


Т: Мне не приходит на ум ни один язык, где нельзя какие-то юнит-тесты сделать. Как правило, они делаются обычно чуть ли не средствами самого компилятора. Это одна строчка. Если нет юнит-тестов, когда первый будет написан, он автоматически будет выполняться. Потом, если это компилируемый код, тогда сам факт, что компиляция прошла успешно, тоже о чём-то да говорит.


Значит, есть несколько таких ступенек?


Т: Да. Всегда принцип 20/80. 20% усилий дают 80% успеха.


А: Тут упомянули проверку кода. Это достаточно важно, и многие инструменты содержат в своём названии как раз CI. Travis CI, GitLab CI. И это очень важно проверить и программиста носом натыкать, это экономия труда тестировщиков. Может, потом код скомпилируется, запустится, даже будет первое время выглядеть нормально, но потом тестировщик найдёт ошибку. А если это сделает автоматика, это намного быстрее и экономия труда, половины рабочего дня тестировщика.


Вы сказали программиста носом натыкать. Это тот самый конфликт между разработкой и QA, о котором говорил Тимофей?


А: Могу привести свой пример, поскольку работаю с CI/CD каждый день, но не с кодом, а с конфигурациями. У нас есть линтеры лишний пробел поставил, он уже ругается. Иногда злишься, ведь это просто формальность, ни на что не повлияет, но это приучает к качеству. И стандарты эти нужны для обеспечения командной работы в том числе. Чтобы новый человек пришел и быстро разобрался.


Получается, весь процесс настройки CI/CD как бы против программиста. Разработчик раньше был этаким свободным художником, а теперь приходит DevOps-инженер и говорит ему, как работать.


А: Можно провести аналогию с конвейером. Если норма у рабочего на конвейере пять деталек в час, а он такой стахановец и делает десять. Это уже может быть непредсказуемость, если мы всегда будем считать, что он делает десять, хотя норма пять. Поэтому лучше пусть стабильно делает пять, но качественно. Можно фигачить код без стандартов, он будет работать, но новый человек не въедет быстро. Поэтому в командной работе есть ограничения, чтобы ото всех можно было получить одинаково стабильную работу. А не так, что один вкалывает, а другой сидит.


Т: А если сеньор проверяет код джуна и говорит: Слушай, а тут вот у тебя функция может вернуть исключение, а ты его не проверяешь. Это сеньор тогда против джуна? А что меняется, если то же самое делает автоматика? Что сеньор, что автоматика они против плохого кода, а не против разработчика.


Давайте мы теперь чуть отступим и поговорим про внедрение CI/CD. Как компании и команды приходят к тому, что пора что-то менять? Ошибки слишком частые, порядка нет, что-то ещё?


Т: Это очень по-разному случается. Вот мой опыт в X5 был такой, что была полная поддержка со стороны менеджмента, но при этом есть менеджмент всей дирекции, а есть отдельные продуктовые команды, у которых некое двоевластие. С одной стороны, есть руководитель отдела разработки, который, наверное, ратует за поставку качественного кода, но, с другой стороны, это продуктовая команда, у них есть дедлайны для поставки фич, и не всегда ясно, что поставить в приоритет. Конечно, нет откровенно глупых людей, которые говорят, что весь этот ваш DevOps не важен, нам нужно фичи пилить. Но при этом в разговорах проскакивает моментами. Мол, ну, я не знаю, у меня сейчас есть бюджет на одну ставку, и для меня вывод очевиден, кого я возьму DevOps или ещё одного разработчика. Мне нужно разрабатывать, так что я возьму разработчика.


Как сделать хорошую мину при плохой игре? Поскольку у нас есть такой инфраструктурный отдел, который ещё в какой-то степени DevOps-евангелисты, то есть отдел, которым я руководил в X5, лучшее, что можно сделать, это облегчить командам внедрение этих пайплайнов, этого CI/CD. Обучать людей до тех пор, пока это возможно, но лучше всего что-то сделать за них. Зашаблонизировать типовые действия. Например, даже банально собрать докер-образ и запушить его в репозиторий. Это нужно авторизоваться в docker registry. Рассчитать, с каким тегом мы соберем докер-образ. Это docker build, потом пуш несколько раз. Возможно, если мы хотим кэши, то нужно сначала скачать старый образ, который уже был, использовать его как кэш, потом закачать новый. Это куча рутины, которая может растянуться строчек на пятнадцать, но это всё однотипно.


Если инфраструктурная команда понимает немного в DevOps, она осознаёт, что это её задача. Сделайте Developer Experience получше. Зашаблонизируйте это, чтобы умещалось в три строчки, а не в пятнадцать. Тогда разработчики будут мыслить не категориями: надо спулить образ, собрать, туда-сюда. А категориями: а вот здесь Docker. Просто один блок, модульный такой. И им будет легче. Они тогда смогут абстрагироваться от деталей и больше заниматься своей непосредственной работой. А именно писать код. В этом плане DevOps он в том, чтобы фасилитировать, предоставлять разработчикам возможность лучше делать свою работу.
И таким образом снижать сопротивление.

А: Отвечу на тот же вопрос со своей стороны. У меня пример внедрения не из мира разработки, больше из мира администрирования. У нас в один прекрасный момент сказали: Мы вот подошли к тому, что откладывать нельзя. Базис мы подготовили для вас. Теперь все новые проекты вы будете создавать вместо старого пути по CI/CD. Вот у вас есть шаблончик, создаете в GitLab и работаете с ним. Каждая команда будет вольна его улучшать. Так и внедряли. Достаточно императивно. В некоторых проектах, когда этот путь нёс больше вреда, чем пользы, мы откатывались на старую версию, но сейчас половина проектов работают через CI/CD. Мы управляем конфигурациями серверов через GitLab CI. Точно так же, как у разработчиков, там есть проверки, линтеры.


Раз уж заговорили про администрирование, поговорим про GitOps. Что это такое, и какое значение имеет: это всё-таки хайп или полезность?


А: Мало что могу сказать про GitOps. Мое мнение такое, что это достаточно хайповое слово. В последнее время я много его слышу. Три года назад так хайповали на DevOps, так что GitOps для меня одно из слов, примазавшихся к DevOps. Для меня это больше хайп.


Это вообще о том, что вся правда хранится в Git. Там и управление конфигурациями серверов, Infrastructure-as-Code, и код проекта, который потом на эти сервера поедет, и описание процесса, как мы с исходного кода получаем приложение на серверах. Так это я понимаю. Возможно, Тимофей меня дополнит, исправит.


Т: GitOps, мне кажется, чуть более честное словечко, чем все эти DevOps, DevSecOps и так далее. Потому что оно значит почти буквально то, что и говорит. Это именно ведение своих действий по эксплуатации своего софта, своей инфраструктуры через Git. Вся правда хранится в Git.


Что это на практике значит? Вроде как мы запушили какой-то код в наш репозиторий, автоматически запустился пайплайн. Что-то произошло, допустим, какие-то манифесты отправились в Kubernetes. И вот у нас то, что мы запушили в Git, это теперь то, что находится в Kubernetes. В принципе, это тоже GitOps, хотя не всегда его так называют. В частности, это так называемая push-модель, когда мы информацию из репозитория толкаем на продакшн.


Еще бывает так называемая pull-модель. Когда, если речь идет о Kubernetes, то у нас там какой-то агент стоит, который мониторит изменения в репозиториях, и если видит, что там что-то закомитили, что-то изменилось, то он стягивает эти изменения. Он умеет и Helm, и Kustomize делать, и просто манифесты ямликами подтягивать. И опять же отправляет их в Kubernetes.


Некоторые пропоненты GitOps вовсе закрывают любое редактирование в Kubernetes и позволяют изменять состояние кластера, только подтягивая изменения из Git. Если этого не делать, всегда есть риск, что из-за чьих-то ручных действий состояние кластера и то, что описано в репозитории разъедется. Кто-то там ручками что-то поменял, а в Git ничего не поменялось. Поэтому частенько говорят про GitOps именно в контексте pull-модели, когда, ну, поменял ты что-то в Kubernetes, ничего страшного, потому что автоматика в ближайшие полчаса все равно вернет все, как описано в Git.


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


К примеру, смежный отдел, который управлял Data Lake в X5, быстро пришёл к тому, что у них вся конфигурация из двух сотен машин управлялась через Ansible, а он запускался каждые 30 минут, как пайплайн в GitLab. Это пример более-менее правильного применения GitOps.


Ну, и те инструменты, которые для GitOps используются, они ещё в какой-то мере нам подсвечивают: а у нас задеплоено именно то, что мы хотим? Иногда они не возвращают автоматически состояния, а просто зажигается желтый или красный светофор: Оп, ребята, у вас рассинхронизировалось состояние тут и тут.


Александр, когда вы говорили о том, как строите процесс работы с клиентами в Southbridge, вы упоминали что-то похожее на GitOps. Настройка конфигурации через описание в Git, это не то же самое?


А: В идеальном мире да. Но у софта зачастую очень много настроек. И чтобы в каждой ручечку/переключатель/ползунок поменять, роль управления конфигурацией должна быть очень развесистая, раскидистая. Либо шаблонизированная. Иногда бывает, что настройки просто нет, и мы идем вручную поправляем. Разработка не успевает за эксплуатацией. Потому что иногда задачи бывают критичные. А чтобы всё разработать, у нас просто нет времени. Но в идеальном случае мы делаем именно то, что описал Тимофей.


Тимофей, а подход GitOps учитывает это обстоятельство неидеального мира?


Т: Это тяжело, конечно. Потому что нам надо бизнес делать, бабки зарабатывать. Казалось бы, вот она, ручка, сходи и поменяй, надо же. Есть потребность, а тут какой-то скрипт ещё писать, дорабатывать helm-чарты. Поэтому да, есть соблазн всё быстренько сделать руками.
Чтобы и бизнес-потребности учесть и себе codebase не испортить, можно хотя бы завести тикет, что вот тут вручную настраиваемое значение, но это баг. Мы положим тикет в бэклог, потом обязательно вернемся и допилим тут автоматизацию. Главное не потерять места, где мы руками настраивали.


В чём ключевые преимущества подхода GitOps?


Т: Основное мы достоверно знаем, что у нас запущено в продакшене. Что все четко описано в репозитории, и не возникает сомнений, что кто-то руками поменял.


А: Когда большая команда, это очень важно, что все могут в любой момент посмотреть и увидеть, какая сейчас конфигурация работает на нужном сервере. Потому что если там 1-2 человека, они могут всегда договориться, а когда 15 человек, GitOps сильно экономит время. Не надо ходить, опрашивать, выяснять, откуда ноги растут у несоответствий.


Чем GitOps отличается от подхода Infrastructure-as-Code?


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


Т: Да, там предлагают даже держать отдельно несколько репозиториев. Один это просто сам код приложения, в котором собирается образ или бинарник. Второй репозиторий туда генерируются, допустим, манифесты для Kubernetes. Третий это такой большой репозиторий, который нам рассказывает, что в нашем кластере манифест из этого репозитория за таким-то тегом, из этого такой-то версии. Он уже просто по номерам версий описывает, что вообще в нашем кластере запущено. На это иногда даже автоматику натягивают: изменили приложение, только код, повесили на него новый тег, а там потом автоматически новый манифест запушился в репозиторий с манифестами, и нужная строчка с нужной версией изменилась в том месте репозитория, которое описывает общее состояние кластера.


Это такая идеальная система?


Т: Ну, да.


CI/CD это такая штука, которую один раз настроил и забыл, или это процесс, который нужно поддерживать?


Т: И да, и нет. Когда он только внедряется, не бывает сразу идеально. Когда в X5 я внедрял, то начальник отдела разработки спрашивал у разработчиков, сколько времени в неделю они тратят на обслуживание их CI/CD. Кто-то отвечал: А что там обслуживать? Вот мы настроили и забыли. И какое-то время это работает. Если сделать сходу правильно, можно сделать очень модульные блоки. И потом не сильно трогать нижележащий код, который это делает, а просто появился новый проектик/микросервис, подключили к нему типовые модульные блоки и поехали дальше. Но пока все учатся, что-то допиливается. Приходится на первых порах долго и мучительно внедрять новые хорошие практики в трудно обслуживаемый код. Дальше уже полегче.


А в больших компаниях как обычно происходит? Приходит DevOps, помогает всё это настроить и остается в команде, или разработчики перенимают на себя его функции, а DevOps уходит в другую команду? Или есть отдел DevOps?


Т: Все очень по-разному. У меня в X5 были продуктовые команды очень разных способностей. Какие-то хорошо въезжали в процесс, и редко надо было им помогать. Были более слабые. К ним привязывали инженера, который помогал писать пайплайны. Работал, в некоторой степени, приходящим релиз-инженером в их команде. Но в X5 порядка 9-10 DevOps обслуживали потребности где-то 200 техспециалистов.


Сейчас я работаю в Тинькофф, тоже в платформенной команде, но там масштабы немного другие. Нас 8 человек, а разработчиков тысячи полторы. И невозможно предоставлять каждой команде DevOps как сервис. Поэтому там предполагается, что команда сама добывает эту экспертизу или нанимает себе DevOps. Либо сами разработчики этому обучаются.


А: Моя версия такая, что да, вначале идет активная разработка. Мы программируем пайплайн, что за чем будет идти, потом ситуация может устаканиться. Если в архитектуре проекта нет глобальных изменений, всё может работать год без правок. Но в мире все меняется. Микросервисы могут быть переписаны за две недели. И если их переписали на другом языке программирования, то тесты выкидываем и пишем новые. Поэтому CI/CD тоже требует обслуживания.


Ну и добавлю, что у разработчиков разная компетенция. Кому-то постоянно нужна помощь, а кому-то нет. Вот у нас был пример с одним из клиентов. Мы им настроили базовые CI/CD, всё показали. Через какое-то время они к нам приходят и говорят: А мы вот переписали всё по-своему. Мы только и ответили: Ух ты, молодцы. Они просто взяли все в свои руки.

Подробнее..

Перевод Koin библиотека для внедрения зависимостей, написанная на чистом Kotlin

26.11.2020 16:14:32 | Автор: admin

Как управлять внедрением зависимостей с помощью механизма временной области (scope)

Для будущих студентов курса "Android Developer. Professional" подготовили перевод полезной статьи.

Также приглашаем принять участие в открытом вебинаре на тему "Пишем Gradle plugin"


О чем эта статья

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

Введение

Разработчики ОС Android не рекомендуют использовать внедрение зависимостей (Dependency Injection, DI (англ.)), если в вашем приложении три экрана или меньше. Но если их больше, лучше применить DI.

Популярный способ реализации DI в Android-приложениях основан на фреймворке Dagger. Но он требует глубокого изучения. Одна из лучших альтернатив этому фреймворку Koin, библиотека, написанная на чистом Kotlin.

Если вы уже пользовались Dagger или любой другой библиотекой для DI, то, скорее всего, знаете, насколько важен в этом процессе механизм временной области (scope). Он позволяет определять, в каких случаях нужно получать один и тот же зависимый объект, а в каких новый. Он также помогает освобождать невостребованные ресурсы и память.

Области в Koin

Концепция области в Koin аналогична таковой в Android. Она позволяет, например, ограничить область живучести модели представления (ViewModel) до определенной активности и использовать эту модель во фрагментах, которыми наполняется активность.

Как правило, в Koin три вида временных областей.

  • single (одиночный объект)создается объект, который сохраняется в течение всего периода существования контейнера (аналогично синглтону);

  • factory (фабрика объектов) каждый раз создается новый объект, без сохранения в контейнере (совместное использование невозможно);

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

Одиночный объект(single). Фабрика объектов(factory)Одиночный объект(single). Фабрика объектов(factory)

Область вида single при каждом запросе возвращает один и тот же экземпляр, а factory каждый раз возвращает новый экземпляр.

Настраиваемая область

Стандартные области single и factory в Koin живут в течение жизненного цикла модулей Koin. Однако в реальных сценариях использования требования к внедрению зависимостей будут отличаться.

Зависимости обычно нужны только на определенный период времени. Например, репозиторий OnBoardRepository в Android-приложении требуется только при регистрации пользователя. Как только пользователь авторизуется, удержание этого репозитория в памяти станет лишней тратой ресурсов.

Чтобы добиться нужного поведения в Koin, можно воспользоваться API для работы с временными областями. В модуле Koin можно создать область со строковым квалификатором и объявить зависимости внутри нее с помощью уникальных квалификаторов. Давайте сделаем это шаг за шагом.

Шаг 1

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

creating custom koin scope

Шаг 2

Следующим шагом объявим необходимые зависимости с использованием областей single и factory в соответствии с требованиями проекта. Ключевой момент заключается в присвоении областям уникальных квалификаторов. Вот так:

dependencies inside custom scopes

Шаг 3

Мы закончили настройку в модуле Koin. На этом шаге нам нужно создать область из того компонента, из которого мы импортируем нужные зависимости. Обычно области создаются из Android-компонентов, например Activity,Fragment и т.п.

Чтобы создать область, сначала нам нужно получить существующий экземпляр компонента Koin, а затем вызвать функцию createScope, передав ей идентификатор и имя области.

val stringQualifiedScope = getKoin().createScope(    "ScopeNameID", named("CustomeScope"))

Получив CustomScope как значение параметра имени, Koin будет искать область, которую мы объявили под этим именем в модулях Koin. ScopeNameID это идентификатор, который мы применяем, чтобы отличать одну область от другой. Он используется на внутреннем уровне в качестве ключа для поиска этой области.

Если вы обращаетесь к областям или создаете их из нескольких Android-компонентов, то вместо функции createScope рекомендуется использовать функцию getOrCreateScope. Из названий этих функций очевидно, что они делают.

Шаг 4

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

val sampleClass = stringQualifiedScope.get<SampleClass>(    qualifier = named("scopedName"))

scopedName и factoryName это квалификаторы, которые мы объявили внутри модуля Koin на шаге2.

Шаг 5

Чтобы избавиться от зависимостей, созданных посредством stringQualifiedScope, вчастности sampleclass, необходимо вызвать функцию close. Например, если вы хотите избавиться от созданных в этой области зависимостей при уничтожении активности, то нужно вызвать функцию close в рамках соответствующего метода onDestroy. Вот так:

override fun onDestroy() {    super.onDestroy()    stringQualifiedScope.close()}

Koin-Android

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

Для этого необходимо импортировать библиотеки Koin-Android. Добавьте следующие строки в узел dependencies файла build.gradle уровня приложения:

// Koin for Androidimplementation "org.koin:koin-android:$koin_version"// Koin Android Scope featuresimplementation "org.koin:koin-android-scope:$koin_version"

Модули Koin-Android

Теперь с целью сократить шаблонный код мы хотим, например, автоматически закрывать область в рамках метода onDestroy компонента Android. Это можно сделать путем привязки Koin к импорту зависимостей посредством lifecyclescope.

Для начала необходимо создать в модуле Koin область для зависимостей с компонентами Android. Как это сделать:

val androidModule = module {    scope<SampleActivity> {        scoped { SampleClass() }    }  }

scoping dependency with android activity

Затем нужно выполнить внедрение зависимости в активности при помощи lifecyclescope:

val sampleClass : SampleClass by lifecycleScope.inject()

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

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)    fun onDestroy() {    if (event == Lifecycle.Event.ON_DESTROY) {               scope.close()        }    }}

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

Дополнительные материалы

На этом все. Надеюсь, вы узнали что-то полезное для себя. Спасибо за внимание!


Подробнее о курсе "Android Developer. Professional". Записаться на открытый урок "Пишем Gradle plugin" можно здесь.

Подробнее..

Перевод Понятность ПО самый важный показатель, который вы не отслеживаете

24.11.2020 14:17:12 | Автор: admin

Привет, Хабр! В OTUS открыт набор на новый поток курса "Software Architect", в связи с этим приглашаем всех желающих на онлайн день открытых дверей, в рамках которого наши эксперты подробно расскажут о программе обучения, а также ответят на ваши вопросы.


Основные идеи

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

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

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

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

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

2500 лет назад Гераклит сказал: Все течет, все меняется. Программирование как нельзя лучше подтверждает правильность этого изречения: разработчики ежедневно модифицируют, адаптируют, дорабатывают и даже перерабатывают свои системы. Еще один аспект, который отличает разработку программного обеспечения ото всех остальных направлений деятельности, большая свобода действий, ограниченная лишь техническими возможностями информационных систем.

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

Пример

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

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

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

Устроившись на работу, любой начинающий разработчик осознает, что написать фрагмент кода для решения простой задачи по информатике (или найти ответ на StackOverflow) это совсем не то же самое, что встроить такой фрагмент в большую сложную систему.

Что такое понятность?

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

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

  • Завершенность. Система должна поставляться с определенным набором исходной информации (исходный код, документация и т.д.). У инженера должна быть вся важная информация о системе.

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

  • Открытость. Это такой метод представления программы, который позволяет человеку понять ее в результате чтения кода. Огромное значение здесь имеют согласованность, соглашения о кодировании, форматирование исходного кода, комментарии к коду и подсветка синтаксиса.

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

Понятность среды выполнения

С распространением модели программное обеспечение как услуга (SaaS) и других моделей поставки приложений многие организации предпочитают самостоятельно реализовывать полный жизненный цикл программного обеспечения.

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

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

Наблюдаемость и понятность не одно и то же

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

  1. Предупреждение о неправильном функционировании системы и помощь в выявлении причины ошибки, чтобы восстановить нормальную работу системы.

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

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

С этими проблемами ИТ-индустрия сталкивается с незапамятных времен. А поскольку их экономический эффект вполне очевиден, рынок насыщен эффективными инструментами для решения этих проблем. Однако это не те задачи, которые возложены на плечи разработчиков ПО, и эти средства никогда не предназначались для помощи в разработке.

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

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

Борьба со сложностями

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

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

Всем, занятым в индустрии программного обеспечения, известно, что системы должны быть максимально простыми. Чем сложнее ПО, тем затратнее разработка и внедрение обновлений и ниже качество системы в целом. Уже написано много руководств по разработке приложений с минимальным уровнем сложности и эффективному масштабированию систем и команд разработчиков.

Понятность нового ПО

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

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

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

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

Понятность существующего ПО

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

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

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

Заключение

Мы получили более широкое представление о том, насколько важно обеспечивать понятность кода при разработке ПО. С одной стороны, мы всегда знали, что код должен быть читаемым и простым в использовании, это одна из важнейших задач разработчиков. И тем не менее сейчас становится все более очевидным, что именно это свойство кода позволяет приложениям эволюционировать.

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


Подробнее о курсе "Software Architect". Посмотреть открытый урок по теме "Мониторинг и Алертинг" можно здесь.


Читать ещё:

Подробнее..

Перевод Базовые концепции Unity для программистов

24.11.2020 16:19:42 | Автор: admin
Привет, Хабр! При проработке темы Unity мы нашли интересный блог, возможно, заслуживающий вашего более пристального внимания. Предлагаем вам перевод статьи о базовых концепциях Unity, также опубликованный на портале Medium


Если вы обладаете опытом программирования и пытаетесь вкатиться в разработку игр, порой бывает непросто найти такие учебные материалы, в которых в достаточной степени объясняется необходимый контекст. Вероятно, придется выбирать между материалами, в одних из которых описана парадигма ООП, в других язык C# и концепции Unity, либо сразу начинать с продвинутых руководств; в последнем случае придется самостоятельно дедуктивно выводить базовые концепции.

Поэтому, чтобы отчасти заполнить этот пробел, я решил написать серию статей Unity for Software Engineers. Это первая из них. Статья рассчитана на читателей, имеющих представление о программировании и программной архитектуре, в особенности на тех, кому близок тот же подход к обучению, что и мне: начинать с основ и постепенно идти вверх.

Я начал путь в программировании около 17 лет назад, открыв для себя Game Maker. Многие часы потратил на самостоятельное программирование маленьких игр и инструментов, в процессе всерьез увлекшись программированием.

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

Сцена


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

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



Редактор сцен Unity, в котором загружена задаваемая по умолчанию пустая сцена в режиме 3D. В пустых сценах Unity3D по умолчанию содержатся объекты Main Camera (Главная Камера) и Directional light (Направленный свет).



Пример сцены в редакторе Unity; здесь выделено несколько объектов. Такое представление сцены можно использовать для редактирования уровней в игре.

Каждый игровой объект в Unity должен находиться в сцене.

Игровые объекты


Игровой Объект (в коде GameObject) один из базовых кирпичиков, из которых строится игра.

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

У каждого игрового объекта есть значения положения и поворота. Для метафизических объектов они не имеют значения.

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



Группа объектов, совместно вложенных в сцене и объединенных в пустом объекте Interior_Props, сделано в целях структурирования

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



Группа объектов, вложенных в объект игрок. Здесь мы видим оружие игрока, его аватарку и различные элементы пользовательского интерфейса, отображаемые вокруг игрока

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

Компоненты (и моноповедения)




Объект Warrior с предыдущего скриншота показан над окном Инспектор в интерфейсе Unity. Каждый из проиллюстрированных разделов (напр., Animator, Rigidbody, Collider) это компоненты, слагающие этот объект

Каждый игровой объект состоит из компонентов.

Компонент реализует четко определенный набор поведений, необходимых, чтобы мог выполниться GameObject. Все, благодаря чему объект получается таким, каков он есть это вклад компонентов, из которых он состоит:

  • У единственного видимого элемента машины будет компонент Renderer, который отрисовывает машину и, вероятно, компонент Collider, задающий для нее границы столкновений.
  • Если машина представляет персонажа, то у самого объекта car может быть Player Input Controller (Контроллер ввода от персонажа), принимающий все события, связанные с нажатиями клавиш, и транслирующий их в код, отвечающий за движение машины.

Притом, что можно писать большие и сложные компоненты, где компонент 1 в 1 равен кодируемому объекту (напр., компонент player содержит код, полностью описывающий персонажа, а компонент enemy, в свою очередь, полностью кодирует противника) обычно принято извлекать логику, дробя ее на небольшие обтекаемые кусочки, соответствующие конкретным признакам. Например:

В коде есть MonoBehavior, вездесущий родительский класс для представления компонентов. Большинство невстроенных компонентов будут наследовать от MonoBehavior, который, в свою очередь, наследует от Behavior и Component, соответственно.

  • Все объекты, обладающие здоровьем, будь то Player (Игрок) или Enemy (Враг) могут иметь компонент LivingObject, задающий исходное значение здоровья, принимающий урон и приводящий в исполнение смерть, когда объект умирает.
  • Кроме того, у игрока может быть компонент ввода, контролирующий сообщаемые ему движения, а у врага может быть аналогичный компонент, реализованный при помощи искусственного интеллекта.

На протяжении жизненного цикла компоненты получают различные обратные вызовы, которые в среде Unity именуются Сообщениями. К Сообщениям относятся, в частности, OnEnable/OnDisable, Start, OnDestroy, Update и другие. Если объект реализует метод Update(), то этот метод будет как по волшебству вызываться Unity в каждом кадре игрового цикла, пока объект активен, а заданный компонент действует. Эти методы могут быть помечены private; в таком случае движок Unity все равно будет их вызывать.

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

Ресурсы


Ресурсы это расположенные на диске сущности, из которых состоит игровой проект. К ним относятся сети (модели), текстуры, спрайты, звуки и другие ресурсы.

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



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

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

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

Шаблонные экземпляры


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

Вложенные шаблоны


Начиная с Unity 2018.3, поддерживается вложение шаблонов, чего и следовало ожидать:

  1. Родительский объект с дочерними объектами, представленными в виде шаблонов, сам может быть представлен в виде шаблона. Внутри родительского шаблона дочерний шаблон допускает собственные модификации. В сцене инстанцируется сразу вся иерархия шаблонов, а поверх нее также могут надстраиваться модификации, специфичные для конкретной сцены.
  2. Шаблонный экземпляр, находящийся в сцене и снабженный собственными локальными модификациями, может быть сохранен как самостоятельный ресурс Prefab Variant. Этот вариант представляет собой шаблонный ресурс, наследующий от другого шаблона, поверх которого применены дополнительные модификации.

Эти концепции компонуются: возможен шаблонный вариант вложенного шаблона или, например, шаблонный вариант шаблонного варианта.

Сериализация и десериализация


Все ресурсы, сцены и объекты вашего проекта долговременно сохраняются на диске. При редактировании игры эти объекты загружаются в память, а затем сохраняются обратно на диск с помощью системы сериализации, действующей в Unity. При тестовых прогонах игры объекты и сцены, находящиеся в памяти, загружаются при помощи одной и той же системы сериализации. Эта система также соотносит ресурсы, находящиеся в скомпилированном пакете, с загруженными/выгруженными объектами сцены в памяти.

Поток сериализации/десериализации, действующий в движке Unity, загружает в память ресурсы, расположенные на диске (в вашем проекте: для редактирования или тестового прогона игры, либо в самой игре, при загрузке сцены) и отвечает за сохранение состояния отредактированных вами объектов и компонентов обратно в соответствующие сцены и шаблонные экземпляры.
Следовательно, система сериализации также является ключевым элементом работы с редактором Unity. Чтобы MonoBehavior мог принять ввод при конструировании сцены в ходе ее инициализации, эти поля должны быть сериализованы.

Большинство базовых типов Unity, в частности, GameObject, MonoBehavior и ресурсы поддаются сериализации и могут получать исходные значения при создании прямо из редактора Unity. Публичные поля в вашем MonoBehavior сериализуются по умолчанию (если относятся к сериализуемому типу), а приватные поля для этого сначала нужно пометить атрибутом Unity [SerializeField], и тогда они тоже могут быть сериализованы.


Скриншот игры Chaos Reborn производства Snapshot Games, 2015 год. BY-CC-SA 3.0

Заключение


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

Перевод Секреты производительности Spark, или Почему важна компиляция запросов

24.11.2020 18:14:36 | Автор: admin

Для будущих студентов курсов "Data Engineer" и "Экосистема Hadoop, Spark, Hive" подготовили еще один перевод полезной статьи.


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

Быстрая обработка больших данных имеет критическое значение для нашего бизнеса:

  • мы часто обновляем наши модели, повышая их производительность для наших клиентов;

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

  • от скорости обработки данных зависят затраты на инфраструктуру.

В этой статье я расскажу о написании эффективного кода Spark и на примерах продемонстрирую распространенные подводные камни. Я покажу, что в большинстве случаев Spark SQL (Datasets) следует отдавать предпочтение перед Spark Core API (RDD), и если сделать правильный выбор, можно повысить производительность обработки больших данных в 210 раз, а это очень значимо.

Конфигурация для экспериментов

Spark 2.4.6, Macbook Pro 2017 с процессором Intel Core i7 с частотой 3,5ГГц

Измерения всегда производятся на разогретой виртуальной Java-машине (выполняется 100 прогонов кода, и берется среднее значение за последние 90 прогонов). Приведенный в этой статье код написан на Scala, но ее выводы должны быть справедливыми и для Python.

Заблуждения, связанные с обработкой больших данных

Существует распространенное мнение, что в процессах обработки больших данных есть два основных узких места, влияющих на производительность:

  • перетасовка данных, поскольку для ее выполнения требуется отправлять данные по сети;

  • дисковый ввод-вывод, поскольку доступ к данным на диске всегда намного медленнее, чем доступ к данным в ОЗУ.

Эти представления имеют под собой исторические основания в 2006 году, когда впервые появилась библиотека Hadoop, обычные жесткие диски были медленными и ненадежными, а основной платформой для обработки больших данных была MapReduce. Именно медленная работа жестких дисков и подстегнула разработку таких средств обработки в памяти, как Spark. С того времени характеристики аппаратного обеспечения значительно улучшились.

В 2015 году висследовании Кей Остерхаут (Kay Ousterhout) и др. были проанализированы узкие места в заданиях Spark, и в результате выяснилось, что скорость их выполнения в большей степени определяется операциями, загружающими ЦП, а не вводом-выводом и передачей данных по сети. В частности, авторами этой научной работы был выполнен широкий спектр запросов к трем тестовым наборам данных, включаяTPC-DS, и было определено, что:

  • если бы пропускная способность сети была безграничной, время выполнения заданий можно было бы сократить на 2% (медианное значение);

  • если бы пропускная способность дискового ввода-вывода была безграничной, время выполнения стандартного аналитического процесса можно было бы сократить на 19% (медианное значение).

Весьма неожиданный результат! Получается, что дисковый ввод-вывод оказывает намного большее влияние на производительность, чем передача данных по сети. Этому есть несколько причин:

  • Spark использует дисковый ввод-вывод не только при считывании входного набора данных и записи результата, но и в ходе выполнения заданий для кэширования и переноса на диск данных, которые не умещаются в ОЗУ.

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

Интересно, что специалисты Databricks примерно в 2016 году пришли к таким же заключениям, что заставило их переориентировать вектор развития Spark на оптимизацию использования процессора. Результатом стало внедрение поддержки SQL, а также API DataFrames и позднее Datasets.

Насколько быстро работает Spark?

Давайте рассмотрим простую задачу посчитаем наивным методом четные числа от 0 до 10. Для выполнения такого задания Spark, в принципе, не требуется, поэтому для начала напишем простую программу на Scala:

var res: Long = 0Lvar i: Long  = 0Lwhile (i < 1000L * 1000 * 1000) {  if (i % 2 == 0) res += 1  i += 1L}

Листинг1. Наивный подсчет

А теперь давайте также вычислим этот же результат с помощью Spark RDD и Spark Datasets. Чтобы эксперимент был честным, я запускаю Spark в локальном[1] режиме:

val res = spark.sparkContext  .range(0L, 1000L * 1000 * 1000)  .filter(_ % 2 == 0)  .count()

Листинг2. Подсчет с помощью RDD

val res = spark.range(1000L * 1000 * 1000)  .filter(col("id") % 2 === 0)  .select(count(col("id")))  .first().getAs[Long](0)

Листинг3. Подсчет с помощью Datasets

Время выполнения всех фрагментов кода приведено ниже. Неудивительно, что написанный вручную код является самым эффективным решением. Удивительно же то, что RDD в пять раз медленнее, тогда как у Datasets время вычисления почти такое же, как у написанного вручную кода.

Парадокс Datasets

Парадокс: API-интерфейс Datasets построен на основе RDD, однако работает намного быстрее, почти так же быстро, как код, написанный вручную для конкретной задачи. Как такое вообще возможно? Дело в новой модели выполнения.

Прошлое модель Volcano

Код, написанный с использованием RDD, выполняется с помощью модели выполнения Volcano. На практике это означает, что каждый RDD следует стандартному интерфейсу:

  • знает свой родительский RDD;

  • предоставляет посредством методаcomputeдоступ к итератору Iterator[T], который перебирает элементы данного RDD (он является private и должен использоваться только разработчиками Spark).

abstract class RDD[T: ClassTag]def compute(): Iterator[T]

Листинг4. RDD.scala

С учетом этих свойств упрощенная версия реализации функции подсчета для RDD, которая игнорирует разбиение, выглядит вот так:

def pseudo_rdd_count(rdd: RDD[T]): Long = {  val iter = rdd.compute  var result = 0  while (iter.hasNext) result += 1  result}

Листинг5. Псевдокод для действия подсчета на основе RDD

Почему этот код работает значительно медленнее, чем написанный вручную код, который приведен в листинге 1? Есть несколько причин:

  • Вызовы итераторов виртуальной функцией: вызовы Iterator.next() несут дополнительную нагрузку по сравнению с функциями, не являющимися виртуальными, которые могут выполняться компилятором илиJIT как встроенные (inline).

  • Отсутствие оптимизации на уровне ЦП: виртуальная Java-машина и JIT не могут оптимизировать байт-код, образуемый листингом 5, так же хорошо, как байт-код, получаемый при использовании листинга 1. В частности, написанный вручную код позволяет виртуальной Java-машине и JIT хранить промежуточные результаты вычислений в регистре ЦП, а не помещать их в основную память.

Настоящее формирование кода всего этапа

Код, написанный с помощью Spark SQL, выполняется не так, как код, написанный с использованием RDD. Когда запускается действие, Spark генерирует код, который сворачивает несколько трансформаций данных в одну функцию. Этот процесс называется формированием кода всего этапа (Whole-Stage Code Generation). Spark пытается воспроизвести процесс написания специального кода для конкретной задачи, в котором не используются вызовы виртуальных функций. Такой код может выполняться JVM/JIT более эффективно. На самом деле Spark генерирует довольно много кода, см., например, код Spark для листинга 3.

Технически Spark только формирует высокоуровневый код, а генерация байт-кода выполняется компилятором Janino. Именно это и делает Spark SQL настолько быстрым по сравнению с RDD.

Эффективное использование Spark

Сегодня в Spark есть 3 API-интерфейса Scala/Java: RDD, Datasets и DataFrames (который теперь объединен с Datasets). RDD все еще широко применяется в Spark в частности, из-за того, что этот API используется большинством созданных ранее заданий, и перспектива продолжать в том же духе весьма заманчива. Однако, как показывают тесты, переход на API-интерфейс Datasets может дать громадный прирост производительности за счет оптимизированного использования ЦП.

Неправильный подход классический способ

Самая распространенная проблема, с которой я сталкивался при использовании Spark SQL, это явное переключение на API RDD. Причина состоит в том, что программисту зачастую проще сформулировать вычисление в терминах объектов Java, чем с помощью ограниченного языка Spark SQL:

val res = spark.range(1000L * 1000 * 1000)    .rdd    .filter(_ %2 == 0)    .count()

Листинг6. Переключение с Dataset на RDD

Этот код выполняется в течение 43 секунд вместо исходных 2,1 секунды, при этом делая абсолютно то же самое. Явное переключение на RDD останавливает формирование кода всего этапа и запускает преобразование элементов наборов данных из примитивных типов в объекты Java, что оказывается очень затратным. Если мы сравним схемы этапов выполнения кода из листингов 3 и 6 (см. ниже), то увидим, что во втором случае появляется дополнительный этап.

Рисунок 1. Визуальные представления этапов для листинга 3 (схема a) и листинга 6 (схема b)Рисунок 1. Визуальные представления этапов для листинга 3 (схема a) и листинга 6 (схема b)

Неправильный подход изысканный способ

Производительность Spark SQL является на удивление хрупкой. Это незначительное изменение приводит к увеличению времени выполнения запроса в три раза (до 6 секунд):

val res = spark  .range(1000L * 1000 * 1000)  .filter(x => x % 2 == 0) // note that the condition changed  .select(count(col("id")))   .first()  .getAs[Long](0)

Листинг7. Замена выражения Spark SQL функцией Scala

Spark не способен генерировать эффективный код для условия в фильтре. Условие является анонимной функцией Scala, а не выражением Spark SQL, и Spark выполнит десериализацию каждой записи из оптимизированного внутреннего представления, чтобы вызвать эту функцию. Причем вот что примечательно это изменение никак не сказывается на визуальном представлении этапов (рис. 1a), поэтому его невозможно обнаружить, анализируя направленный ациклический граф (DAG) задания в пользовательском интерфейсе Spark.

Высокая производительность Spark SQL обеспечивается за счет ограничения круга доступных операций чем-то все равно приходится жертвовать! Чтобы получить максимальную производительность, нужно использовать преобразования, которые работают со столбцами: используйтеfilter(condition: Column)вместоfilter(T => Boolean) иselect()вместоmap(). При этом Spark не придется перестраивать объект, представленный одной строкой набора данных (Dataset). И, разумеется, избегайте переключения на RDD.

Заключение и итоговые замечания

Приведенные в этой статье простые примеры демонстрируют, что большая часть времени выполнения заданий обработки больших данных не тратится на полезную работу. Хорошим решением этой проблемы является компиляция запросов, которая возможна с использованием Spark SQL и обеспечивает более эффективное использование современного аппаратного обеспечения. Последние исследования свидетельствуют, что использование эффективных запросов для стандартных процессов обработки больших данных важнее, чем оптимизация использования сети и дискового ввода-вывода.

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

Образцы кода из этой статьи можно найти здесь. С помощью этого репозитория можно анализировать производительность разных запросов Spark.

Использованные материалы

  1. Ousterhout, Kay, et al. Making sense of performance in data analytics frameworks (Анализ производительности платформ анализа данных).12-й симпозиум {USENIX} по проектированию и реализации сетевых систем ({NSDI} 15). 2015.

  2. http://www.tpc.org/tpcds/

  3. https://databricks.com/blog/2015/04/28/project-tungsten-bringing-spark-closer-to-bare-metal.html

4.https://janino-compiler.github.io/janino/

5.http://people.csail.mit.edu/matei/papers/2015/sigmodsparksql.pdf

6.https://databricks.com/blog/2016/05/23/apache-spark-as-a-compiler-joining-a-billion-rows-per-second-on-a-laptop.html


Узнать подробнее о курсах "Data Engineer" и "Экосистема Hadoop, Spark, Hive".

Подробнее..

Я месяц провел в MIT и понял даже софтверным инженерам не стоит забывать про паяльник

24.11.2020 18:14:36 | Автор: admin


В детстве отец иногда доверял мне свой паяльник. Я занимался нехитрой пайкой, но чаще поглядывал, как руки отца то окунали паяльник в канифоль, то цепляли кусочек припоя. Запах канифоли с тех пор всегда будит во мне воспоминания. Кто бы мог подумать, что я буду так же увлеченно паять, но уже не батиным, а навороченным американским паяльником, и не на кухне в квартире детства, а в Массачусетском Технологическом институте.

Что ты ожидаешь от одного из самых крутых университетов мира? Выматывающие лекции и бесконечные формулы на белесых от мела досках? Конечно. Умудренная семи пядей во лбу профессура со всего мира? Обязательно. Но престижный Massachusetts Institute of Technology знаменит не только этим. MIT ценят за близость к индустрии. Буквально в нескольких кварталах от университетских корпусов в Бостоне расположены офисы техногигантов, таких как Google и Microsoft. Студентов учат не только инженерному делу, но и практическому его применению, а также умению хорошо продать свою идею, или найти человека, который продвинет твою инновацию на рынок.

Знаменитый Гарвард находится в двух станциях метро от MIT. Умники гарвардские выпускники, разбирающиеся в бизнесе, просто приходят к тру инженерам и создают новые компании. Это место генерирует новые стартапы каждый день. И я совсем не удивился, когда услышал очередное задание от преподавателя воркшопа. Каждому из нас надо было во что бы то ни стало получить скидку в местных магазинах. Один парень так старался на кассе в сетевом Best buy, что продавщица черкнула ему свой номерок и имя в чеке.

Что ж, это тоже успех.

Санкт-Петербург, весна 2013 года за моими плечами ИТМО и несколько разрабовских работ.


К тому времени совсем молодой российский Сколтех (Сколковский институт науки и технологий) набирает студентов в магистратуру. Обучение на английском, все в лучших традициях MIT, и более того, Сколтех и создан при поддержке Массачусетского технологического института. На должность ректора Сколтеха приглашен профессор MIT Эдвард Кроули. Программу магистратуры предваряет поездка в США месячный Innovation Workshop. Не раздумывая я подаю документы и принимаюсь готовиться к вступительным испытаниям.

Процесс поступления я запомнил следующим образом: собираешь и отправляешь документы, тебя приглашают в Москву, где проводят трехдневный хакатон и собеседования. Там же сдаешь TOEFL. Все получилось так быстро, что американской визой я занимался уже в Питере. Отпечатки пальцев, несколько вопросов за жизнь, подтверждающие документы от Сколтеха и выписка со счета с кругленькой суммой. Зачисление в магистратуру и одобрение визы я не знаю, чему я больше радовался!

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

Перелет в Бостон


занял порядка 10-12 часов. Нас встретили, посадили в типично американские огромные черные джипы и отвезли в общежитие, где нас ждала пицца. Боже, о чем еще может мечтать человек после долгого перелета?

Для изучения города нам устроили scavenger hunt по Бостону. С помощью подсказок мы искали специальные монеты, которые были раскиданы по улицам. Я думаю, это выглядело забавно: группки русских и не только студентов носятся по бостонским закоулкам, ищут монеты и донимают местных бесконечными вопросами. Но нам было весело! В выходные занятий не было, за несколько уикендов мы успели изучить окрестности, покататься на яхтах, выехать на океан и даже смотаться в Нью-Йорк.

Рядом с MIT и Гарвардом есть магазины, где можно купить футболки, кружки и другие вещи с символикой университетов. И все это не для туристов. Студенты искренне гордятся тем, где они учатся. На футболках то и дело мелькают надписи Nerd pride. Бостонцы знают и уважают людей из MIT. В один из вечеров я пробрался на концерт в Бостоне.

На пути обратно я встретил много людей, которые махали мне и кричали: MIT rules и engineers forever.


После такого тебя просто переполняет мотивация учиться дальше!



В будни каждый день был расписан по часам. Мы начинали в 9 утра и заканчивали ближе к семи вечера. Нередко, чтобы успеть всё, преподаватели продолжали занятия за обедом, на лужайке или в кафе. Однажды мы немного устали и закапризничали. Кто-то бурчал мол либо ужин, либо учеба. Тогда кто-то предложил: Давайте совмещать, но только если будет пюрешка.. Лекции во время еды не нравились, но с пюрешкой так уж и быть были согласны. Все горячо поддержали эту идею. И вы знаете, преподаватель на следующий день выкатил нам тазик с пюрешкой! Ох, уж эти странные русские, черт с вами, ешьте свою mashed potato, рассмеялся он, и продолжил занятие. Мы были в восторге.

И в этом весь дух отношений между студентами и преподавателями. Это история про уважение и страсть. Все вокруг пашут, фигачат, и ты тоже начинаешь ускоряться. Когда приходишь к человеку, который помогает тебе составить твой индивидуальный план обучения в MIT и говоришь: Я хочу вот эти курсы и еще вот эти. Он смотрит на тебя с облегчением и выдает: Наконец-то человек, который знает, чего хочет.

В погружении в процесс помогают и стены. Все аудитории доступны, нет никаких охранников. По всей территории MIT прекрасный бесплатный вай-фай. (Честно, я сначала подумал, что во всей Америке так. Каково было мое огорчение, когда я вышел в Бостон!).

Вот неполный список того, что мы изучали за 3 недели:


математика в инновациях, как добиться быстрого успеха, лидерство, интеллектуальная собственность, инновации и дизайн в инженерии, как правильно писать тексты, инновации и производство, разработка и дизайн продукта, этика и вывод продукта на рынок. Лидерство преподавал серьезный мужик со званием из армии США. Услышав его голос, я сразу узнал его: это был тот самый голос из военных американских фильмов.



Помимо занятий с утра до вечера, в свободное время за 3 недели надо было сделать свой собственный проект или продукт. Кто-то разработал автоматизированный полив, наша команда выкатила систему видеонаблюдения для распознавания того, что человек засыпает. Были еще отдельные тимбилдинговые игры по решению какой-то интересной задачи. Например, надо было используя веревки снять крышку с нефтяной скважины. Починить ее и поместить на место. При условии, что приближаться к скважине можно не ближе чем, на 4 метра.

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

Там я снова взял в руки паяльник и увидел вживую всякие роборуки


На одном из занятий Innovation Workshop преподаватель вручил мне паяльник и сказал что-то в духе: Паяй, сынок. Один преподаватель с помощью света передавал звук. Есть проблема с передачей информации со спутников на землю. Провод не бросить, увы. А что если можно пустить луч, огромная штука его примет и все будет работать! Примерно так он объяснил следующие действия: Смотрите, что могу! Он дает что-то типа приемника одному из нас, а сам берет небольшую указку, которая светит еле видимым светом. Наводит на приемник луч и мы слышим музыку Убирает музыка замолкает. Как ты это делаешь, маг?

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



После той поездки кто-то из ребят углубился в науку и учится на PhD в Англии, кто-то ушел в data science, а кто-то открыл свою компанию и запускает спутники в космос. Я свое вдохновение конвертировал в страсть: я детально изучал все что давали в Сколтехе, а что не давали отбирал и изучал. Лаборатория робототехники в Сколтехе сделана по образу и подобию соответствующей лаборатории в MIT. Я еще много провел в ней времени, получив доступ к квадракоптерам, 3d-принтерам, лазерным резакам и другому крутому оборудованию, которое я раньше не видел.

Все знания, которые я получил имели практическую направленность. И это очень круто. Ты не просто считаешь дифференциальные уравнения, ты потом с их помощью отображаешь снимки МРТ.

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

Перевод Проблема с N1 запросами в JPA и Hibernate

24.11.2020 20:17:07 | Автор: admin

В преддверии курса "Highload Architect" приглашаем вас посетить открытый урок по теме "Паттерны горизонтального масштабирования хранилищ".


А пока делимся традиционным переводом полезной статьи.


Введение

В этой статье я расскажу, в чем состоит проблема N + 1 запросов при использовании JPA и Hibernate, и как ее лучше всего исправить.

Проблема N + 1 не специфична для JPA и Hibernate, с ней вы можете столкнуться и при использовании других технологий доступа к данным.

Что такое проблема N + 1

Проблема N + 1 возникает, когда фреймворк доступа к данным выполняет N дополнительных SQL-запросов для получения тех же данных, которые можно получить при выполнении одного SQL-запроса.

Чем больше значение N, тем больше запросов будет выполнено и тем больше влияние на производительность. И хотя лог медленных запросов может вам помочь найти медленные запросы, но проблему N + 1 он не обнаружит, так как каждый отдельный дополнительный запрос выполняется достаточно быстро.

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

Рассмотрим следующие таблицы БД: post (посты) и post_comments (комментарии к постам), которые связаны отношением "один-ко-многим":

Вставим в таблицу post четыре строки:

INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 1', 1)  INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 2', 2)  INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 3', 3)  INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 4', 4)

А в таблицу post_comment четыре дочерние записи:

INSERT INTO post_comment (post_id, review, id)VALUES (1, 'Excellent book to understand Java Persistence', 1)  INSERT INTO post_comment (post_id, review, id)VALUES (2, 'Must-read for Java developers', 2)  INSERT INTO post_comment (post_id, review, id)VALUES (3, 'Five Stars', 3)  INSERT INTO post_comment (post_id, review, id)VALUES (4, 'A great reference book', 4)

Проблема N+1 с простым SQL

Как уже говорилось, проблема N + 1 может возникнуть при использовании любой технологии доступа к данным, даже при прямом использовании SQL.

Если вы выберете post_comments с помощью следующего SQL-запроса:

List<Tuple> comments = entityManager.createNativeQuery("""    SELECT        pc.id AS id,        pc.review AS review,        pc.post_id AS postId    FROM post_comment pc    """, Tuple.class).getResultList();

А позже решите получить заголовок (title) связанного поста (post) для каждого комментария (post_comment):

for (Tuple comment : comments) {    String review = (String) comment.get("review");    Long postId = ((Number) comment.get("postId")).longValue();     String postTitle = (String) entityManager.createNativeQuery("""        SELECT            p.title        FROM post p        WHERE p.id = :postId        """)    .setParameter("postId", postId)    .getSingleResult();     LOGGER.info(        "The Post '{}' got this review '{}'",        postTitle,        review    );}

Вы получите проблему N + 1, потому что вместо одного SQL-запроса вы выполнили пять (1 + 4):

SELECT    pc.id AS id,    pc.review AS review,    pc.post_id AS postIdFROM post_comment pc SELECT p.title FROM post p WHERE p.id = 1-- The Post 'High-Performance Java Persistence - Part 1' got this review-- 'Excellent book to understand Java Persistence'    SELECT p.title FROM post p WHERE p.id = 2-- The Post 'High-Performance Java Persistence - Part 2' got this review-- 'Must-read for Java developers'     SELECT p.title FROM post p WHERE p.id = 3-- The Post 'High-Performance Java Persistence - Part 3' got this review-- 'Five Stars'     SELECT p.title FROM post p WHERE p.id = 4-- The Post 'High-Performance Java Persistence - Part 4' got this review-- 'A great reference book'

Исправить эту проблему с N + 1 запросом очень просто. Все, что нужно сделать, это извлечь все необходимые данные одним SQL-запросом, например, так:

List<Tuple> comments = entityManager.createNativeQuery("""    SELECT        pc.id AS id,        pc.review AS review,        p.title AS postTitle    FROM post_comment pc    JOIN post p ON pc.post_id = p.id    """, Tuple.class).getResultList(); for (Tuple comment : comments) {    String review = (String) comment.get("review");    String postTitle = (String) comment.get("postTitle");     LOGGER.info(        "The Post '{}' got this review '{}'",        postTitle,        review    );}

На этот раз выполняется только один SQL-запрос и возвращаются все данные, которые мы хотим использовать в дальнейшем.

Проблема N + 1 с JPA и Hibernate

При использовании JPA и Hibernate есть несколько способов получить проблему N + 1, поэтому очень важно знать, как избежать таких ситуаций.

Рассмотрим следующие классы, которые мапятся на таблицы post и post_comments:

JPA-маппинг выглядят следующим образом:

@Entity(name = "Post")@Table(name = "post")public class Post {     @Id    private Long id;     private String title;     //Getters and setters omitted for brevity} @Entity(name = "PostComment")@Table(name = "post_comment")public class PostComment {     @Id    private Long id;     @ManyToOne    private Post post;     private String review;     //Getters and setters omitted for brevity}

FetchType.EAGER

Использование явного или неявного FetchType.EAGER для JPA-ассоциаций плохая идея, потому что будет загружаться гораздо больше данных, чем вам нужно. Более того, стратегия FetchType.EAGER также подвержена проблемам N + 1.

К сожалению, ассоциации @ManyToOne и @OneToOne по умолчанию используют FetchType.EAGER, поэтому, если ваши маппинги выглядят следующим образом:

@ManyToOneprivate Post post;

У вас используется FetchType.EAGER и каждый раз, когда вы забываете указатьJOIN FETCH при загрузке сущностей PostComment с помощью JPQL-запроса или Criteria API:

List<PostComment> comments = entityManager.createQuery("""    select pc    from PostComment pc    """, PostComment.class).getResultList();

Вы сталкиваетесь с проблемой N + 1:

SELECT    pc.id AS id1_1_,    pc.post_id AS post_id3_1_,    pc.review AS review2_1_FROM    post_comment pc SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4

Обратите внимание на дополнительные запросы SELECT, которые появились, потому что перед возвращением списка сущностей PostComment необходимо извлечь ассоциацию с post.

В отличие от значений по умолчанию, используемых в методе find из EntityManager, в JPQL-запросах и Criteria API явно указывается план выборки (fetch plan), который Hibernate не может изменить, автоматически применив JOIN FETCH. Таким образом, вам это нужно делать вручную.

Если вам совсем не нужна ассоциация с post, то не повезло: с использованием FetchType.EAGER нет способа избежать ее получения. Поэтому по умолчанию лучше использовать FetchType.LAZY.

Но если вы хотите использовать ассоциацию с post, то можно использовать JOIN FETCH, чтобы избежать проблемы с N + 1:

List<PostComment> comments = entityManager.createQuery("""    select pc    from PostComment pc    join fetch pc.post p    """, PostComment.class).getResultList(); for(PostComment comment : comments) {    LOGGER.info(        "The Post '{}' got this review '{}'",        comment.getPost().getTitle(),        comment.getReview()    );}

На этот раз Hibernate выполнит один SQL-запрос:

SELECT    pc.id as id1_1_0_,    pc.post_id as post_id3_1_0_,    pc.review as review2_1_0_,    p.id as id1_0_1_,    p.title as title2_0_1_FROM    post_comment pcINNER JOIN    post p ON pc.post_id = p.id     -- The Post 'High-Performance Java Persistence - Part 1' got this review-- 'Excellent book to understand Java Persistence' -- The Post 'High-Performance Java Persistence - Part 2' got this review-- 'Must-read for Java developers' -- The Post 'High-Performance Java Persistence - Part 3' got this review-- 'Five Stars' -- The Post 'High-Performance Java Persistence - Part 4' got this review-- 'A great reference book'

Подробнее о том, почему следует избегать стратегии FetchType.EAGER, читайте в этой статье.

FetchType.LAZY

Даже если вы явно перейдете на использование FetchType.LAZY для всех ассоциаций, то вы все равно можете столкнуться с проблемой N + 1.

На этот раз ассоциация с post мапится следующим образом:

@ManyToOne(fetch = FetchType.LAZY)private Post post;

Теперь, когда вы запросите PostComment:

List<PostComment> comments = entityManager.createQuery("""    select pc    from PostComment pc    """, PostComment.class).getResultList();

Hibernate выполнит один SQL-запрос:

SELECT    pc.id AS id1_1_,    pc.post_id AS post_id3_1_,    pc.review AS review2_1_FROM    post_comment pc

Но если позже вы обратитесь к этой lazy-load ассоциации с post:

for(PostComment comment : comments) {    LOGGER.info(        "The Post '{}' got this review '{}'",        comment.getPost().getTitle(),        comment.getReview()    );}

Вы получите проблему с N + 1 запросом:

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1-- The Post 'High-Performance Java Persistence - Part 1' got this review-- 'Excellent book to understand Java Persistence' SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2-- The Post 'High-Performance Java Persistence - Part 2' got this review-- 'Must-read for Java developers' SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3-- The Post 'High-Performance Java Persistence - Part 3' got this review-- 'Five Stars' SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4-- The Post 'High-Performance Java Persistence - Part 4' got this review-- 'A great reference book'

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

Опять же, решение заключается в добавлении JOIN FETCH к запросу JPQL:

List<PostComment> comments = entityManager.createQuery("""    select pc    from PostComment pc    join fetch pc.post p    """, PostComment.class).getResultList(); for(PostComment comment : comments) {    LOGGER.info(        "The Post '{}' got this review '{}'",        comment.getPost().getTitle(),        comment.getReview()    );}

И как и в примере с FetchType.EAGER, этот JPQL-запрос будет генерировать один SQL-запрос.

Даже если вы используете FetchType.LAZY и не ссылаетесь на дочерние ассоциации двунаправленного отношения @OneToOne, вы все равно можете получить N + 1.

Подробнее о том, как преодолеть проблему N+1 c @OneToOne-ассоциациями, читайте в этой статье.

Кэш второго уровня

Проблема N + 1 также может возникать при использовании кэша второго уровня для обработки коллекций или результатов запроса.

Например, если выполните следующий JPQL-запрос, использующий кэш запросов:

List<PostComment> comments = entityManager.createQuery("""    select pc    from PostComment pc    order by pc.post.id desc    """, PostComment.class).setMaxResults(10).setHint(QueryHints.HINT_CACHEABLE, true).getResultList();

Если PostComment не находится в кэше второго уровня, то будет выполнено N запросов для получения каждого отдельного PostComment:

-- Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache-- Checking query spaces are up-to-date: [post_comment]-- [post_comment] last update timestamp: 6244574473195524, result set timestamp: 6244574473207808-- Returning cached query results  SELECT pc.id AS id1_1_0_,       pc.post_id AS post_id3_1_0_,       pc.review AS review2_1_0_FROM post_comment pcWHERE pc.id = 3  SELECT pc.id AS id1_1_0_,       pc.post_id AS post_id3_1_0_,       pc.review AS review2_1_0_FROM post_comment pcWHERE pc.id = 2  SELECT pc.id AS id1_1_0_,       pc.post_id AS post_id3_1_0_,       pc.review AS review2_1_0_FROM post_comment pcWHERE pc.id = 1

В кэше запросов хранятся только идентификаторы сущностей PostComment. Таким образом, если сущности PostComment не находятся в кэше, они будут извлечены из базы данных и вы получите N дополнительных SQL-запросов.


Подробнее о курсе "Highload Architect".

Подробнее..

Как мы заставили код, портированный с C, работать с моделью памяти C

24.11.2020 22:21:52 | Автор: admin
Привет, Хабр. В прошлой статье я рассказывал о том, как мы создали фреймворк для перевода кода C# на (неуправляемый) C++, чтобы выпускать свои библиотеки, изначально разработанные для платформы .Net, и под C++ тоже. В этой статье я расскажу о том, как нам удалось согласовать модели памяти этих двух языков, добившись работы портированного кода в необычном для него окружении.

Я расскажу о том, какие умные указатели мы используем, и почему нам пришлось разработать для них собственные реализации. Я также расскажу о процессе подготовки кода C# к портированию с точки зрения управления временем жизни объектов, о некоторых проблемах, с которыми мы столкнулись, и о специфических способах диагностики, которыми нам приходится пользоваться при работе.



Модель работы с памятью в C#



Код C# выполняется в управляемой среде со сборкой мусора. Для наших целей это означает, прежде всего, то, что программист C#, в отличие своего коллеги из числа разработчиков C++, освобождён от необходимости заботиться о возвращении системе выделенной на куче памяти, которая более не используется. За него это делает сборщик мусора (GC) компонент среды CLR, периодически проверяющий, какие из объектов ещё используются в программе, и очищающий те, на которые больше нет активных ссылок.

Активной считается ссылка:
  1. Расположенная на стеке (локальная переменная, аргумент метода);
  2. Расположенная в области статических данных (статические поля и свойства);
  3. Расположенная в объекте, на который есть активные ссылки.


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

В отличие от умных указателей, подход с уборкой мусора свободен от проблемы перекрёстных или циклических ссылок: если два объекта ссылаются друг на друга (возможно, через некоторое количество промежуточных объектов), это не удерживает GC от того, чтобы удалить их в тот момент, когда на всю группу (остров изоляции) не остаётся активных ссылок. Отсюда следует, в частности, то, что у программистов C# не существует каких-либо предубеждений против того, чтобы связывать объекты друг с другом в любой момент и в любых комбинациях.

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

Момент уничтожения объекта не определён язык гарантирует лишь, что он не настанет раньше, чем будет удалена последняя активная ссылка на объект. Если в портированном коде объект будет уничтожаться сразу в момент удаления последней активной ссылки, это никак не нарушит работы программы.

Ещё один важный момент связан с поддержкой обобщённых (generic) типов и методов в C#. C# позволяет писать дженерики один раз и затем использовать их как со ссылочными, так и со значимыми типами-параметрами. Как будет показано далее, этот момент для нас важен.

Модель отображения типов



Несколько слов о том, как мы отображаем типы C# на типы C++. Поскольку важным требованием для нас является как можно более точное воспроизведение API оригинального проекта (т. к. мы поставляем библиотеки, а не приложения), мы превращаем классы, интерфейсы и структуры C# в классы C++, наследующие соответствующие базовые типы.

Например, рассмотрим следующий код:

interface I1 {}interface I2 {}interface I3 : I2 {}class A {}class B : A, I1 {}class C : B, I2 {}class D : C, I3 {}class Generic<T> { public T value; }struct S {}


Он будет портирован так:

class I1 : public virtual System::Object {};class I2 : public virtual System::Object {};class I3 : public virtual I2 {};class A : public virtual System::Object {};class B : public A, public virtual I1 {};class C : public B, public virtual I2 {};class D : public C, public virtual I3 {};template <typename T> class Generic { public: T value; };class S : public System::Object {};


Класс System::Object является системным и объявлен в библиотеке, внешней по отношению к портированному проекту. Классы и интерфейсы наследуются от него виртуально (чтобы избежать проблемы бриллианта). Незначимое виртуальное наследование может быть опущено. Структуры в портированном коде наследуются от System::Object, в то время как в C# они наследуются от него через System::ValueType (лишнее наследование убрано с целью оптимизации). Обобщённые типы и методы транслируются в шаблонные классы и методы соответственно.

Для экземпляров портированных типов должны выполняться те же гарантии, которые давал C#. Экземпляры классов должны создаваться на куче, и время их жизни должно определяться временем жизни активных ссылок на них. Экземпляры структур должны создаваться на стеке (за исключением случаев боксинга). Делегаты являются особым и достаточно тривиальным случаем, выходящим за рамки данной статьи.

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

C++: умные указатели или ...?



При разработке фреймворка для портирования кода C# на Java проблем с удалением неиспользуемых объектов не было: Java предоставляет механизм сборки мусора, в достаточной мере похожий на таковой в C#, и портированный код, использующий классы, просто собирается и работает. Отличия проявляются уже в других аспектах например, при портировании структур (которые приходится отображать на классы и следить за тем, чтобы они копировались в нужных местах).

C++ иной случай. Очевидно, отображение ссылок на голые указатели не приведёт к нужным результатам, поскольку такой портированный код не будет удалять ничего (а программисты C#, привыкшие к работе в среде с GC, будут продолжать писать код, создающий множество временных объектов).

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

  1. Использовать подсчёт ссылок на объекты (например, через умные указатели);
  2. Использовать реализацию сборщика мусора для C++ (например, Boehm GC);
  3. Использовать статический анализ для определения мест, в которых производится удаление объектов.


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

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

Таким образом, мы пришли к последнему оставшемуся варианту к использованию умных указателей с подсчётом ссылок, что является довольно типичным для C++ (не случайно 11-ый стандарт расширил их поддержку). Это, в свою очередь, означало, что для решения проблемы циклических ссылок нам придётся использовать слабые ссылки в дополнение к сильным.

Вид умных указателей



Существует несколько широко известных типов умных указателей. shared_ptr можно было бы назвать самым ожидаемым выбором, однако он имеет тот недостаток, что располагает счётчик ссылок на куче отдельно от объекта даже при использовании enable_shared_from_this, а выделение/высвобождение памяти под счётчик относительно дорогая операция. intrusive_ptr в этом смысле лучше, поскольку на наших задачах наличие неиспользуемого поля в 4/8 байт внутри структуры является меньшим злом, чем лишняя аллокация при создании каждого временного объекта.

Теперь рассмотрим следующий код:

class Document{    private Node node;    public Document()    {        node = new Node(this);    }    public void Prepare(Node n) { ... }}class Node{    private Document document;    public Node(Document d)    {        document = d;        d.Prepare(this);    }}


Этот код будет портирован примерно в следующий (нотация для указателей свободная, поскольку решение о конкретном их типе ещё не принято):

class Document : public virtual System::Object{    intrusive_ptr<Node> node;public:    Document()    {        node = make_shared_intrusive<Node>(this);    }    void Prepare(intrusive_ptr<Node> n) { ... }}class Node : public virtual System::Object{    intrusive_ptr<Document> document;public:    Node(intrusive_ptr<Document> d)    {        document = d;        d->Prepare(this);    }}


Здесь видны сразу три проблемы:

  1. Необходим способ разорвать циклическую ссылку, сделав в данном случае Node::document слабой ссылкой.
  2. Должен существовать способ преобразования this в intrusive_ptr (аналог shared_from_this). Если вместо этого начать менять сигнатуры (например, заставив Document::Prepare принимать Node* вместо intrusive_ptr<Node>), начнутся проблемы с вызовом тех же методов с передачей уже сконструированных объектов и/или управлением временем жизни объектов.
  3. Преобразование this в intrusive_ptr на этапе создания объекта с последующим уменьшением счётчика ссылок до нуля (как это происходит, например, в конструкторе Node при выходе из Document::Prepare) не должно приводить к немедленному удалению недоконструированного объекта, на который ещё не существует внешних ссылок.


Первый пункт было решено исправлять в ручном режиме, поскольку во многих ситуациях даже человек с трудом может понять, какая из нескольких ссылок должна быть слабой, а в некоторых случаях на этот вопрос в принципе не существует ответа (что требует изменений в коде C#). Например, в одном из проектов была пара классов действие печати и парамтеры действия печати, конструктор каждого из которых создавал парный объект и связывал с текущим двусторонними ссылками. Очевидно, превращение одной из этих ссылок в слабую нарушило бы сценарий использования. В итоге было решено использовать атрибут CppWeakPtr, указывающий портеру, что соответствующее поле должно содержать слабую ссылку вместо сильной.

Вторая проблема решается элементарно, если intrusive_ptr допускает конверсию из голого указателя (каковым является this). Для реализации из boost это так.

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

С соответствующими правками код до и после портирования выглядел примерно так:

class Document{    private Node node;    public Document()    {        node = new Node(this);    }    public void Prepare(Node n) { ... }}class Node{    [CppWeakPtr] private Document document;    public Node(Document d)    {        document = d;        d.Prepare(this);    }}


class Document : public virtual System::Object{    intrusive_ptr<Node> node;public:    Document()    {        System::Details::ThisProtector guard(this);        node = make_shared_intrusive<Node>(this);    }    void Prepare(intrusive_ptr<Node> n) { ... }}class Node : public virtual System::Object{    weak_intrusive_ptr<Document> document;public:    Node(intrusive_ptr<Document> d)    {        System::Details::ThisProtector guard(this);        document = d;        d->Prepare(this);    }}


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

Шаблоны



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

class MyContainer<T>{    public T field;    public void Set(T val)    {        field = val;    }}class MyClass {}struct MyStruct {}var a = new MyContainer<MyClass>();var b = new MyContainer<MyStruct>();


Портирование в лоб даёт следующий результат:

template <typename T> class MyContainer : public virtual System::Object{public:    T field;    void Set(T val)    {        field = val;    }};class MyClass : public virtual System::Object {};class MyStruct : public System::Object {};auto a = make_shared_intrusive<MyContainer<MyClass>>();auto b = make_shared_intrusive<MyContainer<MyStruct>>();


Очевидно, этот код будет работать совсем не так, как оригинал, поскольку при инстанциировании MyContainer<MyClass> объект field переехал из кучи в поле MyContainer, сломав всю семантику копирования ссылок. В то же время, расположение структуры MyStruct в поле совершенно правильно, поскольку соответствует поведению C#.

Разрешить данную ситуацию можно двумя способами:

  1. Перейдя от семантики MyContainer<MyClass> к семантике MyContainer<intrusive_ptr<MyClass>>:

    auto a = make_shared_intrusive<MyContainer<MyClass>>();
    
  2. Для каждого шаблонного класса создав две специализации: одну обрабатывающую случаи, когда аргумент-тип является значимым типом, вторую для случаев ссылочных типов:

    template <typename T, bool is_T_reference_type = is_reference_type_v<T>> class MyContainer : public virtual System::Object{public:    T field;    void Set(T val)    {        field = val;    }};template <typename T> class MyContainer<T, true> : public virtual System::Object{public:    intrusive_ptr<T> field;    void Set(intrusive_ptr<T> val)    {        field = val;    }};
    


Помимо многословности, растущей экспоненциально с каждым новым парамтером-типом (кроме случаев, когда через синтаксис where ясно указано, может ли параметр-тип быть только ссылочным или только значимым), второй вариант плох тем, что каждый контекст, в котором используется MyContainer<T>, должен знать, является ли T значимым или ссылочным типом, что во многих случаях нежелательно (например, когда мы хотим иметь минимально возможное количество включаемых заголовков или и вовсе спрятать информацию о неких внутренних типах). Кроме того, выбор типа ссылки (сильная или слабая) возможен лишь один раз на контейнер то есть, становится невозможно иметь одновременно List сильных ссылок и List слабых ссылок, хотя в коде наших продуктов существовала необходимость в обоих вариантах.

С учётом этих соображений, было решено портировать MyContainer<MyClass> в семантике MyContainer<System::SharedPtr<MyClass>> (либо MyContainer<System::WeakPtr<MyClass>> для случая слабых ссылок). Поскольку наиболее популярные библиотеки не предоставляют указателей с требуемыми характеристиками, нами были разработаны собственные реализации, получившие названия System::SharedPtr (сильная ссылка, использующая счётчик ссылок в объекте) и System::WeakPtr (слабая ссылка, использующая счётчик ссылок вне объекта). За создание объектов в стиле std::make_shared отвечает функция System::MakeObject.

Тип ссылки как часть состояния указателя



С самого начала речь шла о портировании нескольких проектов объёмом до нескольких миллионов строк кода. По сути, техническое задание на фреймворк сводилось к фразе чтобы всё это портировалось и нормально работало на C++. Работа людей, ответственных за выпуск продуктов для C++, сводится к тому, чтобы портировать код, прогонять тесты, готовить релизные пакеты, и так далее. Проблемы, возникающие при этом, как правило, попадают в одну из нескольких категорий:

  1. Код не портируется (работа портера завершается с ошибкой).
  2. Код портируется, но не компилируется.
  3. Код компилируется, но не линкуется.
  4. Код линкуется и запускается, но тесты не проходят (или происходят падения в рантайме).
  5. Тесты проходят, но при их работе возникают проблемы, не связанные напрямую с функциональностью продукта (утечки памяти, низкая производительность и т. п.).


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

В начале, когда мы исправляли простые случаи утечки памяти, вызванные циклическими зависимостями между объектами, мы навешивали атрибут CppWeakPtr на поля, получая в итоге поля типов WeakPtr. До тех пор, пока WeakPtr может быть преобразован в SharedPtr вызовом метода lock() (или неявно, что удобнее синтаксически), это не вызывает проблем. Единственный нетривиальный сценарий, который приходит в голову, это передача такой ссылки в качестве ref-аргумента, однако это настолько редкая ситуация, что на нашем коде она не стала проблемой. Далее, однако, нам пришлось также делать слабыми ссылки, содержащиеся в контейнерах, используя специальный синтаксис атрибута CppWeakPtr, и вот тут нас ждала пара неприятных сюрпризов.

Первым звоночком, сообщившим о проблемах с принятым нами подходом, стало то, что с точки зрения C++ MyContainer<SharedPtr<MyClass>> и MyContainer<WeakPtr<MyClass>> это два разных типа. Соответственно, они не могут быть сохранены в одну и ту же переменную, переданы в один и тот же метод (или возвращены из него), и так далее. Атрибут, предназначенный сугубо для управления способом хранения ссылок в полях объектов, начал появляться во всё более странных контекстах, затрагивая возвращаемые значения, аргументы, локальные переменные, и так далее. Код портера, отвечающий за его обработку, становился сложнее день ото дня.

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

Очевидно, эти две проблемы не решались в рамках существующей парадигмы, и типы указателей были вновь пересмотрены. Результатом пересмотра подхода стал класс SmartPtr, имеющий метод set_Mode(), принимающий одно из двух значений: SmartPtrMode::Shared и SmartPtrMode::Weak. Те же значения принимают все конструкторы SmartPtr. В итоге каждый экземпляр указателя может находиться в одном из двух состояний:

  1. Сильная ссылка, счётчик ссылок инкапсулирован в объект;
  2. Слабая ссылка, счётчик ссылок находится вне объекта.


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

Полный список функций, поддерживаемых нашим указателем, выглядит так:

  1. Хранение сильной ссылки: управление временем жизни объекта с подсчётом ссылок.
  2. Хранение слабой ссылки на объект.
  3. Семантика intrusive_ptr: любое количество указателей, созданных на один и тот же объект, будут разделять один счётчик ссылок.
  4. Разыменование и оператор стрелка для доступа к объекту, на который указывает указатель.
  5. Полный набор конструкторов и операторов присваивания.
  6. Раздленеие объекта, на который указывает указатель, и объекта, для которого ведётся подсчёт ссылок (aliasing constructor): поскольку наши библиотеки работают с документами, у нас часто бывает ситуация, когда указатель на элемент документа должен держать живым весь документ.
  7. Полный набор кастов.
  8. Полный набор операций сравнения.
  9. Присваивание и удаление указателей работают на неполных типах.
  10. Набор методов для проверки и изменения состояния указателя (режим псевдонима, режим хранения ссылки, число ссылок на объект и т. д.).


Класс SmartPtr является шаблонным и не содержит виртуальных методов. Он тесно связан с классом System::Object, который осуществляет хранение счётчика ссылок, и работает исключительно с его дочерними классами.

Существуют отступления от типового поведения указателей:

  1. Перемещение (конструктор перемещения, перемещающий оператор присваивания) изменяет не всё состояние, сохраняя тип ссылки (слабая/сильная).
  2. Доступ к объекту по слабой ссылке не требует локинга (создания временной сильной ссылки), так как подход, при котором оператор стрелка возвращает временный объект, слишком сильно просаживает производительность на сильных ссылках.


Для сохранения работоспособности старого кода тип SharedPtr стал псевдонимом SmartPtr. Класс WeakPtr теперь наследуется от SmartPtr, не добавляя каких-либо полей, и лишь переопределяет конструкторы, всегда создавая слабые ссылки.

Контейнеры теперь всегда портируются в семантике MyContainer<SmartPtr<MyClass>>, а тип хранимых ссылок выбирается в рантайме. Для контейнеров, написанных вручную на базе структур данных из STL (в первую очередь, контейнеров из пространства имён System), тип ссылки по умолчанию задаётся при помощи кастомного аллокатора, при этом для отдельных элементов контейнера остаётся возможность изменения режима. Для портированных контейнеров необходимый код переключения режима хранения ссылок генерируется портером.

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

Подготовка кода к портированию



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

struct S {    MyClass s; // Сильная ссылка на объёкт    [CppWeakPtr] MyClass w; // Слабая ссылка на объект    MyContainer<MyClass> s_s; // Сильная ссылка на контейнер сильных ссылок    [CppWeakPtr] MyContainer<MyClass> w_s; // Слабая ссылка на контейнер сильных ссылок    [CppWeakPtr(0)] MyContainer<MyClass> s_w; // Сильная ссылка на контейнер слабых ссылок    [CppWeakPtr(1)] Dictionary<MyClass, MyClass> s_s_w; // Сильная ссылка на контейнер, в котором ключи хранятся по сильным ссылкам, а значения - по слабым    [CppWeakPtr, CppWeakPtr(0)] Dictionary<MyClass, MyClass> w_w_s; // Слабая ссылка на контейнер, в котором ключи хранятся по слабым ссылкам, а значения - по сильным}


В некоторых случаях требуется вручную вызвать aliasing конструктор класса SmartPtr или его метод, задающий тип хранимой ссылки. Мы стараемся избегать правок портированного кода, поскольку их приходится внедрять заново после каждого запуска портера. Вместо этого мы стараемся держать такой код в исходниках C#. Для этого у нас есть две возможности:

  1. Мы можем объявить в коде C# сервисный метод, который не делает ничего, а при портировании заменить его на написанный вручную аналог, выполняющий необходимую операцию:

    class Service {    public static void SetWeak<T>(T arg) {}}
    

    class Service {public:    template <typename T> static void SetWeak<T>(SmartPtr<T> &arg)    {        arg.set_Mode(SmartPtrMode::Weak);    }};
    
  2. Мы можем размещать в коде C# специальным образом оформленные комментарии, которые портер преобразует в код C++:

    class MyClass {    private Dictionary<string, object> data;    public void Add(string key, object value)    {        data.Add(key, value);        //CPPCODE: if (key == u"Parent") data->data()[key].set_Mode(SmartPtrMode::Weak);    }}
    


    Здесь метод data() в System::Collections::Generic::Dictionary возвращает ссылку на std::unordered_map, лежащую в основе данного контейнета.


Проблемы



Теперь поговорим о проблемах, относящихся к работе с памятью в нашем проекте.

Циклические сильные ссылки



class Document {    private Element root;    public Document()    {        root = new Element(this);    }}class Element {    private Document owner;    public Element(Document doc)    {        owner = doc;    }}


Этот код портируется в следующий:

class Document : public Object {    SharedPtr<Element> root;public:    Document()    {        root = MakeObject<Element>(this);    }}class Element {    SharedPtr<Document> owner;public:    Element(SharedPtr<Document> doc)    {        owner = doc;    }}


Цепочка сильных ссылок не позволяет удалить объекты Document и Element после их создания. Это решается установкой атрибута CppWeakPtr на поле Element.owner.

class Document {    private Element root;    public Document()    {        root = new Element(this);    }}class Element {    [CppWeakPtr] private Document owner;    public Element(Document doc)    {        owner = doc;    }}


class Document : public Object {    SharedPtr<Element> root;public:    Document()    {        root = MakeObject<Element>(this);    }}class Element {    WeakPtr<Document> owner;public:    Element(SharedPtr<Document> doc)    {        owner = doc;    }}


Удаление объекта на этапе создания



class Document {    private Element root;    public Document()    {        root = new Element(this);    }    public void Prepare(Element elm)    {        ...    }}class Element {    public Element(Document doc)    {        doc.Prepare(this);    }}


На выходе портера получаем:

class Document : public Object {    SharedPtr<Element> root;public:    Document()    {        ThisProtector guard(this);        root = MakeObject<Element>(this);    }    void Prepare(SharedPtr<Element> elm)    {        ...    }}class Element {public:    Element(SharedPtr<Document> doc)    {        ThisProtector guard(this);        doc->Prepare(this);    }}


При входе в метод Document::Prepare создаётся временный объект SharedPtr, который затем может удалить недоконструированный объект Element, так как на него не остаётся сильных ссылок. Как было показано выше, эта проблема решается добавлением локальной переменной ThisProtector guard в код конструктора Element. Портер делает это автоматически. Объект guard в своём конструкторе увеличивает число сильных ссылок на this на единицу, а в деструкторе опять уменьшает, не производя удаление объекта.

Двойное удаление объекта при выбросе исключения конструктором



class Document {    private Element root;    public Document()    {        root = new Element(this);        throw new Exception("Failed to construct Document object");    }}class Element {    private Document owner;    public Element(Document doc)    {        owner = doc;    }}


После портирования получаем:

class Document : public Object {    SharedPtr<Element> root;public:    Document()    {        ThisProtector guard(this);        root = MakeObject<Element>(this);        throw Exception(u"Failed to construct Document object");    }}class Element {    SharedPtr<Document> owner;public:    Element(SharedPtr<Document> doc)    {        ThisProtector guard(this);        owner = doc;    }}


После вылета исключения в конструкторе документа и выхода из кода конструктора начинается раскрутка стека, в том числе удаление полей недоконструированного объекта Document. В свою очередь, это приводит к удалению поля Element::owner, содержащего действительную сильную ссылку на удаляемый объект. Происходит удаление объекта, уже находящегося в составе деконструирования, что заканчивается различными ошибками времени выполнения.

Установка атрибута CppWeakPtr на поле Element.owner решает эту проблему, однако до того, как атрибуты будут расставлены, отладка таких приложений затруднена из-за непредсказуемых завершений. Для упрощения поиска проблем существует особый отладочный режим сборки нашего кода, в котором внутриобъектный счётчик ссылок переносится на кучу, дополняясь флагом, выставляемым лишь после того, как объект будет доконструирован (на уровне функции MakeObject после выхода из конструктора объекта). Если указатель уничтожается до выставления флага, удаление объекта не производится.

Удаление цепочек объектов



class Node {    public Node next;}


class Node : public Object {public:    SharedPtr<Node> next;}


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

Поиск циклических ссылок



Исправление проблемы циклических ссылок производится элементарно добавлением атрибута к коду C#. Плохая новость состоит в том, что разработчик, ответственный за выпуск продукта для языка C++, по умолчанию не знает о том, какая именно ссылка должна быть слабой, равно как и о том, что цикл вообще существует.

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

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

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

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

  1. При вызове соответствующей функции в файл сохраняется полный граф существующих на данный момент объектов, включая информацию о типах, полях и связях. Этот граф может затем быть визуализирован при помощи утилиты graphviz. Как правило, данный файл создаётся после каждого теста, чтобы было удобно отслеживать утечки.
  2. При вызове соответствующей функции в файл сохраняется граф существующих на данный момент объектов, между которыми существуют циклические связи (все ссылки которых являются сильными). Таким образом, граф содержит лишь значащую информацию. Объекты, которые уже были проанализированы, исключаются из анализа при следующем вызове данной функции. Таким образом, видеть, что именно утекло из конкретного теста, становится гораздо проще.
  3. При вызове соответствующей функции в консоль выводится информация о существующих на данный момент островах изоляции наборах объектов, все ссылки на которые находятся в полях других объектов набора. Объекты, на которые ссылаются статические либо локальные переменные, не попадают в данный вывод. Информация о каждом типе острова изоляции (о наборе классов, создающих типовой остров) выводится только один раз.
  4. Деструктор класса SharedPtr проходит по ссылкам между объектами, начиная с объекта, временем жизни которого он управляет, и выводит информацию обо всех найденных циклах (обо всех случаях, когда от текущего объекта по сильным связям можно дойти до него же).


Ещё одним полезным отладочным инструментом является проверка того, что после вызова конструктора некоторого класса функцией MakeObject счётчик сильных ссылок на данный объект равен нулю. Если это не так, это означает потенциальную проблему (цикл ссылок, неопределённое поведение при вылете исключения и т. п.).

Резюме



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

Масштабируем WebSocket соединения на Go

25.11.2020 14:11:26 | Автор: admin
Мессенджер Авито это:
  • 12 m уникальных пользователей в месяц;
  • Версии для всех современных платформ (Web, iOS, Android);
  • Достаточно нагруженное приложение около 800 тысяч подключений онлайн по WebSocket (основной протокол общения с пользователями).

Александр Емелин из компании Авито автор проекта Centrifugo open-source сервера real-time сообщений, где основной протокол передачи данных как раз WebSocket. Сервер используется в проектах Mail.Ru (в том числе в Юле), а также во внутренних проектах Badoo, ManyChat, частично Авито и за рубежом (например, Spot.im). Сейчас сервер базируется на доступной всем Go-разработчикам библиотеке Centrifuge.

На конференции Golang Conf 2019 Александр рассказал, как команда Авито решала проблемы при работе с WebSocket как про детали, касающиеся Go в частности, так и вообще про работу с большим количеством постоянных соединений.




Что такое WebSocket


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



WebSocket соединение стартует с HTTP запроса версии 1.1, и это важно, так как HTTP/2 не имеет механизма Upgrade. Существуют, конечно, спецификации, которые позволяют стартовать WebSocket и работать через HTTP/2, мультиплексируя WebSocket-соединения внутри отдельных стримов HTTP/2, но из драфта они так и не вышли.

Итак, клиент по HTTP обменивается с сервером специализированными заголовками так называемый апгрейд соединения, и в случае успешной операции остается чистая TCP-сессия, по которой передаются WebSocket-фреймы. То есть, WebSocket это фреймовый протокол, при этом через фреймы можно передавать как текстовые, так и бинарные данные.

Почему WebSocket


Почему вы вообще можете захотеть использовать WebSocket в своих приложениях:
  • Это достаточно простой протокол. Но дьявол, как известно, в деталях например, известный питонист Армин Ронахер в своей статье рассказывает о некоторых неочевидных переусложнениях WebSocket-протокола.
  • WebSocket работает на всех современных платформах и, что немаловажно, в браузере. Часто это становится решающим фактором, чтобы его выбрать как основной транспорт (ниже я расскажу чуть больше об альтернативах)
  • У WebSocket небольшой оверхед по сравнению с чистой TCP-сессией, то есть фрейминг WebSocket-протокола добавляет всего от 2 до 8 байт к вашим данным смотрите исследование на эту тему.


Задачи WebSocket-сервера


Я считаю, что WebSocket-сервер должен решать в реальном мире такие задачи:
  • Держать большое количество соединений;
  • Отправлять большое количество сообщений;
  • Обеспечивать fallback WebSocket-соединения. Но даже сейчас не все пользователи могут соединиться по протоколу WebSocket. Мы об этом сегодня поговорим.
  • Аутентификация соединений, причем как мы увидим ниже, WebSocket-приложения обладают своей спецификой.
  • Инвалидация соединений (иначе соединение может быть установлено один раз и висеть неделями).
  • Переживать массовый реконнект WebSocket-приложения отличаются от стандартных HTTP-серверов тем, что у вас масса постоянных соединений: не stateful-запросы, а stateful-приложения. Также есть проблема массового реконнекта от тысяч, сотен тысяч, миллионов пользователей.
  • Не терять сообщения при реконнекте. Если в чате/мессенджере потеряется сообщение при реконнекте, пользователи счастливы не будут. Часто восстанавливают состояние из основной СУБД приложения, но мы посмотрим на некоторые нюансы.

Понятно, что в зависимости от приложения важность того или иного пункта варьируется.

Тюнинг ОС основные моменты


Прежде, чем на ваш сервер придут тысячи соединений по WebSocket, вам нужно затюнить операционную систему.

Лимит файловых дескрипторов
Наверное, это первое, на что все натыкаются. Каждое соединение отъедает файловый дескриптор у ОС. По умолчанию лимит не такой уж большой от 256 до 1024 файловых дескрипторов. Хотите больше соединений поднимайте лимит.

Совет: Ограничивайте максимальное количество соединений. Если вы знаете, что у вас ОС не позволит принимать соединений больше, чем, например, 65535 (такой лимит выставлен), то не принимайте в своем WebSocket-сервере соединений свыше этого лимита:
// ulimit -n == 65535if conns.Len() >= 65500 {     return errors.New("connection limit reached")}conns.Add(conn)

Иначе, когда у вас все пойдет плохо (а оно пойдет), вы не сможете даже зайти в pprof для анализа проблемы, потому что поход в pprof это еще один файловый дескриптор, которого у вас нет. Вы также можете посмотреть в сторону netutil.LimitListener для этой задачи, но тогда pprof имеет смысл запускать на другом порту, отдельном от WebSocket сервера.

Ephemeral ports. Следующая проблема это эфемерные порты. Обычно проблема появляется на связке load balancer WebSocket-сервер и проявляется в том, что балансировщик не может открыть еще одно WebSocket-соединение до вашего WebSocket сервера, потому что у него исчерпались порты. Портов, с которых можно открыть соединение, по умолчанию не так много около 10-15 тысяч. Плюс у сокета есть состояние time wait, когда его нельзя переиспользовать смотрите, чтобы получить больше информации и узнать о способах решения.

Conntrack table. На каждой Linux-машине стоит iptables, в состав которого входит фреймворк netfilter. В нем каждое соединение трекается отдельной записью для контроля за тем, какие соединения установлены с сервером. Размер этой информации по умолчанию ограничен, как его затюнить смотрите здесь.

Sysctl. Возможно, вы захотите затюнить sysctl сетевой стек Linux размер памяти, которое отводится под TCP-соединения и многое другое. Об этом можно прочитать, например, здесь.

Что мы знаем о WebSocket в Go


Перейдем к уровню приложения (которое в нашем случае написано на Go). Что мы вообще знаем про WebSocket в Go:
  • Пакет websocket считается deprecated использовать его не рекомендуется.
  • Стандартом де-факто является пакет Gorilla Websocket практически все WebSocket-приложения на Go, которые сейчас есть, используют его для работы с WebSocket. Большинство моих примеров будут основаны именно на нём.
  • Библиотека от Сергея Камардина дает возможность делать некоторые оптимизации (в основном касающиеся эффективного использования оперативной памяти и оптимизаций аллокаций памяти при апгрейде соединения), на которые gorilla/websocket не способна.
  • Также есть библиотека от Anmol Sethi (nhooyr). Автор активен и даже сделал попытку мейнтейнить gorilla/websocket, чтобы новых пользователей перенаправлять на свою библиотеку :) Из преимуществ этой библиотеки можно отметить goroutine-safe API, встроенную поддержку graceful закрытия WebSocket-соединения (что достаточно нетривиально сделать с Gorilla WebSocket). Сейчас темп разработки этой библиотеки замедлился, а несколько неприятных багов в issue-трекере осталось. И несмотря на заявления автора о том, что пакет имеет внутри некоторые оптимизации на моих бенчмарках Gorilla WebSocket давал более производительный результат.


Простой WebSocket сервер


Посмотрим на простой WebSocket-сервер:
var upgrader = websocket.Upgrader{     ReadBufferSize: 1024,     WriteBufferSize: 1024,}func ServeHTTP(w http.ResponseWriter, r *http.Request) {     conn, _ := upgrader.Upgrade(w, r, nil)     client := newClient(conn)     defer client.Close()     go client.write()     client.read()}

Все начинается с того, что есть HTTP-обработчик. Как я сказал, WebSocket стартует с HTTP. Мы вызываем метод Upgrade:
conn, _ := upgrader.Upgrade(w, r, nil)

Внутри Upgrade библиотека gorilla/websocket делает Hack соединения, то есть берет его под свой контроль и отдает нам *websocket.Conn. На самом деле эта структура wrapper над сетевым соединением.

Далее мы передаем настройки upgrader: ReadBufferSize и WriteBufferSize. Это размер буферов, которые будут использоваться для ввода-вывода, для записи и для чтения для того, чтобы уменьшить количество системных вызовов (syscalls). И эти буферы будут прибиты к вашему соединению железно, то есть они увеличивают размер потребления памяти. Мы к этому еще вернемся.

Обычно мы оборачиваем соединение в какую-то нашу структуру уровня приложения, назовем ее client:
     client := newClient(conn)

В конце хендлера мы закрываем клиента и не забываем там же закрывать соединение:
     defer client.Close()

Стартуем отдельную горутину на запись (кстати нужно помнить о том, что gorilla/websocket не поддерживает конкурентное чтение и конкурентную запись единовременно только одна горутина может писать данные в соединение, и только одна горутина может читать из него):

     go client.write()

Блокируем HTTP хендлер методом read (read вычитывает данные из WebSocket до тех пор, пока не случится ошибка. Как только случается ошибка, вы выходите из цикла read и HTTP-хендлер завершается):
     client.read()


Оптимизации потребления RAM


Формула Камардина


В работе WebSocket-соединения есть известная проблема: они требовательны к оперативной памяти. Эта тема поднималась не так давно в GO-community Сергей Камардиным (Миллион WebSocket и pub/sub). Даже если тема WebSocket вам не интересна, рекомендую эту статью для ознакомления там в каждой строчке инженерная мысль. Сергей говорит о том, что потребление памяти на WebSocket-соединение складывается из следующих факторов:
  1. Две горутины (одна читает, другая пишет);
  2. HTTP-буферы на чтение и на запись от стандартной библиотеки Go, которая аллоцируется, потому что WebSocket-соединение начинается с HTTP;
  3. Буферы, которые мы в приложении используем для ввода-вывода.

Если аппроксимировать, то на миллион соединений нужно минимум 20 GB RAM. Достаточно большая цифра, не правда ли?



Также стоит посмотреть выступление Going Infinite, handling 1 millions websockets connections in Go израильтянина Eran Yanay, который по сути взял идеи Сергея и переложил их на иностранный лад, дописав примеры.

Но на самом деле это самый плохой сценарий. Плюс оперативная память стоит относительно дешево, и современный сервер, например, в Avito имеет 378 GB RAM. Такой сервер позволяет держать много соединений, и в большинстве случаев первым лимитирующим фактором для приложения станет CPU, а не RAM. Тем не менее, экономить память, конечно, нужно, и ниже мы посмотрим на кое-какие трюки с Gorilla WebSocket, которые позволят сократить потребление RAM.

Переиспользуем HTTP-буферы


С gorilla/websocket мы можем переиспользовать буферы, которые аллоцируются HTTP-сервером стандартной библиотеки. Для этого мы ReadBufferSize и WriteBufferSize передаем нулями:

var upgrader = websocket.Upgrader{     ReadBufferSize: 0,     WriteBufferSize: 0,}func ServeHTTP(w http.ResponseWriter, r *http.Request) {     conn, _ := upgrader.Upgrade(w, r, nil)     client := newClient(conn)     defer client.Close()     go client.write()     client.read()}  

Gorilla/websocket может переиспользовать буфер, используя Hijack соединения:
func Upgrade(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {      hj, ok := w.(http.Hijacker)      if !ok {          http.Error(w, "error hijacking", http.StatusInternalServerError)          return      }      conn, bufrw, err := hj.Hijack()      if err != nil {          http.Error(w, err.Error(), http.StatusInternalServerError)          return      }      ...}  

Если посмотреть внутрь функции Upgrade, то когда происходит Hack, возвращается объект bufio.ReadWriter, который содержит именно те буферы, которые HTTP-сервер аллоцировал для обработки изначального запроса:
     conn, bufrw, err := hj.Hijack()

При всем этом у нас все равно остаются две горутины, а буферы прибиты к соединениям.

Gobwas/ws: beyond std lib


Библиотека от Сергея Камардина позволяет отойти от этих проблем и сделать две оптимизации:
  1. Zero-copy upgrade. Мы убираем использование стандартного HTTP-сервера Go и сами парсим HTTP, при этом не аллоцируя дополнительной памяти.
  2. Возможность использовать epoll/kqueue (см. netpoll, gnet, evio, gaio), используя syscalls и минуя рантайм и Go netpoller. Тем самым можно даже отойти от двух горутин, одна из которых пишет, а другая читает. Также можно переиспользовать все буферы, о которых шла речь.

Кстати от буфера, прибитого на чтение к WebSocket-соединениям, может помочь избавиться issue на GitHub в репозитории Go: #15735 net: add mechanism to wait for readability on a TCPConn (правда, движения по ней в последнее время нет). Как только она закроется, у нас будет возможность буферы переиспользовать.

Write Buffer Pool


От Write Buffer Pool уже сейчас в gorilla/websocket мы можем избавиться, используя sync.Pool в upgrader. Тогда буферы на запись будут переиспользоваться:
var upgrader = websocket.Upgrader{     ReadBufferSize: 1024,     WriteBufferPool: &sync.Pool{},}     func ServeHTTP(w http.ResponseWriter, r *http.Request) {     conn, _ := upgrader.Upgrade(w, r, nil)     client := newClient(conn)     defer client.Close()     go client.write()     client.read()}  


Даем поработать GC


Есть еще один трюк, который со стандартной библиотекой Go позволит сократить потребление памяти мы даем поработать GC:
var upgrader = websocket.Upgrader{     ReadBufferSize: 1024,     WriteBufferSize: 1024,}func (hub *Hub) ServeHTTP(w http.ResponseWriter, r *http.Request) {     conn, _ := upgrader.Upgrade(w, r, nil)     ...     // Allow collection of memory referenced by the caller     // by doing all work in new goroutines.     go client.write()     go client.read()}  


Это значит, что в последний момент, когда происходит чтение, мы начинаем читать в отдельной горутине, и тем самым HTTP-хендлер завершается:

     go client.write()     go client.read()


Обратите внимание, что мы используем буферы 1024, потому что на самом деле те буферы, которые использует стандартная библиотека это 4 Кб на чтение и на запись. Но у WebSocket-приложений сообщения обычно гораздо меньше, поэтому мы можем использовать буферы поменьше.

Как только HTTP-хендлер завершается, мы даем возможность garbage collector прибрать за нами убрать те объекты, на которые уже ссылки не нужны. Например, это профиль inuse_space, то есть памяти, потребляемой процессом, в случае, когда мы не стартовали читающую горутину в последний момент:



А это профиль, когда мы стартовали отдельную горутину:



Кажется, что разница видна невооруженным глазом. Но если подсветить, то именно столько объектов garbage collector получает возможность собрать:



Вы можете посмотреть больше примеров и цифр в этом репозитории. Такой техникой можно сэкономить до 40% оперативной памяти.

Отмена сontext


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



Вообще контекст проникает во все уголки нашей разработки. Кажется естественным, что можно взять и прокинуть контекст далее по стеку приложения ведь он может содержать какие-то полезные данные, которые вы в него положили в рамках middleware. Скорее всего, вы захотите отменять тяжелые действия, которые у вас есть в WebSocket-сервере, основываясь на том, когда отменяется контекст. Кажется, что вы ожидаете, что контекст отменится, когда соединение закрывается.

В данном случае контекст закроется сразу, как только завершится HTTP-хендлер. Но на самом деле это не проблема. Мы можем воспользоваться крутой фичей Go оборачивать интерфейсы и менять реализацию методов. Посмотрим на интерфейс Context:
type Context interface {     Deadline() (deadline time.Time, ok bool)     Done() <-chan struct{}     Err() error     Value(key interface{}) interface{}}  

Здесь нас в первую очередь интересует метод Done(), который у него есть. Нам нужно создать свой контекст:
type customCancelContext struct {     context.Context     ch <-chan struct{}}...func (c customCancelContext) Done() <-chan struct{} {      return c.ch}func (c customCancelContext) Err() error {     select {     case <-c.ch:         return context.Canceled     default:         return nil     }}  

Мы назвали его customCancelContext, и далее:
  • Обернули изначальный контекст:

context.Context

  • Передали в него канал, который будет закрываться тогда, когда соединение реально закроется (мы сами это знаем, сами управляем):

ch <-chan struct{}

  • Переопределили метод Done():

func (c customCancelContext) Done() <-chan struct{} {        return c.ch     }

В этом методе Done() мы возвращаем как раз тот канал, закрытием которого управляем. Все, что нам нужно дальше сделать создать такой контекст и прокинуть его по стеку нашей программы. Для других частей программы этот контекст будет все тем же интерфейсом context.Context, однако отменяться он будет тогда, когда нам нужно.

Формат сериализации сообщений


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

Но помимо стандартных реализаций посмотрите также в сторону библиотек, которые генерируют код. Яркий пример gogo/protobuf, который позволяет ускорить сериализацию protobuf и сам по себе достаточно быстрый (в 5 раз), потому что использует не модуль reflect, а кодогенерацию:



Правда, на данный момент использовать gogo/protobuf я бы не рекомендовал. Подобные библиотеки есть и для Json, думаю, вы их знаете easyjson, ffjson, и на этом можно много сэкономить.

Инвалидация соединения


Еще одна проблема, которую мы в мессенджере Авито успешно побороли это инвалидация соединения. Вообще откуда эта проблема? Приходит WebSocket-соединение от пользователя. Оно устанавливается и может висеть неделю. Что может произойти? Пользователь может открыть несколько табов браузера и в одном из них разлогиниться. В админке могут заблокировать учетную запись пользователя за какое-то нарушение, а WebSocket-соединение продолжает жить. Какой тут может быть выход?
  • Push. Вы можете подписаться на события, если у вас есть какая-то шина данных. Например, у нас в Авито есть Kafka как шина данных. Мы подписались на такие события и отключаем WebSocket.
  • Pull. Но если такой шины данных нет, можно просто периодически проверять, что WebSocket-соединение активно (периодическая валидация). Это дороже, но тут нужно найти trade-off между производительностью и тем интервалом, с которым вы проверяете каждое висящее соединение.


Fallback для WebSocket


Не все пользователи могут подключиться по WebSocket даже сейчас. Проблема далеко не в том, что где-то все еще существуют старые браузеры (Internet Explorer 8, 9), которые не поддерживают WebSocket. Основные проблемы связаны с корпоративными пользователями. Работодатель ставит своим сотрудникам доверенный рутовый сертификат на компьютер, а далее имеет возможность перешифровывать TLS-трафик даже TLS-трафик! на своих прокси. Причем прокси режет WebSocket-соединение, намеренно или потому, что там старое ПО. Такие примеры сплошь и рядом (например, в банках).

Когда я работал в Mail.ru, был случай, когда браузерное расширение Adblock заблочило все WebSocket-соединения на домены и поддомены Mail.ru. Почему? Потому что мои коллеги рассылали через WebSocket-соединение рекламу. И нас спас тогда только HTTP-Fallback.

Самый простой способ добавить HTTP-Fallback в приложение на Go это библиотека SockJS-Go, и на стороне браузера использовать SockJS client. SockJS-Go это серверная реализация для клиента SockJS. Если нет соединения по WebSocket, то соединение будет установлено через один из HTTP-транспортов: XHR-streaming, Eventsource, XHR-polling и т.д. Недостаток все эти транспорты однонаправленные (сервер -> клиент), поэтому для работы с SockJS в двунаправленном режиме и масштабировании на несколько серверов придется еще включать sticky сессии на балансере.

Понятно, что fallback можно реализовать и вручную например, через long-polling. Это не так сложно сделать, и, если вы храните стрим сообщений в быстром хранилище, можно обойтись без sticky сессий (об этом мы еще поговорим далее).

Стоит упомянуть, что для общения между клиентом и сервером многие используют GRPC (правда в данный момент GRPC нельзя использовать из браузера без дополнительного прокси). В этом докладе мы GRPC обходим стороной, но для вашего сценария это может быть неплохим вариантом. Пока же для двустороннего общения клиента и сервера из браузера альтернатив WebSocket пока нет. Также при использовании WebSocket + Protobuf вы можете ожидать гораздо более низкое потребление CPU на сервере (смотрите статью про Centrifugo v2).

Интересной технологией, на которую стоит обратить внимание в будущем является WebTransport (начать знакомство можно здесь). Если коротко это эффективная клиент-сервер коммуникация поверх QUIC или HTTP/3 (который базируется на QUIC). Сейчас это еще драфты, но с технологией уже можно поэкспериментировать. Из самого вкусного у разработчиков появится возможность использовать UDP из браузера, что ранее было невозможно без WebRTC обвеса с его STUN, ICE и т.д.

Performance is not scalability


Производительность это не масштабируемость. Как бы мы ни оптимизировали приложение, ни тюнили ОС, сколько бы ни экономили на оперативной памяти, все равно настанет момент, когда нам нужно будет масштабировать WebSocket-сервер на несколько инстансов. Даже в плане отказоустойчивости нам все равно нужно иметь несколько серверов с самого начала. Например, если упал один, то соединения перетекут на другой.

Горизонтальное масштабирование


Мы добавили WebSocket-серверов. Какая здесь основная проблема? Пользователи могут быть подключены к любому инстансу WebSocket-сервера, а мы в этот момент хотим отправить пользователю сообщение что нам делать?



Наверное, первое, что приходит на ум публиковать на все. Но это неудобно, потому что нужно знать обо всех инстансах нашего WebSocket-сервера, которые к тому же могут динамически меняться. Тут в дело вступает PUB/SUB брокер сообщений.

Центральный брокер работает по модели PUB/SUB: когда соединения приходят на WebSocket-сервера, они инициализируют подписку на свой персональный топик в брокере. Брокер знает о всех таких подписках, и когда вы делаете публикацию сообщения, вы делаете ее через брокера. В этот момент через механизм PUB/SUB сообщение долетает только до тех WebSocket-серверов, до которых оно должно долететь, где есть клиенты, реально заинтересованные в этом событии. Далее оно доставляется по WebSocket клиенту:



Про центральный PUB/SUB-брокер для масштабирования WebSocket рассказывается в большинстве статей. Однако конкретики нет. Часто говорят: Ну, а для брокера используйте RabbitMQ или Redis, и на этом повествование завершается.

Я добавил больше конкретики в этот вопрос. Посмотрим, что нам нужно от брокера:
  • Производительность конечно, это первое требование, это логично.
  • Сохранение порядка сообщений тоже важное свойство, которое нам нужно.
  • Масштабируемость брокера. Мы хотим, чтобы брокер сам по себе масштабировался и был бы отказоустойчивым.
  • Миллионы топиков. Мы хотим, чтобы он поддерживал миллионы топиков одновременно, потому что это частый use case когда у каждого соединения есть свой персональный канал. Мы хотим рассылать сообщения конкретному пользователю. И если у вас миллион WebSocket-соединений, появляется миллион топиков в брокере.
  • Кэш/стрим сообщений. Мы хотим, чтобы брокер поддерживал кэш или стрим сообщений в топике/канале. Что это такое и от чего это спасает, поговорим чуть позже.
  • Возможность писать процедуры и это большой бонус. Я расскажу, как мне это помогло в Centrifugo и в мессенджере Авито.


Опции брокера сообщений


RabbitMQ


Он будет работать, пока у вас небольшая нагрузка. На каждое соединение вы создаете очередь. На самом деле это не масштабируется, когда появляется множество пользователей, когда большой коннект-дисконнект рейт, при этом вы часто делаете bind/unbind очереди. Например, когда я пришел в мессенджер Авито, там использовался RabbitMQ, и было 100 тысяч подключений. На 100 тысячах подключений RabbitMQ потреблял 70 CPU. Забегая вперед, мы заменили RabbitMQ на Redis, и получили 0,3 ядра CPU в 200 раз лучше для нашей задачи!

Kafka


Кажется вполне естественным попробовать Kafka для этого. Моё мнение, что это оверкилл для многих приложений, потому что Kafka сохраняет стрим на диск, а консьюмер Kafka работает по pull-модели, что может быть неэффективным, когда у вас миллионы топиков и вам надо пулить данные из всех. Kafka предпочитает более стабильную конфигурацию своих топиков, потому что в динамике, когда у вас появляется subscribe/unsubscribe, топики создаются и удаляются на лету, и кажется, что во многих случаях лучше с Kafka так не делать.

Pulsar


Это еще один кандидат, но мне сложно рассуждать о его преимуществах/недостатках, так как я с ним не работал. Возможно при условии использования in-memory очередей Pulsar может быть неплохим кандидатом.

Nats, Nats-streaming


Мне кажется, что Nats вы вполне можете использовать, если нужен unreliable at most once PUB/SUB. Это производительное решение, написанное на Go. Nats-streaming я бы не использовал, потому что посмотрел его код и в достаточно критичных для себя местах нашел todo, которые не имплементируют определенную логику при восстановлении сообщений. Для меня это показалось критичным.

Tarantool


Tarantool кажется хорошей альтернативой в качестве брокера, потому что он мега-производительный, у него есть LuaJIT на стороне сервера и можно гибко писать логику. С недавней версии он поддерживает пуш со стороны брокера в соединении.

Redis


Мы в мессенджере Авито остановились на Redis, потому что:
  • Redis производительный, в том числе у него производительный PUB/SUB;
  • Он стабильный и, самое главное, предсказуемый;
  • У него есть Sentinel для High Availability;
  • Он позволяет писать атомарные LUA-процедуры;
  • Структуры данных позволяют хранить кэш сообщений. Опять возник этот магический кэш сообщений, но мы к нему скоро вернемся.


Соединения между сервером и брокером


Тут есть несколько очевидных советов если вы делаете центральный PUB/SUB брокер, то:
  • Используйте одно или пул соединений между WebSocket-сервером и вашим брокером.
  • Не используйте новое соединение на каждый коннект! Я видел часто в примерах на GitHub, как пишут код: пришло новое WebSocket-соединение, открываем новое PUB/SUB соединение с Redis или с каким-то другим брокером. Так делать не надо, это антипаттерн и это не масштабируется.
  • Используйте максимально эффективный формат сериализации сообщений для общения между WebSocket-сервером и вашим брокером. Здесь не надо задумываться о том, чтобы формат был человеко-читаемым, потому что его не увидят ни ваши фронтенд-разработчики, ни тестировщики. Это сугубо внутренняя вещь, и вы можете делать ее максимально эффективной.

К брокерам мы еще вернемся. Поговорим о другой проблеме.

Массовый реконнект


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



Часто такая пиковая нагрузка это большая проблема. Посмотрим, как с ней можно бороться с помощью нескольких техник:

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

Rate limiter на WebSocket-сервере. Если вы знаете, что у вас на бэкенде какой-то ресурс деградирует при конкурентном доступе, поставьте туда самый простейший bounded семафор и ограничьте конкурентный доступ. Делается на Go элементарно канал с буфером, и все. Зато у вас не деградирует система. То, что пользователь, предположим, переключится чуть позже обычно не так критично.

JWT-аутентификация как способ не нагружать бэкенд сессий. Часто происходит следующее. Приходит коннект на сервер, его нужно аутентифицировать, а аутентифицируете вы его через бэкенд сессий. Причем бэкенд сессий может быть развернут как локально, так и отдельно в виде сервиса. JSON Web Token позволяет избавиться от большой нагрузки на бэкенд сессий, потому что у JWT есть expiration time.

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



Добивайтесь максимальной производительности соединений. У вас сотни тысяч пользователей приходят и хотят переподписаться. В этот момент вы точно хотите переподписать их как можно скорее. Это хорошо и вам, и пользователям. Например, в случае с Redis мы можем это делать, используя Redis пайплайнинг. Это отправка нескольких запросов Redis за один запрос. Мы это делаем и в Centrifugo, и мессенджере Авито. Мы используем пайплайнинг на полную в этот момент. С этим помогает техника Smart batching паттерн, который позволяет из нескольких независимых источников горутин собирать запросы и делать один batch запрос:

maxBatchSize := 50for {     select {     case channel := <-subCh:         batch := []string{channel}     loop:         for len(batch) < maxBatchSize {              select {              case channel := <-subCh:                     batch = append(batch, channel)              default:                     break loop              }          }          // Do sth with collected batch  send          // pipeline request to Redis for ex.      }} 


У нас есть каналы, из которых приходит какая-то работа. Мы знаем, что эту работу эффективней выполнять пачками. Мы читаем из канала, записываем объект, который нам нужно обработать, в batch (например, в слайс):
batch := []string{channel}

Далее продолжаем вычитывать из этого канала данные до тех пор, пока batch не перерастет свой максимальный размер или пока в канале не останется данных:
for len(batch) < maxBatchSize {     select {     case channel := <-subCh:

Select в Go это позволяет сделать. В итоге копим batch:
batch = append(batch, channel)


На выходе у нас получается пачка объектов, с которыми мы можем работать, например, послать пайплайн запрос в Redis. Пример со smart-batching на play.golang.org. Также в моем репозитории на GitHub вы можете посмотреть, как Smart batching и пайплайнинг помогают во взаимодействии с Redis там есть набор бенчмарков.

Кэш сообщений, чтобы убрать пиковую нагрузку с СУБД. Как я уже сказал, пользователи хотят восстановить свое состояние и не пропускать сообщения, которые были в момент реконнекта. Вы можете хранить стрим сообщений, которые опубликовали, в каком-то быстром и горячем кэше. Мы их храним в Redis в структуре данных LIST. Как вариант, начиная с Redis 5.0, можно использовать Redis STREAM. Происходит публикация, идет запрос в Redis. Там работает LUA процедура, которая нам позволяет атомарно, в один RTT, сделать следующее:



  1. Мы добавляем сообщения в структуру данных List. Как вы видите, здесь уже три сообщения. У каждого сообщения при этом есть инкрементальный номер. Мы его увеличиваем атомарно.
  2. Далее мы публикуем сообщение в PUB/SUB Redis и в этот момент оно улетает в PUB/SUB. В свою очередь, PUB/SUB долетает до клиента (если он подключен). Как только клиент реконнектится, они передают номер сообщения, которое видели последним.
  3. Идем в Redis, смотрим в Lists. Если есть сообщения, восстанавливаем их клиенту из этой структуры данных, как будто он даже не был отключен.
  4. Так клиент получает весь стрим сообщений, которые ему были высланы в интервале отсутствия и так снимается пиковая нагрузка с СУБД.

На самом деле Redis PUB/SUB это at most once гарантия доставки. В мессенджере Авито и в Centrifugo мы дополнительно на уровне кода приложения нивелируем потери, сверяя номер входящего PUB/SUB сообщения с ожидаемым, а затем периодически синхронизируя позицию клиента с ожидаемой в случае долгого отсутствия сообщений из PUB/SUB. Если обнаружили потерю закрываем соединение клиента, давая ему переподключиться и выполнить всю необходимую логику для восстановления состояния.

На таком стриме можно сделать и fallback для WebSocket. В мессенджере Авито мы так и делаем. У нас есть стрим таких сообщений для каждого топика, и когда наш пользователь приходит, мы используем HTTP polling для fallback, чтобы он забрал данные из этого стрима и отдал их клиенту. Как бонус от кэша сообщений на этом же механизме можно построить и long-polling.

Время ответа RPC при раскладке. Такой график мы наблюдали при раскладке нашего сервиса, когда у нас был RabbitMQ и 100 тысяч пользователей. Видно, что ничего хорошего:



Так стало, когда у нас появился Redis и уже 500 тысяч пользователей. Видно, что раскладка происходит совершенно бесшовно для наших клиентов, а время ответа RPC-ручек совсем не растет:



Centrifugo


Многое из того, что я рассказывал, присутствует внутри Centrifugo. Если вам не хочется заморачиваться самим и писать, то можете его использовать. Это отдельно стоящий сервер, который принимает коннекты от пользователей и имеет API, чтобы отправлять сообщения пользователям. При этом он хорошо встраивается как в монолитную, так и в микросервисную архитектуру:



Как я говорил, основа сервера библиотека Centrifuge, хотя это уже не совсем библиотека, а уже к фреймворку, потому что Centrifuge задумывается о масштабируемости за вас, имеет встроенный Redis и кэш сообщений. Плюс диктует клиент-серверный протокол. Так как Go-разработчики (и я в том числе) не очень любят фреймворки, то проще всего ее описать как альтернативу socket.io на Go со всеми сопутствующими за и против.

Демо стенд


Чтобы не быть голословным, я сделал демо-стенд на миллион WebSockets в Kubernetes, который использует библиотеку Centrifuge на сервере. На этом стенде я добился следующих цифр:
  • 1 mln соединений;
  • 30 mln сообщений в минуту, которые будут доставлены пользователям (это 500 тысяч сообщений в секунду);
  • 200 k коннектов в минуту приблизил к тем цифрам, которые у нас в мессенджере Авито есть;
  • 200 ms latency доставки в 99 персентиле.

Хочу подчеркнуть, что это не нагрузочное тестирование, где мы доводим систему до полки (отказа), а просто демо-стенд. У меня было ограниченное количество железа и данные цифры это далеко не потолок возможного масштабирования, поэтому был небольшой time-lapse. Что мы видим?

Number of connections быстро дорастает до миллиона. Мы ждем какое-то время, и видим, что система ведет себя стабильно. Затем начинаем публиковать сообщения (график Messages delivered) и доходим до 30 млн сообщений в минуту. При этом смотрим на серверный CPU, на память, и в том числе на брокер (Redis). В данном случае используется 5 инстансов Redis, мы шардируем Redis по имени топика (консистентное шардирование jump). Мы видим, что latency дорастает до 200 мс и там примерно останавливается, когда рейт сообщений достиг 30 млн:



Чуть больше цифр о использованных ресурсах:
  • 40 ядер CPU total 20 подов в Kubernetes (~ 2 ядра CPU каждый);
  • 27 GB RAM total;
  • 32% ядра CPU утилизировано на каждом из 5 инстансов Redis;
  • 100 mbit/sec rx и 150 mbit/sec tx на каждом из подов.

Чуть подробнее про стенд можно почитать здесь.

Takeaways


Итак, что вы можете использовать:
  • Дайте GC поработать на благо потребления RAM, сказав: Старичок, приберись за нами! Мы тут немножко намусорили. И чем раньше мы это сделаем, тем лучше. Ему (garbage collectorу) все равно работать после того, как соединение завершится, поэтому не откладывай на завтра то, что можно сделать сегодня.
  • Используйте эффективный и компактный формат сериализации.
  • Используйте брокер сообщений для горизонтального масштабирования. Причем выбирайте тот, что подходит именно для вашей задачи.
  • Подумайте над необходимостью HTTP-fallback.
  • Лавину реконнектов нужно и можно переживать.
  • Посмотрите в сторону Centrifugo. Если вам нужно рабочее решение, сейчас вторая версия очень даже хороша JSON протокол, protobuf протокол, масштабируется с Redis.


Как со мной связаться: GitHub, Telegram, Facebook.

Конференция по Golang пройдёт уже в 2021 году, за новостями можно следить в Телеграм-канале. А пока в оставшиеся дни ноября и в декабре этого года у нас будет целая серия онлайн митапов. Ближайшие:

Следите за новостями Telegram, Twitter, VK и FB и присоединяйтесь к обсуждениям.
Подробнее..

PHP 8 Что нового?

25.11.2020 16:04:07 | Автор: admin

PHP, начиная с 7 версии, кардинально изменился. Код стал куда быстрее и надёжнее, и писать его стало намного приятнее. Но вот, уже релиз 8 версии! Ноябрь 26, 2020 примерно на год раньше, чем обещали сами разработчики. И всё же, не смотря на это, мажорная версия получилась особенно удачной. В этой статье я попытаюсь выложить основные приятные изменения, которые мы должны знать.


1. JIT


Как говорят сами разработчики, они выжали максимум производительности в 7 версии (тем самым сделав PHP наиболее шустрым среди динамических ЯПов). Для подальшего ускорения, без JIT-компилятора не обойтись. Справедливости ради, стоит сказать, что для веб-приложений использование JIT не сильно улучшает скорость обработки запросов (в некоторых случаях скорость будет даже меньше, чем без него). А вот, где нужно выполнять много математических операций там прирост скорости очень даже значительный. Например, теперь можно делать такие безумные вещи, как ИИ на PHP.
Включить JIT можно в настройках opcache в файле php.ini.
Подробнее 1 | Подробнее 2 | Подробнее 3


2. Аннотации/Атрибуты (Attributes)


Все мы помним, как раньше на Symfony код писался на языке комментариев. Очень радует, что такое теперь прекратится, и можно будет использовать подсказки любимой IDE, функция "Find usages", и даже рефакторинг!


Забавно, что символ # также можно было использовать для создания комментариев. Так что ничего не меняется в этом мире.

Было очень много споров о синтаксисе для атрибутов, но приняли Rust-like синтаксис:


#[ORM\Entity]#[ORM\Table("user")]class User{    #[ORM\Id, ORM\Column("integer"), ORM\GeneratedValue]    private $id;    #[ORM\Column("string", ORM\Column::UNIQUE)]    #[Assert\Email(["message" => "The email '{{ value }}' is not a valid email."])]    private $email;}

Подробнее 1 | Атрибуты в Symfony


3. Именованые параметры (Named Arguments)


Если вы когда-либо видели код, где есть булевы параметры, то понимаете насколько он ужасен. Ещё хуже, когда этих параметров 8 штук, 6 из которых имеют значение по-умолчанию, а вам нужно изменить значение последнего параметра.


К примеру, код для использования библиотеки phpamqplib:


$channel->queue_declare($queue, false, true, false, false);// ...$channel->basic_consume($queue, '', false, false, false, false, [$this, 'consume']);

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


$channel->queue_declare($queue, durable: true, auto_delete: false);// ...$channel->basic_consume($queue, callback: [$this, 'consume']);

Ещё несколько примеров:


htmlspecialchars($string, default, default, false);// vshtmlspecialchars($string, double_encode: false);

Внимание! Можно также использовать ассоциативные массивы для именованых параметров (и наоборот).


$params = ['start_index' => 0, 'num' => 100, 'value' => 50];$arr = array_fill(...$params);

function test(...$args) { var_dump($args); }test(1, 2, 3, a: 'a', b: 'b');// [1, 2, 3, "a" => "a", "b" => "b"]

Подробнее


4. Оператор безопасного null (Nullsafe operator)


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


$session = Session::find(123);if ($session !== null) {    $user = $session->user;    if ($user !== null) {        $address = $user->getAddress();        if ($address !== null) {            $country = $address->country;        }    }}

По хорошему, должен быть метод Session::findOrFail, который будет кидать исключение в случае отсутствия результата. Но когда эти методы диктует фреймворк, то мы не можем ничего сделать. Единственное, это проверять каждый раз на null либо, где это уместно, использовать ?->.


Да, с оператором nullsafe код станет немного лучше, но всё же это не повод возвращать null.

$country = $session?->user?->getAddress()?->country;

Этот код нельзя назвать чистым, только лишь от части. Для чистого кода, нужно использовать шаблон Null Object, либо выбрасывать exception. Идеальным вариантом было б:


$country = $session->user->getAddress()->country;

Поэтому, если возможно с вашей стороны, никогда не возвращайте null (к Римлянам 12:18).

Также интересным моментом в использовании nullsafe есть то, что при вызове метода с помощью ?->, параметры будут обработаны только если объект не null:


function expensive_function() {    var_dump('will not be executed');}$foo = null;$foo?->bar(expensive_function());

5. Оператор выбора match (Match expression v2)


Для начала покажу код до и после:


$v = 1;switch ($v) {    case 0:        $result = 'Foo';        break;    case 1:        $result = 'Bar';        break;    case 2:        $result = 'Baz';        break;}echo $result; // Bar

VS


$v = 1;echo match ($v) {    0 => 'Foo',    1 => 'Bar',    2 => 'Baz',};  // Bar

Как видим, это очень приятный оператор для выбора значений, который удобно заменяет switch.
Но есть очень важное отчилие switch от match: первый сравнивает нестрого ==, а во втором производится строгое === сравнение.


Наглядный пример различия:


switch ('foo') {    case 0:      $result = "Oh no!\n";      break;    case 'foo':      $result = "This is what I expected\n";      break;}echo $result; // Oh no!

VS


echo match ('foo') {    0 => "Oh no!\n",    'foo' => "This is what I expected\n",}; // This is what I expected

В PHP8 этот пример со switch работает по другому, далее рассмотрим это.

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


$result = match ($x) {    foo() => ...,    $this->bar() => ..., // bar() isn't called if foo() matched with $x    $this->baz => ...,    // etc.};

6. Адекватное приведение строки в число (Saner string to number comparisons)


Проблема


$validValues = ["foo", "bar", "baz"];$value = 0;var_dump(in_array($value, $validValues));// bool(true) ???

Это происходит потому, что при нестрогом == сравнении строки с числом, строка приводится к числу, то-есть, например (int)"foobar" даёт 0.


В PHP8, напротив, сравнивает строку и число как числа только если строка представляет собой число. Иначе, число будет конвертировано в строку, и будет производиться строковое сравнение.


Comparison Before After
0 == "0" true true
0 == "0.0" true true
0 == "foo" true false
0 == "" true false
42 == " 42" true true
42 == "42foo" true false

Стоит отметить, что теперь выражение 0 == "" даёт false. Если у вас из базы пришло значение пустой строки и обрабатывалось как число 0, то теперь это не будет работать. Нужно вручную приводить типы.

Эти изменения относятся ко всем операциям, которые производят нестрогое сравнение:


  • Операторы <=>, ==, !=, >, >=, <, <=.
  • Функции in_array(), array_search(), array_keys() с параметром strict: false (то-есть по-умолчанию).
  • Сотрировочные функции sort(), rsort(), asort(), arsort(), array_multisort() с флагом sort_flags: SORT_REGULAR (то-есть по-умолчанию).

Также, есть специальные значения которые при нестрогом сравнении дают true:


Expression Before After
INF == "INF" false true
-INF == "-INF" false true
NAN == "NAN" false false
INF == "1e1000" true true
-INF == "-1e1000" true true

7. Constructor Property Promotion


Изначально идея позаимствована в языка-брата Hack. Она состоит в том, чтобы упростить инициализацию полей класса в конструкторе.


Вместо прописания полей класа, параметров конструктора, инициализации полей с помощью параметров, можно просто прописать поля параметрами конструктора:


class Point {    public function __construct(        public float $x = 0.0,        public float $y = 0.0,        public float $z = 0.0,    ) {}}

Это эквивалентно:


class Point {    public float $x;    public float $y;    public float $z;    public function __construct(        float $x = 0.0,        float $y = 0.0,        float $z = 0.0,    ) {        $this->x = $x;        $this->y = $y;        $this->z = $z;    }}

С этим всё просто, так как это синтаксический сахар. Но интересный момент возникает при использовании вариативные параметры (их нельзя объявлять таким образом). Для них нужно по-старинке вручную прописать поля и установить их в конструкторе:


class Test extends FooBar {    private array $integers;    public function __construct(        private int $promotedProp,         Bar $bar,        int ...$integers,    ) {        parent::__construct($bar);        $this->integers = $integers;    }}

8. Новые функции для работы со строками (str_contains, str_starts_with, str_ends_with)


Функция str_contains проверяет, содержит ли строка $haystack строку $needle:


str_contains("abc", "a"); // truestr_contains("abc", "d"); // falsestr_contains("abc", "B"); // false // $needle is an empty stringstr_contains("abc", "");  // truestr_contains("", "");     // true

Функция str_starts_with проверяет, начинается ли строка $haystack строкой $needle:


$str = "beginningMiddleEnd";var_dump(str_starts_with($str, "beg")); // truevar_dump(str_starts_with($str, "Beg")); // false

Функция str_ends_with проверяет, кончается ли строка $haystack строкой $needle:


$str = "beginningMiddleEnd";var_dump(str_ends_with($str, "End")); // truevar_dump(str_ends_with($str, "end")); // false

Вариантов mb_str_ends_with, mb_str_starts_with, mb_str_contains нету, так как эти функции уже хорошо работают с мутльтибайтовыми символами.


На самом деле, очень приятно, что наконец-то добавили эти функции. Теперь не нужно писать каждый раз своих костылей с помощью substr, strncmp, strpos.

9. Использование ::class на объектах (Allow ::class on objects)


Раньше, чтобы получить название класса, к которому принадлежит объект, нужно было использовать get_class:


$object = new stdClass;$className = get_class($object); // "stdClass"

Теперь же, можно использовать такую же нотацию, как и ClassName::class:


$object = new stdClass;var_dump($object::class); // "stdClass"

10. Возвращаемый тип static (Static return type)


Тип static был добавлен для более явного указания, что используется позднее статическое связывание (Late Static Binding) при возвращении результата:


class Foo {    public static function createFromWhatever(...$whatever): static {        return new static(...$whatever);    }}

Также, для возвращения $this, стоит указывать static вместо self:


abstract class Bar {    public function doWhatever(): static {        // Do whatever.        return $this;    }}

11. Weak Map


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


Интерфейс класса выглядит следующим образом:


WeakMap implements Countable , ArrayAccess , Iterator {    public __construct ( )    public count ( ) : int    public current ( ) : mixed    public key ( ) : object    public next ( ) : void    public offsetExists ( object $object ) : bool    public offsetGet ( object $object ) : mixed    public offsetSet ( object $object , mixed $value ) : void    public offsetUnset ( object $object ) : void    public rewind ( ) : void    public valid ( ) : bool}

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


class FooBar {    private WeakMap $cache;    public function getSomethingWithCaching(object $obj) {        return $this->cache[$obj] ??= $this->decorated->getSomething($obj);    }    // ...}

Подробнее можно почитать в документации.


12. Убрано возможность использовать левоассоциативный оператор (Deprecate left-associative ternary operator)


Рассмотрим код:


return $a == 1 ? 'one'     : $a == 2 ? 'two'     : $a == 3 ? 'three'     : $a == 4 ? 'four'              : 'other';

Вот как он всегда работал:


$a Result
1 'four'
2 'four'
3 'four'
4 'four'

В 7.4 код как прежде, отрабатывал, но выдавался Deprecated Warning.
Теперь же, в 8 версии, код упадёт с Fatal error.


13. Изменение приоритета оператора конкатенации (Change the precedence of the concatenation operator)


Раньше, приоритет оператора конкатенации . был на равне с + и -, поэтому они исполнялись поочерёдно слева направо, что приводило к ошибкам. Теперь же, его приоритет ниже:


Expression Before Currently
echo "sum: " . $a + $b; echo ("sum: " . $a) + $b; echo "sum :" . ($a + $b);

14. Удалены краткие открывающие php теги


В каждом скрипте, где в настоящее время используется короткий <? открывающий тег, нужно будет внести изменения и использовать стандартный тег <?php.


Это не касается тега <?=, так как он, начиная с 5.4 всегда доступен.


15. Новый интерфейс Stringable


Объекты, которые реализуют метод __toString, неявно реализуют этот интерфейс. Сделано это в большей мере для гарантии типобезопасности. С приходом union-типов, можно писать string|Stringable, что буквально означает "строка" или "объект, который можно преобразовать в строку". В таком случае, объект будет преобразован в строку только когда уже не будет куда оттягивать.


interface Stringable{    public function __toString(): string;}

Рассмотрим такой код:


class A{    public function __toString(): string     {        return 'hello';    }}function acceptString(string $whatever) {    var_dump($whatever);}acceptString(123.45); // string(6) "123.45"acceptString(new A()); // string(5) "hello"

Здесь функция acceptString принимает строку, но что если нам нужно конкретно объект, что может быть преобразован в строку, а не что-либо иное. Вот тут нам поможет интерфейс Stringable:


function acceptString(Stringable $whatever) {    var_dump($whatever);    var_dump((string)$whatever);}// acceptString(123.45); /*TypeError*/acceptString(new A()); /*object(A)#1 (0) {}string(5) "hello"*/

16. Теперь throw это выражение


Примеры использования:


// This was previously not possible since arrow functions only accept a single expression while throw was a statement.$callable = fn() => throw new Exception();// $value is non-nullable.$value = $nullableValue ?? throw new InvalidArgumentException();// $value is truthy.$value = $falsableValue ?: throw new InvalidArgumentException();// $value is only set if the array is not empty.$value = !empty($array)    ? reset($array)    : throw new InvalidArgumentException();

Подробнее можно почитать здесь.


17. Стабильная сортировка


Теперь все сортировки в php стабильные. Это означает, что равные элементы будут оставаться в том же порядке, что и были до сортировки.


Сюда входят sort, rsort, usort, asort, arsort, uasort, ksort, krsort, uksort, array_multisort, а также соответствующие методы в ArrayObject.


18. Возможньсть опустить переменную исключения (non-capturing catches)


Раньше, даже если переменная исключения не использовалась в блоке catch, её всё равно нужно быто объявлять (и IDE подсвечивала ошибку, что переменная нигде не используется):


try {    changeImportantData();} catch (PermissionException $ex) {    echo "You don't have permission to do this";}

Теперь же, можно опустить переменную, если никакая дополнительная информация не нужна:


try {    changeImportantData();} catch (PermissionException) { // The intention is clear: exception details are irrelevant    echo "You don't have permission to do this";}

19. Обеспечение правильной сигнатуры магических методов (Ensure correct signatures of magic methods):


Когда были добавлены type-hints в php, оставалась возможность непавильно написать сигнатуру для магических методов.
К примеру:


class Test {    public function __isset(string $propertyName): float {        return 123.45;    }}$t = new Test();var_dump(isset($t)); // true

Теперь же, всё жёстко контролируется, и допустить ошибку сложнее.


Foo::__call(string $name, array $arguments): mixed;Foo::__callStatic(string $name, array $arguments): mixed;Foo::__clone(): void;Foo::__debugInfo(): ?array;Foo::__get(string $name): mixed;Foo::__invoke(mixed $arguments): mixed;Foo::__isset(string $name): bool;Foo::__serialize(): array;Foo::__set(string $name, mixed $value): void;Foo::__set_state(array $properties): object;Foo::__sleep(): array;Foo::__unserialize(array $data): void;Foo::__unset(string $name): void;Foo::__wakeup(): void;

20. Включить расширение json по-умолчанию (Always available JSON extension)


Так как функции для работы с json постоянно используются, и нужны чуть ли не в каждом приложении, то было принято решение включить ext-json в PHP по-умолчанию.


21. Более строгие проверки типов при для арифметических и побитовых операторов (Stricter type checks for arithmetic/bitwise operators)


Проблема, которую разработчики здесь решили предоставлена кодом ниже:


var_dump([] % [42]);

Что должен вывести этот код? Здесь непредсказуемое поведение (будет 0). Всё потому, что большинство арифметических операторов не должны применятся на массивах.


Теперь, при использовании операторов +, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, -- будет вызывать исключение TypeError для операндов array, resource и object.


22. Валидация абстрактных методов в трейтах (Validation for abstract trait methods)


До восьмой версии, можно было писать что-то вроде:


trait T {    abstract public function test(int $x);}class C {    use T;    // Allowed, but shouldn't be due to invalid type.    public function test(string $x) {}}

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


trait MyTrait {    abstract private function neededByTheTrait(): string;    public function doSomething() {        return strlen($this->neededByTheTrait());    }}class TraitUser {    use MyTrait;    // This is allowed:    private function neededByTheTrait(): string { }    // This is forbidden (incorrect return type)    private function neededByTheTrait(): stdClass { }    // This is forbidden (non-static changed to static)    private static function neededByTheTrait(): string { }}

Случаи, когда реализация приходит из родительского класса, или трейт применён в родительском классе, также проверяются.


23. Объединения типов (Union Types 2.0)


Рассмотрим код:


class Number {    /**     * @var int|float $number     */    private $number;    /**     * @param int|float $number     */    public function setNumber($number) {        $this->number = $number;    }    /**     * @return int|float     */    public function getNumber() {        return $this->number;    }}

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


Теперь же, можно прописать тип int|float (или любой другой) явно, чтобы обеспечить корректность работы модуля:


class Number {    private int|float $number;    public function setNumber(int|float $number): void {        $this->number = $number;    }    public function getNumber(): int|float {        return $this->number;    }}

А также, код становится намного чище.
Как вы уже могли заметить, типы-объединения имеют синтаксис T1|T2|... и могут быть использованы во всех местах, где можно прописать type-hints.


Некоторые оговорки:


  • Тип void не может быть частью объединения.
  • Чтобы обозначить отсутствие результата, можно объявить "Nullable union type", который имеет следующий синтаксис: T1|T2|null.
  • Тип null не может быть использован вне объединения. Вместо него стоит использовать void.
  • Существует также псевдотип false, который по историческим причинам уже используется некоторыми функциями в php. С другой стороны, не существует тип true, так как он нигде не использовался ранее.

Типы полей класса инвариантны, и не могут быть изменены при наследовании.
А вот с методами всё немного интересней:


  1. Параметры методов можно расширить, но нельзя сузить.
  2. Возвращаемые типы можно сузить, но нельзя расширить.

Вот как это выглядит в коде:


class Test {    public function param1(int $param) {}    public function param2(int|float $param) {}    public function return1(): int|float {}    public function return2(): int {}}class Test2 extends Test {    public function param1(int|float $param) {} // Allowed: Adding extra param type    public function param2(int $param) {}       // FORBIDDEN: Removing param type    public function return1(): int {}           // Allowed: Removing return type    public function return2(): int|float {}     // FORBIDDEN: Adding extra return type}

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


class A {}class B extends A {}class Test {    public function param1(B|string $param) {}    public function param2(A|string $param) {}    public function return1(): A|string {}    public function return2(): B|string {}}class Test2 extends Test {    public function param1(A|string $param) {} // Allowed: Widening union member B -> A    public function param2(B|string $param) {} // FORBIDDEN: Restricting union member A -> B    public function return1(): B|string {}     // Allowed: Restricting union member A -> B    public function return2(): A|string {}     // FORBIDDEN: Widening union member B -> A}

Интереснее становится когда strict_types установлен в 0, то-есть по-умолчанию. Например, функция принимает int|string, а мы передали ей bool. Что в результате должно быть в переменной? Пустая строка, или ноль? Есть набор правил, по которым будет производиться приведение типов.


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


  1. int;
  2. float;
  3. string;
  4. bool;

Так вот, будет перебираться этот список с типами, и для каждого проверяться: Если тип существует в объединении, и значение может быть приведёно к нему в соответствии с семантикой PHP, то так и будет сделано. Иначе пробуем следующий тип.


Как исключение, если string должен быть приведён к int|float, то сравнение идёт в первую очередь в соответствии с семантикой "числовых строк". К примеру, "123" станет int(123), в то время как "123.0" станет float(123.0).


К типам null и false не происходит неявного преобразования.

Таблица неявного приведения типов:


Original type 1st try 2nd try 3rd try
bool int float string
int float string bool
float int string bool
string int/float bool
object string

Типы полей и ссылки


class Test {    public int|string $x;    public float|string $y;}$test = new Test;$r = "foobar";$test->x =& $r;$test->y =& $r;// Reference set: { $r, $test->x, $test->y }// Types: { mixed, int|string, float|string }$r = 42; // TypeError

Здесь проблема в том, что тип устанавливаемого значения не совместим с объявленными в полях класса. Для Test::$x это могло быть int(42), а для Test::$y float(42.0). Так как эти значения не эквивалентны, то невозможно обеспечить единую ссылку, и TypeError будет сгенерирован.


24. Тип mixed (Mixed Type v2)


Был добавлен новый тип mixed.
Он эквивалентен типу array|bool|callable|int|float|object|resource|string|null.
Когда параметр объявлен без типа, то его тип это mixed.
Но стоит отметить, что если функция не объявляет возвращаемого значения, то это не mixed, а mixed|void. Таким образом, если функция гарантировано должна что-то возвращать, но тип результата не известен заранее, то стоит написать его mixed.


При наследовании действуют следующие правила:


class A{    public function bar(): mixed {}}class B extends A{    // return type was narrowed from mixed to int, this is allowed    public function bar(): int {}}

class C{    public function bar(): int {}}class D extends C{    // return type cannot be widened from int to mixed    // Fatal error thrown    public function bar(): mixed {}}

Подробнее можно почитать здесь


Где смотреть новые фичи


Более информации про новые функции в PHP можно посмотреть на rfc watch.


IMHO хорошие идеи для PHP



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


Вот пример проблемного кода. Со временем значение изменяется, и при добавлении ещё какой-либо обработки велика вероятность сломать что-то на следующих строках:


$invoice = getInvoice();$invoice = loadDependencies($invoice);$invoice = formatInvoice($invoice);// hm... how do I access initial $invoice now?return $invoice;

Я вижу как минимум 4 недостатка в этом коде:


  1. Никогда точно не знаешь что в переменной;
  2. Невозможность использовать уже перезаписанное значение где-то дальше в коде;
  3. Неустойчивость к изменениям если производиться копипаст большой части кода с такими-же переменными где-то во вложенном if, тогда ночь отладки обеспеченна.
  4. Каждый раз нужно писать знак $ перед $переменной. Да, это спорно, но ведь без долларов проще читать код. Возьмите какого-либо джависта, что он скажет про ваш код? Уххх как много долларов!

Вот каким мог быть этот код:


invoice = getInvoice();invoiceWithDependencies = loadDependencies(invoice);invoiceFormatted = formatInvoice(invoiceWithDependencies);// someAnotherAction(invoice);return invoiceFormatted;

Значения, что содержатся в invoice, invoiceWithDependencies, invoiceFormatted не могут быть перезаписаны. Да, и теперь мы точно знаем что и где хранится.


function printArr(array arr) {    foreach (arr as firmValue) {        print strtr(            "Current value is {current}. +1: {next}",             [                '{current}' => firmValue,                 '{next}'    => firmValue + 1            ]        );    }}


use Spatie\ModelStates\State;abstract class OrderStatus extends State{    public static string $name = static::getName();    abstract protected function getName(): string;}

Как видим, при первом обращении к $name, будет вызван метод getName финального класса. Это дает нам возможность настраивать какие значения будут попадать в поля в зависимости от каких-либо условий. А в данном примере это использовано с шаблоном "Template Method", и финальные классы обязаны предоставить нам значение для поля.


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


Просто сравните это:


    protected string $whatever = $this->doCalculate();

И это:


    public function __construct(        array $query = [],        array $request = [],        array $attributes = [],        array $cookies = [],        array $files = [],        array $server = [],        $content = null    ) {        parent::__construct(            $query,            $request,            $attributes,            $cookies,            $files,            $server,            $content        );        $this->whatever = $this->doCalculate();    }

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

Подробнее..

Перевод Перевод вводной статьи от разработчиков D-BUS

26.11.2020 08:23:40 | Автор: admin

Руководство по D-BUS

https://dbus.freedesktop.org/doc/dbus-tutorial.html

Red Hat, Inc

<hp@pobox.com>

Дэвид Уиллер

Джон Палмиери

Red Hat, Inc.

<johnp@redhat.com>

Колин Уолтерс

Red Hat, Inc.

<walters@redhat.com>

Версия 0.5.0

Перевод Пластов И.В.

plastov.igor@yandex.ru

Документ в процессе разработки

Это руководство не завершено. Оно, вероятно, содержит некоторую полезную информацию, но также имеет много пробелов. Прямо сейчас вам также необходимо обратиться к спецификации D-Bus, справочной документации Doxygen и посмотреть несколько примеров того, как другие приложения используют D-Bus.

Определенно, приветствуется улучшение руководства - отправляйте свои исправления или предложения в список рассылки. Если вы создаете привязку D-Bus, пожалуйста, добавьте раздел в учебник для вашей привязки, хотя бы небольшой раздел с парой примеров.

Что такое D-Bus?

D-Bus - это система межпроцессного взаимодействия (IPC). Архитектурно он имеет несколько слоев:

  • Библиотекаlibdbus, которая позволяет двум приложениям подключаться друг к другу и обмениваться сообщениями.

  • Исполняемый файл демона шины сообщений, построенный наlibdbus, к которому могут подключаться несколько приложений. Демон может направлять сообщения от одного приложения к нулю или нескольким другим приложениям.

  • Библиотеки враперов или привязок основанных на частичном применении конкретных фреймворков. Например,libdbus-glibиlibdbus-qt. Также существуют привязки к таким языкам, как Python. Эти библиотеки-враперы представляют собой API-интерфейс, который следует использовать большинству людей, поскольку это упрощают детали программирования D-Bus.libdbusпредназначена для низкоуровневого бэкенда в привязках более высокого уровня. Большая часть APIlibdbusполезна только для реализации привязок.

Библиотекаlibdbusподдерживает только соединения точка-точка, подобноraw-сокету. Однако, вместо отправки по соединению потока байтов, вы отправляете сообщения. Сообщения имеют заголовок, определяющий его тип и тело, содержащее полезные данные.libdbusтакже абстрагирует конкретно используемый транспорт (сокеты или что-то еще) и обрабатывает такие детали, как аутентификация.

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

На обычном компьютере имеется несколько экземпляров демона шины. Первый экземпляр - это глобальный синглтон машины, то есть системный демон, похожий на sendmail или Apache. Этот экземпляр имеет строгие ограничения безопасности на то, какие сообщения он будет принимать, и используется для общесистемной связи. Остальные экземпляры создаются по одному для каждого сеанса входа пользователя. Эти экземпляры позволяют приложениям в сеансе пользователя взаимодействовать друг с другом.

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

Применения D-Bus

В мире существует очень много технологий, заявленная цель которых - межпроцессное взаимодействие или сеть: CORBA, DCE, DCOM, DCOP, XML-RPC, SOAP, MBUS, Internet Communications Engine (ICE) и наверное еще сотни. Каждый из них предназначен для определенных типов использования. D-Bus разработан для двух конкретных случаев:

  • Связь между настольными приложениями в одном рабочем столе; для обеспечения интеграции сеанса рабочего стола в целом и решения проблем жизненного цикла процесса (когда компоненты рабочего стола запускаются и останавливаются).

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

Для случая использования в рамках сеанса рабочего стола рабочие столы GNOME и KDE имеют значительный предыдущий опыт работы с различными решениями IPC, такими как CORBA и DCOP. D-Bus основан на этом опыте и тщательно адаптирован для удовлетворения потребностей, в частности, таких настольных проектов. D-Bus может подходить или не подходить для других приложений; в FAQ есть некоторые сравнения с другими системами IPC.

Проблема, решаемая общесистемным случаем или случаем связи с ОС, хорошо объясняется следующим текстом из проекта Linux Hotplug:

Пробел в текущей поддержке Linux заключается в том, что в настоящее время не поддерживаются политики с каким-либо динамическим компонентом взаимодействия с пользователем. Например, это часто требуется при первом подключении сетевого адаптера или принтера, а также для определения подходящих мест для монтирования дисков. Казалось бы, такие действия могут поддерживаться в любом случае, когда можно идентифицировать ответственного человека: однопользовательские рабочие станции или любая система, которая администрируется удаленно.

Это классическая проблема удаленного системного администратора, когда в случае горячего подключения должно доставляться событие из его домена безопасности (в данном случае ядра операционной системы) в другой (рабочий стол для вошедшего в систему пользователя или удаленного системного администратора). Любой эффективный ответ должен идти другим путем: удаленный домен предпринимает действия, позволяющие ядру выяснить возможности устройства. (Действие часто может быть выполнено асинхронно, например, позволяя новому оборудованию бездействовать до завершения переговоров.) На момент написания этой статьи в Linux не было широко распространенных решений таких проблем. Однако новые разработки D-Bus могут начать решать эту проблему.

D-Bus может оказаться полезным для целей, отличных от тех, для которых он был разработан. Есть общие свойства, которые отличают его от других вариантов IPC:

  • Двоичный протокол, предназначенный для асинхронного использования (в духе протокола X Window System);

  • Постоянные и надежные соединения остаются открытыми все время;

  • Шина сообщений - это демон, а не рой или распределенная архитектура;

  • Многие вопросы реализации и деплоя описаны, а не остаются неоднозначными / настраиваемыми / подключаемыми.

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

Функции безопасности для поддержки режима общесистемной шины сообщений.

Концепции

Некоторые базовые концепции применимы независимо от того, какую платформу приложения вы используете для написания приложения D-Bus. Однако конкретный код, который вы напишете, будет отличаться для приложений GLib, Qt и Python.

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

Нативные объекты /и пути к объектам

Вероятно, в вашем фреймворке, определено, что такое объект; обычно это базовый класс. Например:java.lang.Object, GObject, QObject,базовый объект Python или что-то еще. Назовем их нативным объектом.

Протокол D-Bus низкого уровня и соответствующий APIlibdbusне оперирует нативными объектами. Однако он предоставляет концепцию, называемую путем к объекту. Идея пути к объекту заключается в том, что привязки более высокого уровня могут давать имена экземплярам собственных объектов и позволяет удаленным приложениям обращаться к ним.

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

/org/kde/kspread/sheet/3/cells/4/5

. Удобочитаемые пути - это хороший тон, но вы можете создать объект с именем

/com/mycompany/c5yo817y0c1y1c5b

, если это имеет смысл для вашего приложения.

Разумно начинать пути к объектам с их пространств имен - с компонентов вашего доменного имени (например,

/org/kde

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

Методы и сигналы

У каждого объекта есть члены. Два вида членов - это методы и сигналы. Методы - это операции, которые могут быть вызваны у объекта, с необязательным вводом (они же аргументы или входные параметры) и выводом (они же возвращаемые значения или исходящие параметры).Сигналы - это широковещательные передачи от объекта всем заинтересованным наблюдателям объекта; сигналы могут содержать полезную нагрузку.

На методы, и сигналы ссылаются по их имени, например Frobate или OnClicked.

Интерфейсы

Каждый объект поддерживает один или несколько интерфейсов. Понимайте интерфейс как именованную группу методов и сигналов, как в GLib, Qt или Java. Интерфейсы определяют тип экземпляра объекта.

DBus идентифицирует интерфейсы с помощью простой строки с именами, например

org.freedesktop.Introspectable

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

Прокси

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

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

Message message = new Message("/remote/object/path", "MethodName", arg1, arg2);          Connection connection = getBusConnection();          connection.send(message);          Message reply = connection.waitForReply(message);          if (reply.isError()) {      } else {        Object returnValue = reply.getReturnValue();      }</pre></div><div class="standard" id="magicparlabel-247" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Программирование с использованием прокси может выглядеть так:</div><div class="float-listings" style="border: 2px solid black; padding: 1ex; margin: 1ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><pre class="listings">Proxy proxy = new Proxy(getBusConnection(), "/remote/object/path");      Object returnValue = proxy.MethodName(arg1, arg2);</pre></div><h2 class="section_" id="magicparlabel-254" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Шинные имена</h2><div class="standard" id="magicparlabel-260" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Когда приложение подключается к демону шины, демон немедленно присваивает ему имя, называемое уникальным именем подключения.<span>&nbsp;</span><a id="magicparlabel-264"></a>Уникальное имя начинается с символа ':' (двоеточия). Эти имена, во время существования шинного демона никогда не используются повторно - то есть вы знаете, что данное имя всегда будет относиться к одному и тому же приложению. Примером уникального имени может быть<pre class="listings">:34-907</pre>. Цифры после двоеточия не имеют иного смысла, кроме их уникальности.</div><div class="standard" id="magicparlabel-269" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Когда имя сопоставляется с подключением определенного приложения, считается, что это приложение<span>&nbsp;</span><span style="font-style: oblique;">владеет</span><span>&nbsp;</span>этим именем.</div><div class="standard" id="magicparlabel-270" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Приложения могут запрашивать дополнительные общеизвестные (<span style="font-style: oblique;">well-known</span>) имена. Например, вы можете написать спецификацию для определения имени<pre class="listings">com.mycompany.TextEditor</pre>. В вашем определении можно указать, что для владения этим именем приложение должно иметь объект с путём<pre class="listings">/com/mycompany/TextFileManager</pre>,</div><div class="standard" id="magicparlabel-279" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">поддерживающий интерфейс<pre class="listings">org.freedesktop.FileHandler</pre>.</div><div class="standard" id="magicparlabel-284" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Затем приложения, для вызова методов, могут отправлять сообщения на это шинное имя, объект и интерфейс.</div><div class="standard" id="magicparlabel-285" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Вы можете думать об уникальных именах как об IP-адресах, а об общеизвестных именах как о доменных именах. Таким образом,<pre class="listings">com.mycompany.TextEditor</pre>может отображаться например как<pre class="listings">:34-907</pre>так же, как<pre class="listings">mycompany.com</pre>соответствовать чему-то вроде<pre class="listings">192.168.0.5</pre>.</div><div class="standard" id="magicparlabel-302" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Имена, помимо маршрутизации сообщений, имеют второе важное применение. Они используются для отслеживания жизненного цикла. Когда приложение завершает работу (или аварийно закрывается), ядро операционной системы закрывается его соединение с шиной сообщений. Затем шина сообщений отправляет сообщения уведомления, информирующие остальные приложения о том, что имена приложения потеряли своего владельца. Отслеживая эти уведомления, ваше приложение может надежно отслеживать время жизни других приложений.</div><div class="standard" id="magicparlabel-303" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Шинные имена также могут использоваться для координации одноэкземплярных приложений. Если, например, вы хотите быть уверенным, что работает только одно приложение<pre class="listings">com.mycompany.TextEditor</pre>, закрывайте приложение текстового редактора, если такое шинное имя уже имеет владельца.</div><h2 class="section_" id="magicparlabel-308" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Адреса</h2><div class="standard" id="magicparlabel-314" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Приложения, использующие D-Bus, являются либо серверами, либо клиентами. Сервер прослушивает входящие соединения; клиент подключается к серверу. Как только соединение установлено, образуется симметричный поток сообщений. Различие клиент-сервер имеет значение только при настройке соединения.</div><div class="standard" id="magicparlabel-315" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Если вы, используете демон шины, ваше приложение будет клиентом демона шины. То есть демон шины прослушивает соединения, а ваше приложение инициирует соединение с демоном шины.</div><div class="standard" id="magicparlabel-316" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">D-Bus адрес указывает, где сервер будет слушать, и куда будет подключаться клиент. Например, адрес<pre class="listings">unix:path=/tmp/abcdef</pre>указывает, что сервер будет прослушивать сокет домена UNIX с путём<pre class="listings">/tmp/abcdef</pre>, и клиент будет подключаться к этому сокету. Адрес может также определять TCP/IP сокеты или любой другой транспорт, который будет определен в будущих итерациях спецификации D-Bus.</div><div class="standard" id="magicparlabel-325" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">При использовании D-Bus с демоном шины сообщений,<span>&nbsp;</span><span style="font-style: oblique;">libdbus</span><span>&nbsp;</span>автоматически обнаруживает адрес сеансового демона шины, считывая переменную среды. Он обнаруживает демон общесистемной шины, проверяя известный путь сокета домена UNIX (хотя вы можете переопределить этот адрес с помощью переменной среды).</div><div class="standard" id="magicparlabel-326" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Если вы используете D-Bus без демона шины, то вам решать, какое приложение будет сервером, а какое - клиентом, а также указать механизм для согласования адреса сервера. Это нетипичный случай.</div><h2 class="section_" id="magicparlabel-327" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Большаяконцептуальная картина</h2><div class="standard" id="magicparlabel-333" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Собирая все эти концепции воедино, для вызова конкретного метода для конкретного экземпляра объекта, необходимо указать несколько вложенных компонентов:</div><div class="standard" id="magicparlabel-334" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Шинное имя указано в квадратных скобках, чтобы указать, что оно необязательно - вы указываете имя только для маршрутизации вызова метода в нужном приложение при использовании демона шины. Если у вас есть прямое соединение с другим приложением, то демон шины отсутствует и шинные имена не используются.</div><div class="float-listings" style="border: 2px solid black; padding: 1ex; margin: 1ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><pre class="listings">Адрес -> [Шинное имя] -> Путь -> Интерфейс -> Метод</pre></div><div class="standard" id="magicparlabel-339" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Интерфейс также не является обязательным, в первую очередь по историческим причинам; DCOP не требует указания интерфейса, вместо этого просто запрещает дублирование имен методов в одном экземпляре объекта. Таким образом, D-Bus позволит вам не указывать интерфейс, но если имя вашего метода неоднозначно, то не определено, какой метод будет вызван.</div><h2 class="section_" id="magicparlabel-340" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><a id="sec______________________"></a>За кулисамисообщения</h2><div class="standard" id="magicparlabel-346" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">D-Bus работает, отправляя сообщения между процессами. Если вы используете привязку достаточно высокого уровня, возможно вам не понадобится работать с сообщениями напрямую.</div><div class="standard" id="magicparlabel-347" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Есть 4 типа сообщений:</div><ul class="itemize" id="magicparlabel-348" style="margin-top: 0.7ex; margin-bottom: 0.7ex; margin-left: 3ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><li class="itemize_item">Сообщения о вызове метода запрашивают вызов метода для объекта;</li><li class="itemize_item">Сообщения о завершении метода возвращают результаты вызова метода;</li><li class="itemize_item">Сообщения об ошибках возвращают исключение, возникшее при вызове метода;</li><li class="itemize_item">Сигнальные сообщения - это уведомления о том, что данный сигнал был послан (что произошло событие). Вы также можете понимать это как сообщения о событиях.</li></ul><div class="standard" id="magicparlabel-352" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Вызов метода очень просто сопоставляется с сообщениями: вы отправляете сообщение о вызове метода и получаете в ответ либо сообщение о завершении метода, либо сообщение об ошибке.</div><div class="standard" id="magicparlabel-353" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">У каждого сообщения есть заголовок, содержащий поля, и тело, включающее аргументы. Вы можете думать о заголовке как о маршрутной информации для сообщения, а о теле - как о полезной нагрузке. Поля заголовка могут включать шинное имя отправителя, шинное имя назначения, имя метода или сигнала и так далее. Одно из полей заголовка - это сигнатура типа, описывающая значения, находящиеся в теле. Например, буква i означает 32-битное целое число, поэтому сигнатура ii означает, что полезная нагрузка содержит два 32-битных целых числа.</div><h2 class="section_" id="magicparlabel-354" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><a id="chap_______________"></a>За кулисамивызова метода</h2><div class="standard" id="magicparlabel-360" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Вызов метода в DBus состоит из двух сообщений; сообщение о вызове метода, отправленное из процесса A в процесс B, и ответное сообщение соответствующего метода, отправленное из процесса B в процесс A. И вызов, и ответное сообщение маршрутизируются через демон шины. Вызывающий включает в каждое сообщение о вызове отличающийся серийный номер, ответное сообщение содержит этот же номер, чтобы вызывающий процесс мог сопоставить ответы с вызовами.</div><div class="standard" id="magicparlabel-361" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Сообщение о вызове метода будет содержать любые аргументы метода. Ответное сообщение может указывать на ошибку или содержать данные, возвращаемые методом.</div><div class="standard" id="magicparlabel-362" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Вызов метода в DBus происходит следующим образом:</div><ul class="itemize" id="magicparlabel-363" style="margin-top: 0.7ex; margin-bottom: 0.7ex; margin-left: 3ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><li class="itemize_item">Привязка языка может предоставлять прокси, так что вызов метода внутрипроцессного объекта вызывает метод удаленного объекта в другом процессе. Если это так, приложение вызывает метод на прокси-сервере, и прокси создает сообщение о вызове метода для отправки удаленному процессу.</li><li class="itemize_item">Для более низкоуровневых API приложение может создать сообщение о вызове метода само, без использования прокси.</li><li class="itemize_item">В любом случае сообщение о вызове метода содержит: шинное имя, принадлежащее удаленному процессу, название метода, аргументы метода, путь к объекту внутри удаленного процесса и, опционально, имя интерфейса, определяющего метод.</li><li class="itemize_item">Сообщение о вызове метода отправляется демону шины.</li><li class="itemize_item">Демон шины просматривает шинное имя назначения. Если это имя принадлежит процессу, демон шины перенаправляет вызов метода этому процессу. В противном случае демон шины создает сообщение об ошибке и отправляет его обратно в качестве ответа на сообщение о вызове метода.</li><li class="itemize_item">Принимающий процесс распаковывает сообщение о вызове метода. В простой ситуации низкоуровневого API он может немедленно запустить метод и отправить ответное сообщение метода демону шины. При использовании API привязки высокого уровня, привязка может проверять путь к объекту, интерфейс и имя метода и преобразовывать сообщение вызова метода в вызов метода для нативного объекта (GObject, java.lang.Object, QObject, и т.д.), а затем преобразовать возвращаемое значение из нативного метода в ответное сообщение метода.</li><li class="itemize_item">Демон шины получает ответное сообщение метода и отправляет его процессу, который вызывал метод.</li><li class="itemize_item">Процесс, вызывавший метод, просматривает ответ метода и использует любые возвращаемые значения, находящиеся в ответе. Ответ также может указывать на то, что произошла ошибка. При использовании привязки ответное сообщение метода может быть преобразовано в возвращаемое значение прокси-метода или в исключение.</li></ul><div class="standard" id="magicparlabel-371" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Демон шины никогда не меняет порядок сообщений. То есть, если вы отправите два сообщения о вызове метода одному и тому же получателю, они будут получены в том порядке, в котором они были отправлены. Однако получатель не обязан отвечать на вызовы по порядку; например, он может обрабатывать каждый вызов метода в отдельном потоке и возвращать ответные сообщения в неопределенном порядке в зависимости от того, в каком порядке завершаются потоки. Вызовы методов имеют уникальный серийный номер, используемый вызывающим методом для сопоставления ответных сообщений с сообщениями вызова.</div><h2 class="section_" id="magicparlabel-372" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">За кулисамиизлучения сигнала</h2><div class="standard" id="magicparlabel-378" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Сигнал в DBus состоит из одного сообщения, отправляемого одним процессом любому количеству других процессов. То есть<span>&nbsp;</span><a id="magicparlabel-382"></a>сигнал - это однонаправленная трансляция. Сигнал может содержать аргументы (полезные данные), но поскольку он является широковещательным, он никогда не имеет возвращаемого значения. Сравните это с вызовом метода (см.<span>&nbsp;</span><a href="#chap_______________">#</a>), где сообщение о вызове метода имеет соответствующее ответное сообщение метода.</div><div class="standard" id="magicparlabel-383" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Эмитент (он же отправитель) сигнала не знает получателей сигнала. Получатели регистрируются с помощью демона шины для получения сигналов на основе правил соответствия - эти правила обычно включают отправителя и имя сигнала. Демон шины отправляет каждый сигнал только тем получателям, которые проявили интерес к этому сигналу.</div><div class="standard" id="magicparlabel-384" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Сигнал в DBus передается следующим образом:</div><ul class="itemize" id="magicparlabel-385" style="margin-top: 0.7ex; margin-bottom: 0.7ex; margin-left: 3ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><li class="itemize_item">Создается сигнальное сообщение и отправляется демону шины. При использовании низкоуровневого API это можно сделать вручную, с некоторыми привязками это может быть сделано за вас с помощью привязки, когда нативный объект испускает нативный сигнал или событие.</li><li class="itemize_item">Сигнальное сообщение содержит имя интерфейса, определяющего сигнал, название сигнала, шинное имя процесса, отправляющего сигнал и любые аргументы.</li><li class="itemize_item">Любой процесс на шине сообщений может зарегистрировать правила сопоставления, указывающие, какие сигналы ему интересны. У шины есть список зарегистрированных правил сопоставления.</li><li class="itemize_item">Демон шины исследует сигнал и определяет, какие процессы в нем заинтересованы. Он отправляет этим процессам сигнальное сообщение.</li><li class="itemize_item">Каждый процесс, получивший сигнал, решает, что с ним делать; при использовании привязки, привязка может излучать нативный сигнал для прокси-объекта. При использовании низкоуровневого API процесс может просто взглянуть на отправителя сигнала и имя и решить, что на основании этого сделать.</li></ul><h2 class="section_" id="magicparlabel-390" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Интроспекция</h2><div class="standard" id="magicparlabel-396" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Объекты D-Bus могут поддерживать интерфейс<pre class="listings">org.freedesktop.DBus.Introspectable</pre>. У этого интерфейса есть один метод<span>&nbsp;</span><span style="font-style: oblique;">Introspect</span>, который не принимает аргументов и возвращает строку XML. Строка XML описывает интерфейсы, методы и сигналы объекта. См. Спецификацию D-Bus для получения более подробной информации об этом формате интроспекции.</div><h2 class="section_" id="magicparlabel-406" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">GLib API</h2><div class="standard" id="magicparlabel-407" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Рекомендуемый GLib API для D-Bus - GDBus, который распространяется с GLib начиная с версии 2.26. Здесь он не описана, для получения подробной информации об использовании GDBus см. Документацию GLib по ссылке:</div><div class="standard" id="magicparlabel-408" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">https:<a href="http://personeltest.ru/aways/developer.gnome.org/gio/stable/gdbus-convenience.html">//developer.gnome.org/gio/stable/gdbus-convenience.html</a></div><div class="standard" id="magicparlabel-409" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Также существует более старый API,<span>&nbsp;</span><span style="font-style: oblique;">dbus-glib</span>. Он устарел и не должен использоваться в новом коде. По возможности также рекомендуется переносить существующий код из<span>&nbsp;</span><span style="font-style: oblique;">dbus-glib</span><span>&nbsp;</span>в GDBus.</div><h2 class="section_" id="magicparlabel-410" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Python API</h2><div class="standard" id="magicparlabel-416" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Python API,<span>&nbsp;</span><span style="font-style: oblique;">dbus-python</span>, теперь документирован отдельно в руководстве<span>&nbsp;</span><span style="font-style: oblique;">dbus-python</span></div><div class="standard" id="magicparlabel-417" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><a href="http://personeltest.ru/away/dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html">http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html</a></div><div class="standard" id="magicparlabel-418" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">(также доступен в<span>&nbsp;</span><span style="font-style: oblique;">doc/tutorial.txt</span><span>&nbsp;</span>и<span>&nbsp;</span><span style="font-style: oblique;">doc/tutorial.html</span>, если он собран с помощью<span>&nbsp;</span><span style="font-style: oblique;">python-documenttils</span>, в исходном дистрибутиве<span>&nbsp;</span><span style="font-style: oblique;">dbus-python</span>).</div><h2 class="section_" id="magicparlabel-419" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Qt API</h2><div class="standard" id="magicparlabel-425" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Привязка Qt для<span>&nbsp;</span><span style="font-style: oblique;">libdbus</span>, QtDBus, распространяется с Qt начиная с версии 4.2. Здесь она не описана. Для получения подробной информации о том, как использовать QtDBus см. документацию Qt:</div><div class="standard" id="magicparlabel-426" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><a href="http://personeltest.ru/away/qt-project.org/doc/qt-5/qtdbus-index.html">http://qt-project.org/doc/qt-5/qtdbus-index.html</a>.</div><div class="standard" id="magicparlabel-427" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"></div><div class="index chapter" style="color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><h1 class="chapter">I</h1></div><!--EndFragment-->
Подробнее..

Авалония для самых маленьких

26.11.2020 12:17:06 | Автор: admin
В свежем превью Rider, помимо прочего, появилась поддержка Авалонии. Авалония это самый крупный .NET фреймворк для разработки кроссплатформенного UI, и его поддержка в IDE отличный повод наконец разобраться, как писать десктопные приложения для любых платформ.

В этой статье я на примере простой задачи по реализации калькулятора покажу:

  • как управлять разметкой,
  • как связывать функциональность с компонентами,
  • как управлять стилями.



Подготовка


Для работы я использовал:


Единственным обязательным инструментов в этом списке является сам дотнет. Остальное можете выбирать сами: любимую операционную систему и IDE (например, тот же Rider).
Для инициализации проекта мы воспользуемся шаблонами .NET приложений для Авалонии. Для этого нам потребуется клонировать репозиторий с шаблонами, а затем установить скачанные шаблоны:

git clone https://github.com/AvaloniaUI/avalonia-dotnet-templates.gitdotnet new --install /path/avalonia-dotnet-templates/

Типы проектов Авалонии
Типы проектов

Теперь, когда шаблоны установлены, мы можем создать новый проект на основе MVVM шаблона Авалонии:

dotnet new avalonia.mvvm -o ACalc

Перейдем в директорию проекта и обновим все версии пакетов на самые новые (на момент написания статьи):

dotnet add package Avalonia --version 0.10.0-preview6dotnet add package Avalonia.Desktop --version 0.10.0-preview6dotnet add package Avalonia.ReactiveUI --version 0.10.0-preview6

Давайте внимательнее посмотрим на структуру проекта, сгенерированную шаблоном:

image

  • В папке Assets хранятся ресурсы, используемые нами в данном проекте. На текущий момент там лежит лого Авалонии, использующееся в качестве иконки приложения.
  • В папку Model мы будем складывать все общие модели, используемые в нашем приложении. На текущий момент она пуста.
  • Папка ViewModels предназначена для хранения логики, которая будет использоваться в каждом из окон. Прямо сейчас в этой папке хранится ViewModel главного окна и базовый класс для всех ViewModel.
  • В папке Views хранится разметка окон (а также code behind файл, в который хоть и можно положить логику, но лучше для этих целей использовать ViewModel). На текущий момент у нас есть только главное окно.
  • App.xaml общий конфиг приложения. Несмотря на то, что он и выглядит как еще одно окно, на самом деле, этот файл служит для задания общих настроек приложения.
  • ViewLocator нам в этот раз не пригодится, так как он используется для создания кастомных контролов. Подробнее о нем можно почитать в документации Авалонии.

Запустим наше приложение командой dotnet run.



Теперь все готово для разработки.

Разметка


Начнем с создания базовой разметки. Перейдем в файл Views/MainWindow.xaml там будет храниться разметка главного окна нашего калькулятора.



В данный момент наша разметка состоит из базовых параметров окна (размеров, иконки и заголовка) и одного блока с текстом. Давайте заменим этот блок с текстом на Grid, который будет служить скелетом нашей разметки. Этот контрол разложит все элементы по порядку, один за другим.

Итак, заменим TextBlock на пустой Grid:

<Grid></Grid>

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

<Grid>    <Grid.RowDefinitions>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="*"></RowDefinition>    </Grid.RowDefinitions></Grid>

Теперь заполним разметку основными компонентами добавим строку меню, базовый экран и вложенный Grid для блока клавиш:

<Grid>    <Grid.RowDefinitions>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="*"></RowDefinition>    </Grid.RowDefinitions>    <!--строка меню-->    <Menu>    </Menu>    <!--Импровизированный экран нашего калькулятора-->    <TextBlock>    </TextBlock>    <!--Grid для клавиш-->    <Grid></Grid></Grid>

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

<Grid>    <Grid.RowDefinitions>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>    </Grid.RowDefinitions>    <Grid.ColumnDefinitions>        <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>    </Grid.ColumnDefinitions>    <Button Grid.Row="0" Grid.Column="0">1</Button></Grid>

Стоит отметить, что элементы внутри Grid могут занимать несколько ячеек. Для этого используются параметры ColumnSpan и RowSpan:

 <Button Grid.Row="3" Grid.Column="3" Grid.ColumnSpan="2">=</Button>

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

Последнее, что нам осталось сделать это задать параметры окна. Установим стартовые и минимальные размеры окна (они задаются в корневом элементе Window).

MinHeight="300"MinWidth="250"Height="300"Width="250"

После добавления всех элементов разметки наше окно калькулятора будет выглядеть так:



Основной функционал


С разметкой закончили, пора реализовать логику!

Начнем с добавления в папку Models нового Enum, который описывает возможные операции:

public enum Operation{    Add,    Subtract,    Multiply,    Divide,    Result}

Теперь перейдем в класс ViewModel/MainWindowViewModel. Здесь будет храниться основная функциональность нашего приложения.

Добавим в файл несколько приватных полей, с которыми мы будем работать:

private double _firstValue;private double _secondValue;private Operation _operation = Operation.Add;

Теперь реализуем основные методы:

  • AddNumber добавляет новую цифру к числу.
  • ExecuteOperation выполняет одну из операций, описанных в енаме Operation.
  • RemoveLastNumber удаляет последнюю введенную цифру.
  • ClearScreen очищает экран калькулятора.

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

Связывание


Теперь, когда у нас готовы и разметка, и логика, пора связать их друг с другом.
В Авалонию по умолчанию включен Reactive UI это фреймворк, предназначенный как раз для связывания View и Model при использовании MVVM. Подробнее о нем вы сможете прочитать на официальном сайте и в документации Авалонии. Конкретно сейчас нас интересует возможность фреймворка обновлять View при изменении данных.

Для хранения актуального значения, выводимого на экране, реализуем свойство ShownValue:

public double ShownValue{    get => _secondValue;    set => this.RaiseAndSetIfChanged(ref _secondValue, value);}

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

Привяжем это свойство к созданному на этапе разметки текстовому полю:

<TextBlock Grid.Row="1" Text="{Binding ShownValue}" />

Благодаря директиве Binding и методу RaiseAndSetIfChanged значение свойства Text в этом поле будет обновляться при каждом изменении значения свойства ShownValue.

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

public ReactiveCommand<int, Unit> AddNumberCommand { get; }public ReactiveCommand<Unit, Unit> RemoveLastNumberCommand { get; }public ReactiveCommand<Operation, Unit> ExecuteOperationCommand { get; }

Команды нужно инициализировать в конструкторе класса, связав их с соответствующими методами:

public MainWindowViewModel(){    AddNumberCommand = ReactiveCommand.Create<int>(AddNumber);    ExecuteOperationCommand = ReactiveCommand.Create<Operation>(ExecuteOperation);    RemoveLastNumberCommand = ReactiveCommand.Create(RemoveLastNumber);}

Теперь обновим разметку кнопок. Например, для клавиши Backspace новая разметка будет выглядеть так:

<Button Grid.Row="3" Grid.Column="2" Command="{Binding RemoveLastNumberCommand}"></Button>

Несколько сложнее дела обстоят с номерными кнопками и кнопками операций. Для них мы должны передать в качестве параметра вводимую цифру или операцию. Для этого в корневом теге Window нам нужно добавить пространство имен System:

xmlns:s="clr-namespace:System;assembly=mscorlib"

А затем обновить разметку кнопок, добавив в них связанный метод и параметр:

<Button Grid.Row="0" Grid.Column="0" Command="{Binding AddNumberCommand}">    <Button.CommandParameter>        <s:Int32>1</s:Int32>    </Button.CommandParameter>     1</Button>

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



Стили


Итак, логика нашего калькулятора полностью реализована, но его визуальная сторона оставляет желать лучшего. Самое время поиграться со стилями!

В Авалонии есть три способа управлять стилями:

  • настроить стили внутри компонента,
  • настроить стили в рамках окна,
  • подключить пакет стилей.

Пройдемся по каждому из них.

Начнем с настройки стилей внутри конкретного компонента. Очевидный претендент на точечные изменения это экран нашего калькулятора. Давайте увеличим для него размер шрифта и перенесем текст вправо.

<TextBlock Grid.Row="1" Text="{Binding ShownValue}" TextAlignment="Right" FontSize="30" />

Теперь поиграемся со стилями в рамках окна. Здесь мы можем изменить вид всех компонентов определенного типа. Например, можно немного раздвинуть кнопки.

<Window.Styles>    <Style Selector="Button">        <Setter Property="Margin" Value="5"></Setter>     </Style></Window.Styles>

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

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



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

dotnet add package Material.Avalonia --version 0.10.3

А теперь обновим файл App.xaml и укажем в нем используемый пакет стилей и его параметры.

<Application ...             xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"             ...>    <Application.Resources>        <themes:BundledTheme BaseTheme="Dark" PrimaryColor="Purple" SecondaryColor="Amber"/>    </Application.Resources>    <Application.Styles>        <StyleInclude Source="avares://Material.Avalonia/Material.Avalonia.Templates.xaml" />    </Application.Styles></Application>

Установленный пакет обновит визуальный стиль нашего приложения, и теперь оно будет выглядеть так:



Такие же пакеты стилей можно создавать самостоятельно их можно использовать внутри вашего проекта или распространять в виде пакета на nuget. Больше информации о стилях и способах управления ими можно найти в документации.

Заключение


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

Все исходники проекта вы можете найти в репозитории на Github.

На этом все! Оставайтесь на связи, мы вернемся со статьями о более продвинутых возможностях Авалонии.
Подробнее..

Задачи и инструментыMLи их практическое применение

26.11.2020 12:17:06 | Автор: admin

Машинное обучение распространившийся термин, но не все понимают его верно. В этом материале эксперты направления аналитических решений ГК КОРУС Консалтинг Алена Гайбатова и Екатерина Степанова расскажут, что же на самом деле такоеmachinelearning(ML), в каких случаях эту технологию стоит использовать в проектах, а также где машинное обучение активно применяется на практике.

Как работают с данными

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

Итак, общее название технологии искусственный интеллект. Он бывает двух типов сильный (он же общий) и слабый. Мы не будем особенно обсуждать сильный ИИ, так как это решения уровня Терминатора. Мы к нему потихонечку приближаемся, но до сих пор он существует только в виде собранных вместе фрагментов слабого ИИ (как, например, в умных колонках).

Намного интереснее поговорить о слабом искусственном интеллекте. Он тоже делится на два типа. Первый экспертные системы, алгоритмы, запрограммированные вручную (например, запрограммированный группой лингвистом алгоритм перевода слов из одного языка в другой).

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

  • модные в 90-е и нулевые data mining и knowledge discovery from database (KDD),

  • datascience, вошедший в обиход ближе к 2010-м,

  • bigdataпопулярная ныне. Единственное исключение, точнее дополнение, которое привносит именно этот термин наличие огромного количества сложноструктурированных данных.

Для разных задач разные алгоритмы

В соответствии с двумя типами слабого ИИ выводы из данных мы можем сделать вручную (при экспертных системах) и с помощью машинного обучения. Оно же в свою очередь подразделяется на два типа: классическийMLиdeeplearning(с использованием глубоких нейронных сетей с большим количеством слоев).

В проектах на базеMLиспользуются модели. Прежде всего, прогнозные, которые отвечают на базовые вопросы: к какой группе относится объект, какое числовое значение у него будет и так далее. В зависимости от того, на какой вопрос мы отвечаем, это могут быть модель классификатора или регресcии.

Классификаторы

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

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

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

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

Регрессоры

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

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

Кластеризация

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

Самый популярный алгоритм кластеризации метод ближайших соседей.Cнова к кошкам. Мы хотим разбить наших зверей на 4 группы. Наши объекты снова точки на плоскости.Мы выбираем случайным образом центры наших групп, затем смотрим расстояние от центра группы до точек, ближайших к этому центру группы. После мы смещаем центры таким образом, чтобы расстояние до точек своей группы оказывалось меньше, чем до точек другой группы. Через нескольких итераций у нас получатся хорошо разделенные группы.

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

Нейронные сети

Первая нейронная сеть появилась еще в 1950-х гг. Сейчас при помощи нейронных сетей можно ответить на любой вопрос, но лишь с одной оговоркой: ответ не всегда можно интерпретировать.

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

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

Обучение с подкреплением

Это и есть тот самый сильный искусственный интеллект, о котором мы уже говорили выше. К примеру, по этому принципу работают беспилотные автомобили.

Система состоит из агента и среды. Для агента задано конечное число операций (на примере машины максимальная скорость, торможение, поворот направо или налево и так далее). После совершения действия агент получает либо вознаграждение, если его действие приводит к правильному выполнению задачи, либо наказание, если действие, наоборот, отдаляет его от выполнения задания.

Мы также пользуемся алгоритмамиUplift, нейролингвистического программирования и рекомендательными моделями.Upliftпозволяет понять, нужно ли коммуницировать с объектом, НЛП использует алгоритмы для анализа текста (к примеру, на этом принципе работает функция подсказки слов в смартфоне), а рекмодели могут быть персонализированными и не персонализированными.

Теория на практике

Посмотрим, как эти модели используются на для решения реальных задач. Мы сформулировали предпосылки для использованияMLв проектах. Безусловно, они не гарантируют стопроцентного успеха, но на старте могут значительно снизить риски.

  • Экономический эффект, который может принести оптимизация бизнес-процесса в несколько процентов;

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

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

Одна из самых успешных отраслей в плане применения машинного обучения это розничная торговля.Cвязано это с тем, что в ней много регулярных процессов

Например, категорийные менеджеры ежедневно занимаются управлением ассортиментом, промоакциями, ценообразованием, прогнозированием спроса, управлением логистикой. Оптимизация на доли процентов даже одного такого бизнес-процесса в масштабе торговой сети приобретает существенный эффект.

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

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

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

Еще две отрасли, в которых распространены проекты на базе искусственного интеллекта, это банки и телекоммуникации. Это и управление клиентскими рисками (кредитный скоринг), и оптимизация регулярных рассылок клиентам. Задачи, стоящие в этих проектах, разношерстны от предсказания оттока клиентов до маркировки клиентов, от кросс-сейл кредитов и депозитов до предсказания крупных транзакций.

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

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

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

Подробнее..

А вы умеете готовить nested_flatbuffers?

26.11.2020 14:19:49 | Автор: admin
У протокола FlatBuffers имеется интересная возможность использовать вложенную структуру внутри другой структуры, но хранить ее, как массив сырых данных. Такая оптимизация позволяет уменьшить затраты на память и производительность при чтении/записи данных. Для этого необходимо использовать специальный атрибут nested_flatbuffers.

Правда, как это часто водится за разработчиками протоколов, на нормальные примеры сил им уже не хватает. И даже на тематических форумах типа stackoverflow, groups.google и т.п. сложно найти полную информацию приходится буквально по крупицам собирать все части паззла, чтобы в конце концов понять, как именно написать рабочий код.

В статье я раскрою проблему подробнее и приведу примеры на C, C++ и Rust.

In concept this is very simple: a nested buffer is just a chunk of binary data stored in a ubyte vector, typically with some convenience methods generated to access a stored buffer. In praxis it adds a lot of complexity.


image

Intro


Во многих проектах часто встречается ситуация, когда необходимо использовать какой-то протокол для передачи данных между компонентами например, по сети. Конечно, можно использовать самописный вариант, но что если в проекте бэкенд на C, сервер на C++, а фронтенд на JS? Одним языком ограничиться уже не получается, и тогда на помощь могут прийти сторонние библиотеки, например Protocol Buffers (или Protobuf) и FlatBuffers (FB), обе от Google.

Как это устроено? Программист создает специальный файл со схемой данных, где описывает, какие типы и структуры будут использоваться в качестве сообщений. Затем с помощью отдельного компилятора протокола генерируются файлы на нужном языке. После чего они импортируются в проект: сгенерированный код содержит необходимые типы, структуры и функции (классы и методы), с помощью которых создается сообщение с данными. После чего производится сериализация это превращение данных в байтовый буффер типа uint8_t*. Этот буффер можно отправлять куда-нибудь по сети, и на приёмной стороне распаковывать обратно в человекочитаемые данные это десериализация.
Для справки: у Protobuf схема хранится в файле формата .proto, компилятор protoc; у FlatBuffers соответственно файлы с расширением .fbs, компилятор flatc.

И хотя FlatBuffers является официально более новым протоколом по сравнению с Protobuf первый релиз в 2014 году против 2008 года соответственно, возможности для написания кода как будто бы сильнее ограничены. Например, из-за отсутствия таких, казалось бы, жизненно важных функций, как CopyMessage, во FlatBuffers приходится долго курить документацию и сгенерированные файлы. С другой стороны FB считается более быстрым в плане сериализации/десериализации, а данные занимают меньше памяти.

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

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

Problem Setting


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

Файл client.fbs:
namespace my_project.client;table Request{    name:string;}table Response{    x:int;    y:int;    z:int;}union CommonMessage{    Request,    Response}table Message{    content:CommonMessage;}root_type Message;

Здесь тип Message является основным для передачи данных. Автоматически сгенерированный код будет храниться в файлах client_reader.h, client_builder.h и client_verifier.h для C, для C++ в client_generated.h и т.д.
В чем недостатки такого подхода? Допустим, клиент отправил на сервер сообщение типа Response, а на сервере его не надо читать только переслать дальше без изменений. Предположим, что сервер использует собственную схему данных.

Файл server.fbs:
namespace my_project.server;table Response{    extra_info: string;    data: my_project.client.Response;}

Получили сообщение my_project.client.Response от клиента, хотим добавить к нему какие-то данные и отправить my_project.server.Response куда-нибудь дальше (например, клиенту на JS). В таком случае придётся собирать это сообщение как-то так (если сервер написан на C++):
void processClientResponse(const my_project::client::Response* msg){    flatbuffers::FlatBufferBuilder fbb;    auto clientResponse = my_project::client::CreateResponse(fbb, msg->x(), msg->y(), msg->z());    auto extraInfo = fbb.CreateString("SomeInfo");    auto serverResp = my_project::server::CreateResponse(fbb, extraInfo, clientResponse);//}

Обратили внимание на clientResponse? Мы пересоздаём заново сообщение, которое только что получили! Причем надо полностью перечислить все поля для копирования из старого в новое. Почему бы просто не написать как-то так: auto clientResponse {*msg} или вообще использовать msg напрямую?
Увы, но так хорошо жить API флэтбуфферов нам просто не позволяет.
А тем более на C, откуда там взяться конструктору копирования.



Итак, в чём именно мы здесь проигрываем:
  • время выполнения программы надо сначала прочитать сообщение, а потом запаковать его обратно
  • время работы программиста затраты на написание кода для перегона сообщения из старого в новое. Я здесь рассмотрел простой пример с типом Response: но что если поля сами являются сложными структурами, а их ещё и много и всё это по новой, да ещё и на другом языке программирования! Бррр, мы разве за этим здесь, в IT?
  • память по сути мы храним внутри Message специальную структуру данных с какими-то внутренними особенностями, которые могут раздувать размер сообщения

И какой выход?

Attribute nested_flatbuffers


На помощь приходят специальные возможности можно заменить тип сообщения на массив байтов [ubyte] и добавить к нему атрибут nested_flatbuffers указывающий на тип, который раньше соответствовал сообщению ну почти. Тогда возвращаясь к схеме client.fbs:
union CommonMessage{    Request,    Response}table MessageHolder{    data: CommonMessage;}table Message{    content: [ubyte](nested_flatbuffer: "MessageHolder");}

Почему нам понадобился MessageHolder, и мы не могли обойтись просто CommonMessage? Дело в том, что nested_flatbuffer не может иметь тип union, поэтому нужна промежуточная обертка.

Хорошо, у нас теперь есть обновлённое сообщение типа Message: но как узнать, что за данные хранятся внутри массива content?
Для этого можно завести вспомогательный enum Type, завернуть его в заголовок Header и добавить как новое поле в типе Message. Перечисление Type будет по сути повторять объединение CommonMessage.
На самом деле промежуточная структура Header в данном примере необязательна, но в общем случае удобна, если вы захотите добавить что-нибудь ещё.

Файл client.fbs (финальные правки):
// остальная часть схемы без измененийenum Type:ubyte{    request,    response}table Header{    type: Type;}table Message{    header: Header;    content: [ubyte](nested_flatbuffer: "MessageHolder");}

Круто! А как этим пользоваться?

It's coding time


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

Как это делается на C


В C используется особый подход в работе с FB: это не ООП язык, у него даже компилятор другой flatcc вместо общего flatcне всем это оказалось удобно). А ещё на C принято использовать специальный макрос для сокращения неймспейса:
#define ns(x) FLATBUFFERS_WRAP_NAMESPACE(my_project_client, x).
Основывался я на этом примере от разработчиков и на этом от одного из пользователей.

Конструирование и сериализация сообщений
#include "client_builder.h"#include "client_verifier.h"#include <stdio.h>int main(int argc, char ** argv){    printf("Testing on C\n");    // builder необходим для сериализации: превращения сообщения в байтовый буффер    flatbuffers_builder_t builder;    flatcc_builder_init(&builder);    size_t size = 0;    void* buf;    {        createRequest(&builder);        buf = flatcc_builder_finalize_aligned_buffer(&builder, &size);        receiveBuffer(buf, size);        flatcc_builder_aligned_free(buf);    }    {        flatcc_builder_reset(&builder);        createResponse(&builder);        buf = flatcc_builder_finalize_aligned_buffer(&builder, &size);        receiveBuffer(buf, size);        flatcc_builder_aligned_free(buf);    }    flatcc_builder_clear(&builder);    return 0;}void createRequest(flatbuffers_builder_t* builder){    ns(Type_enum_t) type = ns(Type_request);    ns(Header_ref_t) header = ns(Header_create(builder, type));    ns(Message_start_as_root(builder));        ns(Message_header_create(builder, type));        ns(Message_content_start_as_root(builder));            flatbuffers_string_ref_t name = flatbuffers_string_create_str(builder, "This is some string for tests!");            ns(Request_ref_t) request = ns(Request_create(builder, name));            ns(CommonMessage_union_ref_t) msg_union = ns(CommonMessage_as_Request(request));            ns(MessageHolder_data_add(builder, msg_union));        ns(Message_content_end_as_root(builder));    ns(Message_end_as_root(builder));}void createResponse(flatbuffers_builder_t* builder){    ns(Type_enum_t) type = ns(Type_response);    ns(Header_ref_t) header = ns(Header_create(builder, type));    ns(Message_start_as_root(builder));        ns(Message_header_create(builder, type));        ns(Message_content_start_as_root(builder));            ns(Response_ref_t) response = ns(Response_create(builder, 2, 3, 9));            ns(CommonMessage_union_ref_t) msg_union = ns(CommonMessage_as_Response(response));            ns(MessageHolder_data_add(builder, msg_union));        ns(Message_content_end_as_root(builder));    ns(Message_end_as_root(builder));}


Обработка полученного буффера и десериализация сообщений
void receiveBuffer(void* buf, size_t size){    const int verification_result = ns(Message_verify_as_root(buf, size));    if (flatcc_verify_error_ok != verification_result) {        printf("Unable to verify flatbuffer message\n");    }    ns(Message_table_t) msg = ns(Message_as_root(buf));    ns(Header_table_t) header = ns(Message_header(msg));    ns(Type_enum_t) type = ns(Header_type(header));    printf("Received Type: %u\n", type);    switch(type) {case ns(Type_request):    processRequest(&msg);    break;case ns(Type_response):    processResponse(&msg);    break;default:       printf("Unknown type!\n");}}void processRequest(ns(Message_table_t)* msg){    ns(MessageHolder_table_t) content = ns(Message_content_as_root(*msg));    ns(Request_table_t) request = (ns(Request_table_t)) ns(MessageHolder_data(content));    const char* name = ns(Request_name(request));    printf("Result request: %s\n", name);}void processResponse(ns(Message_table_t)* msg){    ns(MessageHolder_table_t) content = ns(Message_content_as_root(*msg));    ns(Response_table_t) response = (ns(Response_table_t)) ns(MessageHolder_data(content));    int x = ns(Response_x(response));    int y = ns(Response_y(response));    int z = ns(Response_z(response));    printf("Result response: %d.%d.%d\n", x, y, z);}


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

Как это делается на C++


Конструирование и сериализация сообщений
#include "flatbuffers/flatbuffers.h"#include "client_generated.h"#include "server_generated.h"#include <iostream>namespace cli = my_project::client;namespace srv = my_project::server;void createRequest(flatbuffers::FlatBufferBuilder& fbb){    auto type = cli::Type::request;    auto header = cli::CreateHeader(fbb, type);    flatbuffers::FlatBufferBuilder fbb2;    auto name = fbb2.CreateString("This is some string for tests!");    auto rq = cli::CreateRequest(fbb2, name);    auto data = cli::CreateMessageHolder(fbb2, cli::CommonMessage::Request, rq.Union());    fbb2.Finish(data);    auto content = fbb.CreateVector(fbb2.GetBufferPointer(), fbb2.GetSize());    auto msg = cli::CreateMessage(fbb, header, content);    cli::FinishMessageBuffer(fbb, msg);} void createResponse(flatbuffers::FlatBufferBuilder& fbb){    auto type = cli::Type::response;    auto header = cli::CreateHeader(fbb, type);    flatbuffers::FlatBufferBuilder fbb2;    auto rp = cli::CreateResponse(fbb2, 2, 3, 9);    auto data = cli::CreateMessageHolder(fbb2, cli::CommonMessage::Response, rp.Union());    fbb2.Finish(data);    auto content = fbb.CreateVector(fbb2.GetBufferPointer(), fbb2.GetSize());    auto msg = cli::CreateMessage(fbb, header, content);    cli::FinishMessageBuffer(fbb, msg);}


Обработка полученного буффера и десериализация сообщений
void receiveBuffer(std::uint8_t* buf, std::size_t size){    flatbuffers::Verifier verifier(buf, size);    if (!cli::VerifyMessageBuffer(verifier))    {        std::cerr << "Unable to verify flatbuffer message\n";        return;    }    auto msg = cli::GetMessage(buf);    auto header = msg->header();    auto type = header->type();    std::cout << "Received HeaderType from client: " << static_cast<uint16_t>(type) << "\n";    switch (type)    {        case cli::Type::request:            processRequest (msg);            break;        case cli::Type::response:            processResponse(msg);            break;    }}void processRequest(const cli::Message* msg){    auto content = msg->content_nested_root();    auto rq = content->data_as_Request();    auto name = rq->name();    std::cout << "Result request: " << name->str() <<"\n";}void processResponse(const cli::Message* msg){    auto content = msg->content_nested_root();    auto rp = content->data_as_Response();    auto x = rp->x();    auto y = rp->y();    auto z = rp->z();    std::cout << "Result response: " << x << "."  << y << "." << z <<"\n";}


Код практически не отличается по смыслу от написанного на C, за исключением того, что построение сообщения производится другим способом с помощью дополнительного экземпляра fbb2 типа FlatBufferBuilder для вложенного сообщения. На самом деле разработчики заявляют, что вложенный флэтбуффер можно собирать и так, и так, но в C мне не удалось заставить такую конструкцию работать (а жаль cо вторым экземпляром билдера код выглядит несколько читабельнее).

Level Up


А теперь самое главное для чего всё это было нужно? Как именно воспользоваться преимуществом атрибута nested_flatbuffers?
Рассмотрим вариант, когда C++-сервер использует следующую схему данных:

Файл server.fbs:
include "client.fbs";namespace my_project.server;table ClientData{    extra_info:string;    client_msg:[ubyte](nested_flatbuffer: "my_project.client.MessageHolder");}table ServerData{    server_name:string;}union CommonMessage{    ServerData,    ClientData}table Message{    header: my_project.client.Header;    content: CommonMessage;}root_type Message;

Здесь тоже используется сообщение типа Message, но хранящее просто union со своими собственными типами. Самое главное находится в таблице ClientData: сообщение с информацией от клиента, которое мы хотим переслать на сервере, содержит вложенный флэтбуффер client_msg и он должен быть точно такого же типа, что отправил клиент. Под катом продемонстрировано, как правильно его скопировать не распаковывая (комментарии на русском я делал для статьи, на английском для себя в коде):

example.cpp
// используем глобальный экземпляр для простоты flatbuffers::FlatBufferBuilder fbb_;// общий обработчик сообщения, полученного от клиентаvoid processClientMessage(const cli::Message* msg){    fbb_.Clear();    // constructing srv::Message from cli::Message    switch (msg->header()->type())    {        case cli::Type::request:            forwardMessage(msg, "SERVER-REQUEST");            break;        case cli::Type::response:            forwardMessage(msg, "SERVER-RESPONSE");            break;    }    // processing constructed srv::Message to verify it is correct    processServerMessage();}// Самая интересная часть  пересылка сообщения от клиента без распаковки деталейvoid forwardMessage(const cli::Message* msg, const char* extra_info_str){    // Yes, we should recreate header as FlatBuffers don't have API to just copy it from msg->header()    auto header = cli::CreateHeader(fbb_, msg->header()->type());    auto extra_info = fbb_.CreateString(extra_info_str);    // The main part: copying nested buffer from client to server message    auto client_msg = msg->content();    auto client_msg_vec = fbb_.CreateVector(client_msg->Data(), client_msg->size());    auto content = srv::CreateClientData(fbb_, extra_info, client_msg_vec);    auto server_msg = srv::CreateMessage(fbb_, header, srv::CommonMessage::ClientData, content.Union());    srv::FinishMessageBuffer(fbb_, server_msg);}// пример обработки сообщения, сгенерированного серверомvoid processServerMessage() const{    std::uint8_t* buf = fbb_.GetBufferPointer();    auto msg = srv::GetMessage(buf);    auto header = msg->header();    auto header_type = header->type();    auto content_type = msg->content_type();    std::cout << "Received HeaderType from server: " << static_cast<uint16_t>(header_type) << "\n";    std::cout << "Received ContentType from server: " << static_cast<uint16_t>(content_type) << "\n";    if (content_type != srv::CommonMessage::ClientData)    {        std::cerr << "Not implemented Handler for this content_type\n";        return;    }    // process only ClientData for demonstration purposes    auto content = msg->content_as_ClientData();    auto extra_info = content->extra_info();    auto client_msg = content->client_msg_nested_root();    std::cout << "Result request from server: extra_info: " << extra_info->str() << "\n";    switch(header_type)    {        case cli::Type::request:        {            auto client_rq = client_msg->data_as_Request();            auto client_name = client_rq->name();            std::cout << "- client_msg: " << client_name->str() << "\n";            break;        }        case cli::Type::response:        {            auto client_rp = client_msg->data_as_Response();            auto x = client_rp->x();            auto y = client_rp->y();            auto z = client_rp->z();            std::cout << "- client_msg: " << x << "." << y << "." << z << "\n";            break;        }    }}


Отдельно ещё раз хочу сделать акцент на копировании:
auto client_msg = msg->content();auto client_msg_vec = fbb_.CreateVector(client_msg->Data(), client_msg->size());

И всё! Не надо знать деталей, что именно к нам пришло в msg->content() мы просто берём и копируем сырой буффер как есть.
Красиво и удобно

Bonus


Так случилось, что у меня для вас есть ещё и полноценный пример на Rust. Согласен, внезапно, но почему бы и нет. Сейчас язык набирает обороты, и уже всё чаще случается, что им заменяют C++. Наконец-то, теперь мы будем спасены! Короче говоря, who knows

main.rs
extern crate flatbuffers;#[allow(dead_code, unused_imports, non_snake_case)]#[path = "../../fbs/client_generated.rs"]mod client_generated;pub use client_generated::my_project::client::{    get_root_as_message,    Type,    Request,    Response,    Header,    CommonMessage,    MessageHolder,    Message,    RequestArgs,    ResponseArgs,    HeaderArgs,    MessageHolderArgs,    MessageArgs};fn create_request(mut builder: &mut flatbuffers::FlatBufferBuilder){// в Rust слово type является ключевым, поэтому генератор автоматически добавляет _    let type_ = Type::request;    let header = Header::create(&mut builder, &mut HeaderArgs{type_});    let data_builder = {        let mut b = flatbuffers::FlatBufferBuilder::new();        let name = b.create_string("This is some string for tests!");        let rq = Request::create(&mut b, &RequestArgs{name: Some(name)});        let data = MessageHolder::create(&mut b, &MessageHolderArgs{            data_type: CommonMessage::Request,            data: Some(rq.as_union_value())});        b.finish(data, None);        b    };    let content = builder.create_vector(data_builder.finished_data());    let msg = Message::create(&mut builder, &MessageArgs{        header: Some(header),        content: Some(content)});    builder.finish(msg, None);}fn create_response(mut builder: &mut flatbuffers::FlatBufferBuilder){    let type_ = Type::response;    let header = Header::create(&mut builder, &mut HeaderArgs{type_});    let data_builder = {        let mut b = flatbuffers::FlatBufferBuilder::new();        let rp = Response::create(&mut b, &ResponseArgs{x: 2, y: 3, z: 9});        let data = MessageHolder::create(&mut b, &MessageHolderArgs{            data_type: CommonMessage::Response,            data: Some(rp.as_union_value())});        b.finish(data, None);        b    };    let content = builder.create_vector(data_builder.finished_data());    let msg = Message::create(&mut builder, &MessageArgs{        header: Some(header),        content: Some(content)});    builder.finish(msg, None);}fn process_request(msg: &Message){    let content = msg.content_nested_flatbuffer().unwrap();    let rq = content.data_as_request().unwrap();    let name = rq.name().unwrap();    println!("Result request: {:?}", name);}fn process_response(msg: &Message){    let content = msg.content_nested_flatbuffer().unwrap();    let rp = content.data_as_response().unwrap();    println!("Result response: {}.{}.{}", rp.x(), rp.y(), rp.z());}fn receive_buffer(buf: &[u8]){    // NOTE: no verification exists in Rust yet    let msg = get_root_as_message(buf);    let header = msg.header().unwrap();    let type_ = header.type_();    println!("Received Type: {:?}", type_);    match type_ {        Type::request => {            process_request(&msg);        }        Type::response => {            process_response(&msg);        }    }}fn main() {    println!("Testing on Rust");    let mut builder = flatbuffers::FlatBufferBuilder::new_with_capacity(1024);    {    create_request(&mut builder);    let buf = builder.finished_data();receive_buffer(&buf);    }    {    builder.reset();    create_response(&mut builder);    let buf = builder.finished_data();receive_buffer(&buf);    }}


FIN


Атрибут nested_flatbuffers является очень полезным и удобным способом для оптимизации передачи данных при использовании протокола FlatBuffers. Другое дело, что не так просто понять сходу, как именно его применять.

Вообще документация и примеры от разработчиков тех или иных протоколов иногда оставляют желать лучшего. Синтаксис таких библиотек является довольно специфичным, и по сути на его понимание приходится часто тратить не меньше времени и сил, чем на изучение просто нового языка программирования. Особенно часто это случается, когда упоминаются какие-то редкие возможности.
Например
Похожим образом помимо nested_flatbuffers обстоит дело с использованием собственных аллокаторов в C: Да, юзеры, вы можете использовать свои аллокаторы аж двумя (2!) разными способами, но примеров мы вам, конечно, не дадим. А зачем?! Курите исходники *звуки trolololo
Так что если вы вдруг озадачитесь такой же проблемой, то наверняка наткнётесь на моё обсуждение с разработчиками на одном из форумов, жаль только, что их ответ был крайне развёрнутый, но почти бесполезный.

Поэтому хочется просто пожелать вам поменьше сталкиваться с такими неприятностями, а ещё иногда не жалеть время на написание доков для других программистов мы же ведь коллеги по цеху, да?
Подробнее..
Категории: Программирование , C++ , Rust , C , Flatbuffers

Перевод Расширяемая и удобная в сопровождении архитектура игр на Unity

26.11.2020 14:19:49 | Автор: admin

Будущих студентов курса "Unity Game Developer. Professional" приглашаем посетить открытый вебинар на тему "Продвинутый искусственный интеллект врагов в шутерах".

А пока предлагаем прочитать перевод полезной статьи.


Введение

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

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

Эта статья является обновленной версией моего выступления на GDC в 2017 году (Data Binding Architectures for Rapid UI Creation in Unity).

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

Второй дисклеймер: после того, как эта статья была опубликована, читатели обратили мое внимание, что я не одинок в данном подходе, поскольку Kolibri Games также практикует нечто подобное: их статья

Архитектура

Основными целями этой архитектуры являются:

  • поддерживаемость

  • расширяемость

  • тестируемость

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

  1. Инверсия управления (inversion of control)

  2. Интерфейс передачи сообщений (MPI)

  3. Модель / представление / контроллер (MVC)

  4. Модульное тестирование (Unit testing)

Инверсия управления

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

ClassA напрямую зависит от ServiceA/ServiceB. Это обременяет независимое тестирование ClassA необходимостью заботиться о деталях реализации этих двух служб.

Внедрение зависимостей (DI Dependency Injection) это подход к реализации инверсии управления. На следующем рисунке показан предыдущий пример с использованием внедрения зависимостей:

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

Для реализации этого паттерна мы остановились на Zenject/Extenject. Он основан на рефлексии. Используя функцию запекания рефлексий (reflection-baking), мы можем избавиться от негативного влияния рефлексии на производительность.

Модель-Представление-Контроллер

Суть этой архитектуры разбиение кода на отдельные уровни. Паттерн Модель-Представление-Контроллер (Model-View-Controller MVC), перенесенный на Unity, выглядит следующим образом:

Monobehaviour-ы Unity обитают на уровне представления (View), что, как предполагается, защищает остальную часть архитектуры от затрудняющих модульное тестирование элементов Unity. Этот уровень имеет доступ только к уровню контроллера. Представление создает инстансы префабов и использует [SerializeField] для использования типичных dragndrop компонентов Unity. Здесь не должно быть никакой игровой логики, только чистая визуализация данных.

Уровень контроллера содержит бизнес-логику и выполняет всю тяжелую работу. Этот код должен быть тестируемым, он не зависит от специфики уровня представления Unity. Но все же этот уровень не определяет способ хранения данных на уровне модели, он только контролирует изменения на нем.

Модель содержит фактические данные, они могут быть эфемерными, хранимыми на диске или в каком-либо бэкенде. Обычно модели это старые добрые, хорошо нам известные типы данных.

Поскольку представление не должно запрашивать информацию об изменении данных, для его уведомления мы используем передачу сообщений (Message Passing). Так мы можем сохранять слои обособленными друг от друга и при этом сохранять производительность.

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

Передача сообщений

Вышеупомянутая архитектура полагается на соответствующих уведомлениях (notification messages), чтобы уровень представления мог подписаться и реагировать на изменения/события (events):

Мы используем Zenject Signals.

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

struct MessageType {}bus.Subscribe<MessageType>(()=>Debug.Log("Msg received"));bus.Fire<MessageType>();

Важно отметить, что сигналы (Signals) должны быть легковесными и не содержать данных для этого мы используем остальные уровни MVC. Сигналы это инструмент чисто для уведомления, распространения событий и уменьшения связанности кода.

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

Модульное тестирование

Благодаря всем вышеперечисленным ограничениям и механизмам мы теперь можем покрыть модульными тестами почти всю нашу игровую (бизнес) логику.

Для реализации технической части написания этих тестов мы используем стандартный фреймворк Unity NUnit и NSubstitute в качестве решения для создания моков.

Давайте посмотрим на один из наших тестов:

var level = Substitute.For<ILevel>();var buildings = Substitute.For<IBuildings>();// test subject: var build = new BuildController(null,buildings,level);// smoke testAssert.AreEqual(0, build.GetCurrentBuildCount());// assert that `GetCurrent` was exactly called oncelevel.ReceivedWithAnyArgs(1).GetCurrent();

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

Давайте посмотрим на более интересный пример билдинга чего-либо на слоте 0:

var level = Substitute.For<ILevel>();var bus = _container.Resolve<SignalBus>();var buildCommandSent = false;bus.Subscribe<BuildingBuild>(() => buildCommandSent = true);// test subject var build = new BuildController(bus,new BuildingsModel(),level);// test callbuild.Build(0);Assert.AreEqual(1, build.GetCurrentBuildCount());// assert signals was firedAssert.IsTrue(buildCommandSent);

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

"Погодите-ка, нельзя мокать то, что имеет корни в Zenject?" (что очень метко сказано моим хорошим другом Питером)

Да, к сожалению, SignalBus не имеет интерфейса, который мы можем передать в NSubstitute - поэтому мы должны фактически подписаться и проверить, был ли запущен правильный сигнал.

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

Заключение

Это было всего лишь взгляд с высоты птичьего полета на эту тему. Но подведем итоги:

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

В будущих статьях мы напишем конкретный пример игры, чтобы применить все это на практике, и, кроме того, посмотрим, как объединить эту архитектуру с:

  • практическим примером применения этих подходов,

  • мокингом сцены для тестирования пользовательского интерфейса

  • фейковыми бэкендами и сторонними SDK

  • промисами для поддерживаемого асинхронного кода


- Узнать подробнее о курсе "Unity Game Developer. Professional" и карьерных перспективах.

- Зарегистрироваться на бесплатный вебинар на тему "Продвинутый искусственный интеллект врагов в шутерах" .


Подробнее..

Проектируем мульти-парадигменный язык программирования. Часть 4 Основные конструкции языка моделирования

26.11.2020 14:19:49 | Автор: admin
Продолжаем рассказ о создании мульти-парадигменного языка программирования, сочетающего декларативный стиль с объектно-ориентированным и функциональным, который был бы удобен при работе со слабоструктурированными данными и интеграции данных из разрозненных источников. Наконец-то после введения и обзоров существующих мульти-парадигменных технологий и языков представления знаний мы добрались до описания той части гибридного языка, которая ответственна за описание модели предметной области. Я назвал ее компонентой моделирования.
Компонента моделирования предназначена для декларативного описания модели предметной области в форме онтологии сети из экземпляров данных (фактов) и абстрактных понятий, связанных между собой с помощью отношений. В ее основе лежит фреймовая логика гибрид объектно-ориентированного подхода к представлению знаний и логики первого порядка. Ее основной элемент понятие, описывающее моделируемый объект с помощью набора атрибутов. Понятие строится на основе других понятий или фактов, исходные понятия назовем родительскими, производное дочерним. Отношения связывают значения атрибутов дочернего и родительских понятий или ограничивают их возможные значения. Я решил включить отношения в состав определения понятия, чтобы вся информация о нем находилась по возможности в одном месте. Стиль синтаксиса для определений понятий будет похож на SQL атрибуты, родительские понятия и отношения между ними должны быть разнесены по разным секциям.
В этой публикации я хочу представить основные способы определения понятий.

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

Начнем с фактов.


Факты представляют собой описание конкретных знаний о предметной области в виде именованного набора пар ключ-значение:
fact <имя факта> {
<имя атрибута> : <значение атрибута>
...
}

Например:
fact product {
name: Cabernet Sauvignon,
type: red wine,
country: Chile
}


Имя факта может быть не уникальным, например, может быть множество продуктов с разным названием, типом и страной происхождения. Будем считать факты идентичными, если совпадают их имена, а также имена и значения их атрибутов.
Можно провести аналогию между фактами компоненты моделирования и фактами языка Prolog. Их отличие заключается только в синтаксисе. В Prolog аргументы фактов идентифицируются по их позиции, а атрибуты фактов компоненты моделирования по имени.

Понятия.


Понятие представляет собой структуру, описывающую абстрактную сущность и основанную на других понятиях и фактах. Определение понятия включает в себя имя, списки атрибутов и дочерних понятий. А также логическое выражение, описывающее зависимости между его (дочернего понятия) атрибутами и атрибутами родительских понятий, позволяющие вывести значение атрибутов дочернего понятия:
concept <имя понятия> <псевдоним понятия> (
<имя атрибута> = <выражение>,
...
)
from
<имя родительского понятия> <псевдоним родительского понятия> (
<имя атрибута> = <выражение>
...
),

where <выражение отношений>

Пример определения понятия profit на основе понятий revenue и cost:
concept profit p (
value = r.value c.value,
date
) from revenue r, cost c
where p.date = r.date = c.date


Определение понятия похоже по форме на SQL запрос, но вместо имени таблиц нужно указывать имена родительских понятий, а вместо возвращаемых столбцов атрибуты дочернего понятия. Кроме того, понятие имеет имя, по которому к нему можно обращаться в определениях других понятий или в запросах к модели. Родительским понятием может быть как непосредственно понятие, так и факты. Выражение отношений в секции where это булево выражение, которое может включать логические операторы, условия равенства, арифметические операторы, вызовы функций и др. Их аргументами могут быть переменные, константы и ссылки на атрибуты как родительских так и дочернего понятий. Ссылки на атрибуты имеют следующий формат:
<псевдоним понятия>.<имя атрибута>
По сравнению с фреймовой логикой в определении понятия его структура (атрибуты) объединена с отношениями с другими понятиями (родительские понятия и выражение отношений). С моей точки зрения это позволяет сделать код более понятным, так как вся информация о понятии собрана в одном месте. А также соответствует принципу инкапсуляции в том смысле, что детали реализации понятия скрыты внутри его определения. Для сравнения небольшой пример на языке фреймовой логики можно найти в прошлой публикации.
Выражение отношений имеет конъюнктивную форму (состоит из выражений, соединенных логическими операциями AND) и должно включать условия равенства для всех атрибутов дочернего понятия, достаточные для определения их значений. Кроме того, в него могут входить условия, ограничивающие значения родительских понятий или связывающие их между собой. Если в секции where будут связаны между собой не все родительские понятия, то механизм логического вывода вернет все возможные комбинации их значений в качестве результата (аналогично операции FULL JOIN языка SQL).
Для удобства часть условий равенства атрибутов может быть вынесена в секции атрибутов дочернего и родительских понятий. Например, в определении понятия profit условие для атрибута value вынесено в секцию атрибутов, а для атрибута date оставлено в секции where. Также можно перенести их и в секцию from:
concept profit p (
value = r.value c.value,
date = r.date
) from revenue r, cost c (date = r.date)

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

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

Поскольку список родительских понятий и условия отношений разнесены по отдельным секциям, логический вывод будет немного отличаться от такового в Prolog. Опишу в общем виде его алгоритм. Вывод родительских понятий будет выполнен в том порядке, в котором они указаны в секции from. Поиск решения для следующего понятия выполняется для каждого частичного решения предыдущих понятий так же, как и в SLD резолюции. Но для каждого частичного решения выполняется проверка истинности выражения отношений из секции where. Поскольку это выражение имеет форму конъюнкции, то каждое подвыражение проверяется отдельно. Ели подвыражение ложно, то данное частичное решение отвергается и поиск переходит к следующему. Если часть аргументов подвыражения еще не определена (не связана со значениями), то его проверка откладывается. Если подвыражением является оператор равенства и определен только один из его аргументов, то система логического вывода найдет его значение и попытается связать его с оставшимся аргументом. Это возможно, если свободным аргументом является атрибут или переменная.
Например, при выводе сущностей понятия profit сначала будут найдены сущности понятия revenue, и, соответственно, значения его атрибутов. После чего равенство p.date = r.date = c.date в секции where даст возможность связать со значениями атрибуты date и других понятий. Когда логический поиск доберется до понятия cost, значение его атрибута date будет уже известно и будет является входным аргументом для этой ветви дерева поиска. Подробно рассказать об алгоритмах логического вывода я планирую в одной из следующих публикаций.
Отличие от Prolog заключается в том, что в правилах Prolog все является предикатами и обращения к другим правилам и встроенные предикаты равенства, сравнения и др. И порядок их проверки нужно указывать явным образом, например, сначала должны идти два правила а затем равенство переменных:
profit(value,date) :- revenue(rValue, date), cost(cValue, date), value = rValue cValue
В таком порядке они и будут выполнены. В компоненте моделирования же предполагается, что все вычисления условий в секции where являются детерминированными, то есть не требуют рекурсивного погружения в следующую ветвь поиска. Поскольку их вычисление зависит только от их аргументов, они могут быть вычислены в произвольном порядке по мере связывания аргументов со значениями.

В результате логического вывода все атрибуты дочернего понятия должны быть связаны со значениями. А также выражение отношений должно быть истинно и не содержать неопределенных подвыражений. Стоит заметить, что вывод родительских понятий не обязательно должен быть успешным. Допустимы случаи, когда требуется проверить неудачу выведения родительского понятия из исходных данных, например, в операциях отрицания. Порядок родительских понятий в секции from определяет порядок обхода дерева решений. Это дает возможность оптимизировать поиск решения, начав его с тех понятий, которые сильнее ограничивают пространство поиска.
Задача логического вывода найти все возможные подстановки атрибутов дочернего понятия и каждую из них представить в виде объекта. Такие объекты считаются идентичными если совпадают имена их понятий, имена и значения атрибутов.

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

Наследование понятий.


Одними из наиболее распространенных отношений между понятиями являются иерархические отношения, например род-вид. Их особенностью является то, что структуры дочернего и родительского понятий будут очень близки. Поэтому поддержка механизма наследования на уровне синтаксиса очень важна, без нее программы будут переполнены повторяющимся кодом. При построении сети понятий было бы удобно повторно использовать как их атрибуты, так и отношения. Если список атрибутов легко расширять, сокращать или переопределять некоторые из них, то с модификацией отношений дело обстоит сложнее. Поскольку они представляют собой логическое выражение в конъюнктивной форме, то к нему легко прибавить дополнительные подвыражения. Но удаление или изменение может потребовать значительного усложнения синтаксиса. Польза от этого не так очевидна, поэтому отложим эту задачу на будущее.
Объявить понятие на основе наследования можно с помощью следующей конструкции:
concept <имя понятия> <псевдоним понятия> is
<имя родительского понятия> <псевдоним родительского понятия> (
<имя атрибута> = <выражение>,
...
),
...
with <имя атрибута> = <выражение>, ...
without <имя родительского атрибута>, ...
where <выражение отношений>

Секция is содержит список наследуемых понятий. Их имена можно указать напрямую в этой секции. Или же указать полный список родительских понятий в секции from, а в is псевдонимы только тех из них, которые будут наследоваться:
concept <имя понятия> <псевдоним понятия> is
<псевдоним родительского понятия>,

from
<имя родительского понятия> <псевдоним родительского понятия> (
<имя атрибута> = <выражение>
...
),

with <имя атрибута> = <выражение>, ...
without <имя родительского атрибута>, ...
where <выражение отношений>

Секция with позволяет расширить список атрибутов наследуемых понятий или переопределить некоторые из них, секция without сократить.

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

Рассмотрим несколько примеров использования механизма наследования. Наследование позволяет создать понятие на основе уже существующего избавившись от тех атрибутов, которые имеют смысл только для родительского, но не для дочернего понятия. Например, если исходные данные представлены в виде таблицы, то ячейкам определенных столбцов можно дать свои имена (избавившись от атрибута с номером столбца):
concept revenue is tableCell without columnNum where columnNum = 2
Также можно преобразовать несколько родственных понятий в одну обобщенную форму. Секция with понадобится для того, чтобы преобразовать часть атрибутов к общему формату и добавить недостающие. Например, исходными данными могут быть документы разных версий, список полей которых менялся со временем:
concept resume is resumeV1 with skills = 'N/A'
concept resume is resumeV2 r with skills = r.coreSkills

Предположим, что в первой версии понятия Резюме не было атрибута с навыками, а во второй он назывался по-другому.
Расширение списка атрибутов может потребоваться во многих случаях. Распространенными задачами являются смена формата атрибутов, добавление атрибутов, функционально зависящих от уже существующих атрибутов или внешних данных и т.п. Например:
concept price is basicPrice with valueUSD = valueEUR * getCurrentRate('USD', 'EUR')
Также можно просто объединить несколько понятий под одним именем не меняя их структуру. Например, для того чтобы указать, что они относятся к одному роду:
concept webPageElement is webPageLink
concept webPageElement is webPageInput

Или же создать подмножество понятия, отфильтровав часть его сущностей:
concept exceptionalPerformer is employee where performanceEvaluationScore > 0.95

Возможно также множественное наследование, при котором дочернее понятие наследует атрибуты всех родительских понятий. При наличии одинаковых имен атрибутов приоритет будет отдан тому родительскому понятию, которое находится в списке левее. Также можно решить этот конфликт вручную, явно переопределив нужный атрибут в секции with. Например, такой вид наследования был бы удобен, если нужно собрать в одной плоской структуре несколько связанных понятий:
concept employeeInfo is employee e, department d where e.departmentId = d.id

Наследование без изменения структуры понятий усложняет проверку идентичности объектов. В качестве примера рассмотрим определение exceptionalPerformer. Запросы к родительскому (employee) и дочернему (exceptionalPerformer) понятиям вернут одну и ту же сущность сотрудника. Объекты, представляющие ее, будут идентичны по смыслу. У них будет общий источник данных, одинаковый список и значения атрибутов, на разное имя понятия, зависящее от того, к какому понятию был выполнен запрос. Поэтому операция равенства объектов должна учитывать эту особенность. Имена понятий считаются равными, если они совпадают или связаны транзитивным отношением наследования без изменения структуры.

Наследование это полезный механизм, позволяющий выразить явным образом такие отношения между понятиями как класс-подкласс, частное-общее, множество-подмножество. А также избавиться от дублирования кода в определениях понятий и сделать код более понятным. Механизм наследования основан на добавлении/удалении атрибутов, объединении нескольких понятий под одним именем и добавлении условий фильтрации. Никакой специальной семантики в него не вкладывается, каждый может воспринимать и применять его как хочет. Например, построить иерархию от частного к общему как в примерах с понятиями resume, price и webPageElement. Или, наоборот, от общего к частному, как в примерах с понятиями revenue и exceptionalPerformer. Это позволит гибко подстроиться под особенности источников данных.

Понятие для описания отношений.


Было решено, что для удобства понимания кода и облегчения интеграции компоненты моделирования с ООП моделью, отношения дочернего понятия с родительскими должны быть встроены в его определение. Таким образом, эти отношения задают способ получения дочернего понятия из родительских. Если модель предметной области строится слоями, и каждый новый слой основан на предыдущем, это оправдано. Но в некоторых случаях отношения между понятиями должны быть объявлены отдельно, а не входить в определение одного из понятий. Это может быть универсальное отношение, которое хочется задать в общем виде и применить к разным понятиям, например, отношение Родитель-Потомок. Либо отношение, связывающее два понятия, необходимо включить в определение обоих понятий, чтобы можно было бы найти как сущности первого понятия при известных атрибутах второго, так и наоборот. Тогда, во избежание дублирования кода отношение удобно будет задать отдельно.
В определении отношения необходимо перечислить входящие в него понятия и задать логическое выражение, связывающее их между собой:
relation <имя отношения>
between <имя вложенного понятия> <псевдоним вложенного понятия> (
<имя атрибута> = <выражение>,
...
),
...
where <логическое выражение>

Например, отношение, описывающее вложенные друг в друга прямоугольники, можно определить следующим образом:
relation insideSquareRelation between square inner, square outer
where inner.xLeft > outer.xLeft and inner.xRight < outer.xRight
and inner.yBottom > outer.yBottom and inner.yUp < outer.yUp

Такое отношение, по сути, представляет собой обычное понятие, атрибутами которого являются сущности вложенных понятий:
concept insideSquare (
inner = i
outer = o
) from square i, square o
where i.xLeft > o.xLeft and i.xRight < o.xRight
and i.yBottom > o.yBottom and i.yUp < o.yUp


Отношение можно использовать в определениях понятий наряду с другими родительскими понятиями. Понятия, входящие в отношения, будут доступны извне и будут играть роль его атрибутов. Имена атрибутов будут соответствовать псевдонимам вложенных понятий. В следующем примере утверждается, что в HTML форму входят те HTML элементы, которые расположены внутри нее на HTML странице:
сoncept htmlFormElement is e
from htmlForm f, insideSquareRelation(inner = e, outer = f), htmlElement e

При поиске решения сначала будут найдены все значения понятия htmlForm, затем они будут связаны со вложенным понятием outer отношения insideSquare и найдены значения его атрибута inner. А в конце будут отфильтрованы те значения inner, которые относятся к понятию htmlElement.

Отношению можно придать и функциональную семантику использовать его как функцию булева типа для проверки, выполняется ли отношение для заданных сущностей вложенных понятий:
сoncept htmlFormElement is e
from htmlElement e, htmlForm f
where insideSquareRelation(e, f)

В отличие от предыдущего случая, здесь отношение рассматривается как функция, что повлияет на порядок логического вывода. Вычисление функции будет отложено до того момента, когда все ее аргументы будут связаны со значениями. То есть, сначала будут найдены все комбинации значений понятий htmlElement и htmlForm, а затем отфильтрованы те из них, которые не соответствуют отношению insideSquareRelation. Более подробно об интеграции логической и функциональной парадигм программирования я планирую рассказать в одной из следующих публикаций.

Теперь пришло время рассмотреть небольшой пример.


Определений фактов и основных видов понятий достаточно, чтобы реализовать пример с должниками из первой публикации. Предположим, что у нас есть два файла в формате CSV, хранящих информацию о клиентах (идентификатор клиента, имя и адрес электронной почты) и счетах (идентификатор счета, идентификатор клиента, дата, сумма к оплате, оплаченная сумма).
А также имеется некая процедура, которая считывает содержимое этих файлов и преобразовывает их в набор фактов:
fact cell {
table: TableClients,
value: 1,
rowNum: 1,
columnNum: 1
};
fact cell {
table: TableClients,
value: John,
rowNum: 1,
columnNum: 2
};
fact cell {
table: TableClients,
value: john@somewhere.net,
rowNum: 1,
columnNum: 3
};

fact cell {
table: TableBills,
value: 1,
rowNum: 1,
columnNum: 1
};
fact cell {
table: TableBills,
value: 1,
rowNum: 1,
columnNum: 2
};
fact cell {
table: TableBills,
value: 2020-01-01,
rowNum: 1,
columnNum: 3
};
fact cell {
table: TableBills,
value: 100,
rowNum: 1,
columnNum: 4
};
fact cell {
table: TableBills,
value: 50,
rowNum: 1,
columnNum: 5
};


Для начала дадим ячейкам таблиц осмысленные имена:
concept clientId is cell where table = TableClients and columnNum = 1;
concept clientName is cell where table = TableClients and columnNum = 2;
concept clientEmail is cell where table = TableClients and columnNum = 3;
concept billId is cell where table = TableBills and columnNum = 1;
concept billClientId is cell where table = TableBills and columnNum = 2;
concept billDate is cell where table = TableBills and columnNum = 3;
concept billAmountToPay is cell where table = TableBills and columnNum = 4;
concept billAmountPaid is cell where table = TableBills and columnNum = 5;


Теперь можно объединить ячейки одной строки в единый объект:
concept client (
id = id.value,
name = name.value,
email = email.value
) from clientId id, clientName name, clientEmail email
where id.rowNum = name.rowNum = email.rowNum;


concept bill (
id = id.value,
clientId = clientId.value,
date = date.value,
amountToPay = toPay.value,
amountPaid = paid.value
) from billId id, billClientId clientId, billDate date, billAmountToPay toPay, billAmountPaid paid
where id.rowNum = clientId.rowNum = date.rowNum = toPay.rowNum = paid.rowNum;


Введем понятия Неоплаченный счет и Должник:
concept unpaidBill is bill where amountToPay > amountPaid;
concept debtor is client c where exist(unpaidBill {clientId: c.id});


Оба определения используют наследование, понятие unpaidBill является подмножеством понятия bill, debtor понятия client. Определение понятия debtor содержит вложенный запрос к понятию unpaidBill. Подробно механизм вложенных запросов мы рассмотрим позже в одной следующих публикаций.
В качестве примера плоского понятия определим также понятие Долг клиента, в котором объединим некоторые поля из понятия Клиент и Счет:
concept clientDebt (
clientName = c.name,
billDate = b.date,
debt = b. amountToPay b.amountPaid
) from unpaidBill b, client c(id = b.client);


Зависимость между атрибутами понятий client и bill вынесена в секцию from, а зависимости дочернего понятия clientDebt в секцию его атрибутов. При желании все они могут быть помещены в секцию where результат будет аналогичным. Но с моей точки зрения текущий вариант более краток и лучше подчеркивает назначение этих зависимостей определять связи между понятиями.

Теперь попробуем определить понятие злостного неплательщика, который имеет как минимум 3 неоплаченных счета подряд. Для этого понадобится отношение, позволяющее упорядочить счета одного клиента по их дате. Универсальное определение будет выглядеть следующим образом:
relation billsOrder between bill next, bill prev
where next.date > prev.date and next.clientId = prev.clientId and not exist(
bill inBetween
where next.clientId = inBetween.clientId
and next.date > inBetween.date > prev.date
);

В нем утверждается, что два счета идут подряд, если они принадлежат одному клиенту, дата одного больше даты другого и не существует другого счета, лежащего между ними. На данном этапе я не хочу останавливаться на вопросах вычислительной сложности такого определения. Но если, например, мы знаем, что все счета выставляются с интервалом в 1 месяц, то можно его значительно упростить:
relation billsOrder between bill next, bill prev
where next.date = prev.date + 1 month and next.clientId = prev.clientId;


Последовательность из 3х неоплаченных счетов будет выглядеть следующим образом:
concept unpaidBillsSequence (clientId = b1.clientId, bill1 = b1, bill2 = b2, bill3 = b3)
from
unpaidBill b1,
billsOrder next1 (next = b1, prev = b2)
unpaidBill b2
billsOrder next2 (next = b2, prev = b3)
unpaidBill b3;

В этом понятии сначала будет найдены все неоплаченные счета, затем для каждого из них с помощью отношения next1 будет найден следующий счет. Понятие b2 позволит проверить, что этот счет является неоплаченным. По этому же принципу с помощью next2 и b3 будет найден и третий неоплаченный счет подряд. Идентификатор клиента вынесен в список атрибутов отдельно, чтобы в дальнейшем облегчить связывание этого понятия с понятием клиентов:
concept hardCoreDefaulter is client c where exist(unpaidBillsSequence{clientId: c.id});

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

Краткие выводы.


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

Эти три вида понятий имеют разную форму и назначение, но внутренняя логика поиска решений у них одинакова, различается только способ формирования списка атрибутов.
Определения понятий напоминают SQL запросы как по форме, так и по внутренней логике исполнения. Поэтому я надеюсь, что предложенный язык будет понятен разработчикам и иметь относительно низкий порог входа. А дополнительные возможности, такие как использование понятий в определениях других понятий, наследование, вынесенные отношения и рекурсивные определения позволят выйти за рамки SQL и облегчат структурирование и повторное использование кода.

В отличии от языков RDF и OWL, компонента моделирования не делает различия между понятиями и отношениями все является понятиями. В отличии от языков фреймовой логики фреймы, описывающие структуру понятия, и правила, задающие связи между ними, объединены вместе. В отличии от традиционных языков логического программирования, таких как Prolog основным элементом модели являются понятия, имеющие объектно-ориентированную структуру, а не правила, имеющие плоскую структуру. Такой дизайн языка может быть не так удобен для создания масштабных онтологий или набора правил, но зато гораздо лучше подходит для работы со слабоструктурированными данными и для интеграции разрозненных источников данных. Понятия компоненты моделирования близки к классам ООП модели, что должно облегчить задачу включения декларативного описания модели в код приложения.

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

Полный текст в научном стиле на английском языке доступен по ссылке: papers.ssrn.com/sol3/papers.cfm?abstract_id=3555711

Ссылки на предыдущие публикации:
Проектируем мульти-парадигменный язык программирования. Часть 1 Для чего он нужен?
Проектируем мульти-парадигменный язык программирования. Часть 2 Сравнение построения моделей в PL/SQL, LINQ и GraphQL
Проектируем мульти-парадигменный язык программирования. Часть 3 Обзор языков представления знаний
Подробнее..

NaN все еще может немного удивить

26.11.2020 18:13:16 | Автор: admin
image

Сначала, я подумал, что это очередной вопрос из тех, которые могут задаваться на собеседовании. Наверное, если как следует пораскинуть мозгами, то можно догадаться до того, каким будет результат. Откинувшись на спинку кресла, начал размышлять, включать логику, вспоминать что-нибудь, на что можно опереться в рассуждениях. Но тщетно! Вдруг стало совершенно очевидно, что найти ответ не удается. Но почему? В чем нужно разбираться, чтобы он был найден? В математике? В языке программирования?



Ответ должен быть NaN. Но почему я не уверен в этом? Всю дорогу была уверенность в том, что любые выражения, содержащие NaN, вернут NaN. Ну разве что только если поделить NaN на ноль в этом случае будет вызвано исключение ZeroDivisionError. Сто процентов NaN! Ввожу выражение в ячейку блокнота:
>>> 1**nan + 1**nan2.0

В самом деле? Постойте:
>>> arange(5)**nanarray([nan,  1., nan, nan, nan])

То есть, по какой-то причине, единица в степени NaN это единица, а вот ноль и все остальные числа в степени NaN это NaN. Где логика? В чем дело?

Так, давайте еще раз:
>>> 0**nan, 1**nan(nan, 1.0)


Может быть я просто из-за отсутствия какой-то практической надобности в глубоких познаниях о NaN, просто о чем-то не подозревал? А может я знал, но забыл? А может еще хуже я не знал и забыл?

Заходим на Википедию. Там данный вопрос тоже обозначен как проблема, но почему все именно так устроено, никак не объясняется. Зато узнал что:
>>> hypot(inf, nan)inf

Хотя, в то же время:
>>> sqrt(inf**2 + nan**2)nan

Что, согласитесь, тоже немного странно.

Ладно, с Википедии отправляемся в C99 на 182 страницу и наконец-то получаем логическое объяснение, почему pow(x, 0) возвращает 1 для любых x, даже для x равного NaN:
>>> power(nan, 0)1.0

Если функция $f(x)$ возводится в степень $g(x)$ и при этом $g(x)$ стремится к 0, то в результате получится 1, вне зависимости от того, какое значение имеет $f(x)$.
image
А если результат не зависит от числового значения функции $f(x)$, то 1 является подходящим результатом, даже для NaN. Однако это по-прежнему не объясняет, почему 1 в степени NaN равна 1.

Отыскиваем еще один C99 и на 461 странице не видим никаких объяснений, просто требование того, что pow(+1, y) должно возвращать 1 для всех y, даже равных NaN. Все.

С другой стороны, объяснение, почему pow(NaN, 0)=1 является более предпочтительным, чем pow(NaN, 0)=NaN все-таки наталкивает на мысль о том, что NaN не стоит воспринимать буквально, как Not-a-Number. Допустим, в результате каких- то вычислений мы получили число, превышающее размер памяти, выделенный под данный тип чисел, например:
>>> a = pi*10e307>>> ainf

В результате мы получили inf, что именно это за число мы не знаем, но все же это какое-то число. Затем мы снова что-то вычислили и снова получили слишком большое число:
>>> b = e*10e307>>> binf

Разность a и b вернет NaN:
>>> c = a - b>>> cnan

Единственная причина, по которой мы можем считать c не числом, заключается в том, что мы использовали недостаточно точные вычисления. Однако, в c под NaN все же скрывается какое-то значение. О том, что это за значение, мы не знаем. Но все же это число, а раз это число, то нет ничего удивительного в том, что pow(1, NaN)=1.

Почему же тогда pow(0, NaN)=NaN? Дело в том, что если возвести 0 в любую степень, то мы действительно получим ноль. Кроме одного единственного случая когда степень равна 0:
>>> 0**01

Из-за чего в выражении pow(0, NaN) появляется неопределенность с конкретным значением NaN. Конечно, вероятность того, что под NaN может скрываться 0 исчезающе мала и можно было бы принять, что pow(0, NaN)=0. Но все же лучше перестраховаться, мало ли к чему это может привести. Возможно, так и рассуждали, когда создавались стандарты.

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

P.S. Поскольку NaN относится к числам с плавающей точкой, оно может быть ключом словаря:
>>> d = {0.1: 'a', nan: 'b'}>>> d[nan]'b'

Имеет ли смысл исользовать такое на практике? Думаю, что лучше не стоит.
Подробнее..

Шаблоны проектирования в Go Абстрактная Фабрика

26.11.2020 18:13:16 | Автор: admin

Привет, Хабр! Представляю вашему вниманию перевод очередной статьи Design Patterns: Abstract Factory Pattern автора Shubham Zanwar.

Абстрактная фабрика - это порождающий шаблон проектирования. Он используется, когда нам нужно создать семейство похожих продуктов. Давайте для понимания возьмем пример сети пиццерий.

Пиццерия

Представим, что вы глава бизнеса и открываете сеть пиццерий по всему городу. Одной из ваших обязанностей является производство всех основных продуктов (в нашем случае это пиццы и жареный чесночный хлеб), которые будут представлять такие бренды как Домино и Жаровня (назовем их так - прим. перев.).

Есть множество способов как это сделать. Самый простой - создать фабрику по производству пицц каждого бренда и еще похожую фабрику для жаренного хлеба.

Если вы еще не представляете, как работают фабрики, вы можете почитать здесь

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

Не волнуйтесь, есть простой способ.

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

Открыв пиццерию, вы передаете менеджеру фабрику Домино или Жаровню и можете отдохнуть, потому что теперь никто ничего не перепутает.

Давайте посмотрим на код. Перед тем как мы напишем фабрики, создадим сами продукты:

Обычная пицца

type iPizza interface {    GetPrice() float64    GetName() string    GetToppings() []string}type pizza struct {    name     string    price    float64    toppings []string}func (p *pizza) GetName() string {    return p.name}func (p *pizza) GetPrice() float64 {    return p.price}func (p *pizza) GetToppings() []string {    return p.toppings}

Пиццы наших брендов

type pizzaHutPizza struct {    pizza}type dominosPizza struct {    pizza}

Жареный чесночный хлеб

type iGarlicBread interface {    GetPrice() float64    GetName() string}type garlicBread struct {    name  string    price float64}func (g *garlicBread) GetName() string {    return g.name}func (g *garlicBread) GetPrice() float64 {    return g.price}

И наших брендов

type pizzaHutGarlicBread struct {    garlicBread}type dominosGarlicBread struct {    garlicBread}

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

Теперь напишем сами фабрики, сначала общая

type iPizzaFactory interface {    createPizza() iPizza    createGarlicBread() iGarlicBread}

Теперь наших брендов: Жаровня-фабрика и Домино-фабрика с унифицированной функциональностью

type PizzaHutFactory struct {}func (p *PizzaHutFactory) createPizza(): iPizza {    return &pizzaHutPizza{        pizza{            name:     "pepperoni",            price:    230.3,            toppings: []string{"olives", "mozzarella", "pork"},        },    }}func (p *pizzaHutFactory) createGarlicBread() iGarlicBread {    return &pizzaHutGarlicBread{        garlicBread{            name:  "garlic bread",            price: 180.99,        },    }}
type dominosFactory struct{}func (d *dominosFactory) createPizza() iPizza {    return &dominosPizza{        pizza{            name:     "margherita",            price:    200.5,            toppings: []string{"tomatoes", "basil", "olive oil"},        },    }}func (d *dominosFactory) createGarlicBread() iGarlicBread {    return &dominosGarlicBread{        garlicBread{            name:  "cheesy bread sticks",            price: 150.00,        },    }}

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

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

Думайте о наших фабриках как о другом объекте. Основываясь на типе или пиццерии, которые мы хотим открыть (Жаровня или Домино), мы создаем нужную фабрику (просто другой объект). Чтобы автоматически получать эти "объекты", у нас будет своя фабрика.

Этот код поможем вам - Фабрика фабрик

func getPizzaFactory(chain string) (iPizzaFactory, error) {    if chain == "P" {        return &pizzaHutFactory{}, nil    }    if chain == "D" {        return &dominosFactory{}, nil    }    return nil, fmt.Errorf("Enter a valid chain type next time")}

Надеюсь, стало понятнее.

Основное, что нужно запомнить: шаблон абстрактная фабрика реализует фабрику фабрик. Внутренние фабрики используются для создания продуктов нужного вида.

Вы можете найти этот код на github

Пока

Подробнее..

DartUP 2020 архитектура Dart VM, non-nullability в действии и Flutter для бизнеса

26.11.2020 20:11:58 | Автор: admin


Уже 4 и 5 декабря пройдет DartUP конференция по Dart и Flutter на русском и английском языках. Обычно в это время мы смотрим площадку, печатаем стикеры и запасаем в офисе коробки со свежеприготовленным Dart-пивом. Но в этом году все будет по-другому. Под катом рассказываем про темы докладов, спикеров и онлайн-активности, которые нас ждут на DartUP 2020.

Программа


Слава Егоров разработчик Dart VM из Google, который уже 10 лет работает с Dart. Слава расскажет про архитектуру Dart Virtual Machine и ее эволюцию в ходе развития языка. Хардкорный доклад с огромным количеством примеров с кодом.

Michael Thomsen, Product Manager языка Dart из Google, проведет лайвкодинг-сессию на тему Dart non-nullability в действии. Недавно команда Dart выпустила null-safety один из важнейших релизов со времен второй версии. Во время своего выступления Майкл ответит на один из главных вопросов комьюнити: как переносить реальные проекты на мажорную версию.

Вместе с Filip Hracek, DevRel Flutter и Dart из Google, мы решили подготовить не обычный доклад, а веселый интерактив. Поэтому объявляем конкурс Cracking up Flutter: присылайте на wriketechclub@team.wrike.com свои Codepen с любым Flutter-приложением, которое не работает из-за ошибки в одной строчке кода, и правильный ответ. В теме письма укажите Cracking up Flutter.

Мы покажем ваши submissions Филиппу во время его выступления, и он за ограниченный промежуток времени попробует разобраться и найти место, в котором прячется ошибка. Присоединяйтесь к чату во время трансляции, чтобы помочь Филиппу в процессе! Участнику конкурса, который спрячет ошибку так виртуозно, что Филипп не сможет ее найти, мы подарим приз.




На круглом столе Flutter для бизнеса Борис Горячев (CTO в Meduza), Геннадий Евстратов (Head of iOS в Yandex.Taxi) и Александр Денисов (Co-Head of Flutter Competency в EPAM) поспорят о том, как продавать Flutter бизнесу и отвечать на три самых распространенных вопроса: А что если Google решит закрыть Flutter через год?, Где искать разработчиков? и Какие перспективы есть у Flutter?.

Kevin Segaud Dart и Flutter GDE, который уже выступал на DartUP в прошлом году. В этот раз Кевин расскажет про интересную и достаточно новую для комьюнити тему Dart FFI. Будет немного теории и много практики: Кевин в реальном времени покажет, как использовать Dart в связке с кодом C и расскажет про плюсы и минусы такого подхода.

Андрей Смирнов из Wrike знает о виджетах практически все. На прошлой конференции Андрей рассказывал про работу с графикой, а в этом году погрузится в устройство Flutter Engine, расскажет про Rendering Pipeline, Constraints и про то, как эти инструменты использовать на практике.

Кирилл Бубочкин из чешской компании Mews поделится опытом использования Flutter в продакшне: команда год назад переписала на Flutter свое большое B2B-приложение. На DartUP 2020 Кирилл расскажет про архитектурные подходы и полезные библиотеки.

Thomas Burkhart выступит с темой, которую редко удается встретить на Flutter-конференциях. Томас расскажет про RVMS практичную архитектуру для Flutter-приложений, поделится своим опытом и последними наработками.

Доклад Efthymis Sarbanis (Athens Flutter) круто зайдет в комбинации с предыдущим докладом Томаса. Efthymis Dart и Flutter GDE и организатор Flutter Greek Community. В своем докладе он расскажет про изоляцию фич во Flutter и использование принципов Domain-Driven Design и SOLID.

Взаимодействие с картами требуется во многих приложениях. Алиса Цветкова из HIQ разберет библиотеки карт для Flutter, расскажет о подводных камнях при работе с ними и о том, как построить взаимодействие вашего приложения с картами.

Леша Шаров из Wrike готовит доклад про нейронные сети на Dart. Во время выступления Леши мы поговорим про то, что из себя представляют простейшие нейронные сети и можно ли использовать Dart для их написания. А еще будет несколько работающих примеров.

Нейронные сети Леша использовал и для создания Bugz онлайн-игры, в которой команды ботов-жуков, управляемые сгенерированным генокодом, сразятся друг с другом или с жуками Wrike. Всем зарегистрировавшимся на конференцию мы пришлем правила игры и ссылку.

Любителям хардкорных тем особенно понравится доклад Михаила Зотьева из Surf про внутренности Flutter: устройство Rendering, вывод виджетов и другие аспекты фреймворка. Будет полезно как новичкам, так и тем, кто хочет лучше разобраться во внутреннем устройстве Flutter.

Александр Денисов из EPAM расскажет про Navigator 2.0, который появился во Flutter относительно недавно. Саша расскажет, зачем они затащили его в проект, с какими сложностями столкнулись в процессе и что получилось в итоге.


Владимир Иванов из EPAM расскажет про проблему pixel perfect верстки, длинный фидбек луп на дизайн и про то, как инструмент Flutter Figma Preview может помочь в этой ситуации. Павел Мартынов из QuantumArt про особенности дизайна и разработки Flutter-приложений для AR-устройств. Андрей Скалкин из Datagrok поделится опытом создания высокопроизводительного веб-приложения на Dart.

Это далеко не полный список тем, о которых мы поговорим на конференции. Больше информации про спикеров, доклады и программу (которую мы опубликуем уже совсем скоро) ищите на dartup.ru.

Нетворкинг и онлайн-активности


Участники (и мы тоже!) любят DartUP не только за актуальные и полезные доклады, но и за неформальную атмосферу и возможность пообщаться с комьюнити.



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

Все неформальные нетворкинг- и Q&A-сессии будут проходить в SpatialChat. Там спикеры и эксперты из Wrike и Surf ответят на любые вопросы участников про Dart и Flutter. Готовьте свои трудные кейсы и приходите с кодом. Ребята из Surf объявили сбор идей и болей разработчиков для Open Source. А также эксперты из команды проведут код-ревью ваших репозиториев в прямом эфире. Все подробности по этой ссылке.

А пока присоединяйтесь к Slack-каналу, в котором мы будем постить анонсы во время конференции, отвечать на вопросы и неформально общаться.

Регистрируйтесь на DartUP до 4 декабря, готовьте вопросы спикерам и код на ревью. За день до конференции мы пришлем вам на почту ссылки на трансляции и активности. До встречи в декабре!
Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru