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

Блог компании jug ru group

Интервью с Дэном Абрамовым React 17, Suspense, Redux, холивары

11.03.2021 14:15:49 | Автор: admin


Главный миф о Дэне Абрамове что он создал React. Но хотя это и не так, сейчас он имеет самое прямое отношение к фреймворку, так что поговорить с ним про React очень интересно. Обычно Дэна не увидеть на российских конференциях, но нам помог онлайн-формат, и на HolyJS его подробно расспросили Наталия Теплухина (член core team Vue.js) и Наталия Короткова (занимается веб-проектами с 2010 года).


Тут получился не поверхностный разговор о погоде, а обсуждение технической конкретики для тех, кто вдается в детали. Но под конец были и более общие вопросы: когда появится новая документация React, переписанная с нуля? Что не так с Redux? Как Дэн воспринимает холивары о фреймворках? Что он может рассказать о релокации?


Так что, если не осилите все технические детали в первой половине интервью, переходите ко второй, там попроще. Под катом и видеозапись разговора, и текстовая расшифровка.



Оглавление



О Suspense


Первый вопрос немного философский. Начну его с вводной. Чем стабильность отличается от стагнации? В 17-м апдейте React у нас не было никаких классных фич и заметных обновлений, хотя ожидались какие-то глобальные апдейты. Почему React вообще сменил версию с 16 на 17, ради чего это было?


Изначально не планировали такой 17-й версии, которая получилась. Действительно, планировали апдейты, связанные с конкурентным режимом, Suspense. Но так сложилось, что было несколько проблем, которые мы хотели пофиксить довольно давно, с 2014 или 2015 года. Они связаны с ивентами. И мы снова столкнулись с ними внутри.


Была конкретная проблема. Вы, наверное, помните редактор Atom: там с ней столкнулись до того, как редактор закончился. Они какое-то время использовали React, на нем можно было писать плагины, и столкнулись с тем, что если вложить в одно React-дерево другое они плохо работают вместе.


Если у тебя File Explorer на React 13, а Project View на 14-м, конкретная вещь, которая ломается
event.stopPropagation(). Эти реакты не знают друг о друге, не могут координировать события.


И мы столкнулись с похожей проблемой: переписывали сайт (Facebook прим. ред.) и хотели перевести некоторые части на новую версию, которая убирает совсем старые недокументированные фичи из 20142015 годов.


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


Мы решили пофиксить события, работал в основном Доминик (Dominic Gannaway прим. ред.). Большое изменение события перенеслись. Раньше они подписывались на документе, а теперь делегируются к корню дерева React. Даже если вы используете две версии React, что в целом не очень хорошо, в большом приложении есть разумный компромисс: какие-то старые трехуровневые диалоги можно держать на старой версии, а основное приложение перевести на новую. Благодаря этому изменению они могут сосуществовать, код можно переводить с одной версии на другую, и это не должно сломать приложение в целом. Может, придется немного подтянуть его.


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


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


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


Почему вы так долго пилите Suspense, что мешает его релизнуть?


Давайте я сначала расскажу, как мы в принципе работаем, а потом отвечу на вопрос.


Мы не просто команда разработки, мы команда Research & Development. В продуктовой команде бывает, что кто-то просто просит какую-то штуку, и её делают. У нас немного другой подход, потому что продукт другой.


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


Если начать думать, как встроить само понятие асинхронности в UI-программирование, то это довольно обширная область. И одна из основных идей в Suspense мы хотим утащить понятие того, что какой-то компонент не готов зарендериться, что он ждет каких-то данных или чего-то ещё, в декларативную модель, как React делает с другими вещами. Вместо того, чтобы приходилось делать что-то вручную, это становится элементом дерева.


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



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


И в целом у нас это уже хорошо работает c Relay. То есть у нас есть опенсорсная библиотека для GraphQL, которая использует Suspense. Но мы знаем, что в опенсорсе никто не использует Relay, мы хотим общее решение.


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


Мы по-прежнему развиваем эту идею. Есть конкретные вещи, которые, я надеюсь, мы сможем показать в ближайшие месяцы. Они дополняют картинку и делают её целостной. (прим. ред.: в конце декабря были представлены React Server Components)


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


О Concurrent mode


Как вы конкретно исследуете эту проблематику? Насколько я знаю, вы тестируете Concurrent mode и Suspense data fetching на Next.js-проекте. Можешь рассказать об итогах тестирования? Помогает ли это вам в развитии React?


Да, если кто-то не знает, у нас есть активная коллаборация с Next.js, это React-фреймворк. Это коллаборация между нами, Next.js и кусочком команды Google Chrome. У нас нет какого-то направления вроде помогать только Next.js, просто в разное время работаем с разными командами над разными проектами.


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


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


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


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


Во главе Concurrent mode, потому что очень сложно сделать сайт такого объёма на классическом React. Это не то же самое, что перевести существующее приложение мы и с Next.js работаем, у них есть какие-то клиенты. И через них можно попробовать перевести один проект на Next.js и посмотреть, что будет на Concurrent mode.


У них есть своя data fetching-история с getInitialProps, которая в чем-то нас вдохновила. Это один из ингредиентов, который мы получили от Relay, нашего собственного ресёрча, а теперь мы хотим посмотреть можем ли мы дополнить то, что Next.js сделал с getInitialProps, внести свои идеи туда и посмотреть более общее решение, которое имеет смысл попробовать.


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


По экспериментам насколько знаю, Concurrent mode уже пробовали в каких-то проектах. На Next.js Conf недавно был доклад от команды Chrome, в котором они показали слайд с изменениями в хорошую сторону.



Вот этот момент в докладе.


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


Будет ли Concurrent mode работать с React Native, и правда ли Facebook использует React Native, который отличается от того, что лежит в общем доступе на GitHub?


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


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


Сам билд можно сделать, если заклонить репозиторий React на GitHub. Можно сделать yarn build --type=FB и получить те же бандлы, что мы используем. Только там будут внутренние референсы.


Про конкурентный режим React Native сейчас он там не включен, и я думаю, что это один из проектов, над которым мы будем работать или помогать работать в 2021 году. Но он будет позже, чем стабильный вебовый релиз, потому что там просто больше работы.


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


Мы столкнулись с тем, что на React Native легко делать экраны приложения, но трудно создать что-то встраиваемое. Например, ленту новостей Facebook никто не будет никогда переписывать на React Native, там уже семь лет коду. И, например, хочется встроить какой-нибудь feed item на React Native, но поскольку он всегда был асинхронным, его нельзя отрендерить, измерить, чтобы он синхронно встал в ленту новостей, пока ты скроллишь.


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


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


О state managers


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


Почему анстейбл? Потому что это очень странный API, которым сложно пользоваться. Я думаю, мы это точно уберем.


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


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


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


И в таком случае я думаю, будет спорный вопрос, что нужен такой API для state management. Если проблему закрыть именно с тем, что слишком агрессивно всё ререндерится автоматической мемоизацией, то результат будет похож на тот, что у Svelte или Vue.


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


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


Почему React всегда позиционировался как библиотека для View-слоя с возможностью хранить и обрабатывать данные, но никогда не предоставлял API для интеграции с внешними источниками данных, например, state managers?


И похожий вопрос: так как GraphQL тоже технология Facebook, и, к примеру, в Redux можно увидеть, что какая-то его часть встроена в React посредством hooks, планируется ли внедрять похожую историю с GraphQL, к примеру, как Relay для стандартизации? Будут ли предусмотрены какие-то API, чтобы сделать state manager в React более стандартизированным?


У меня первая реакция на такие вопросы я не знаю, что такое state management, не понимаю, что люди имеют в виду, когда говорят об этом. Может, у меня профдеформация, потому что в React-команде мы просто думаем об этом иначе.


В React уже есть понятие State, это UI-состояние, его менеджишь тем, что просто используешь State, если нужно передать глубоко, используешь контекст и все дела.


Есть вещи, которые туда класть неудобно. В основном то, что не является UI-состоянием. Я не имею в виду бизнес-логику, это ещё одно слово, значения которого я не знаю, а я имею в виду кэши. Например, если ты фетчишь данные, у тебя есть, скажем, страница Twitter Feed, и эти твиты не State.


Понятно, что обычно мы используем State, если пользуемся чистым React, или используем Redux, кладём всё туда, но так или иначе оно в конечном итоге становится реактовым State где-то внутри или копируется туда.


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


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


Получается, что у людей возникают проблемы вроде zombie children или нестабильного батчинга, я так понимаю, в интеграции со State-менеджерами. Также неясно, как интегрироваться с конкурентным режимом. То есть для большинства state management это не совсем тривиально.


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


Первая часть была про то, что в React есть API unstable batched updates и если внутри вызова сделаешь много substates, React всё равно пройдет по дереву один раз, и не будет каких-то странных несоответствий, то, что назвали Zombie children. Это проблема, когда компонент где-то внизу и у него состояние не соответствует чему-то, что он получил по props, потому что другой компонент над ним ещё не обновился.


Эта проблема уходит, если ты делаешь батчинг. Конкурентный режим включает батчинг для всего. Unstable batched updates можно спокойно использовать в любой библиотеке, мы его используем. Это плохой пример, его не нужно было называть unstable, но сейчас уже глупо его переименовывать, потому что в конкурентном режиме это просто default.


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


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


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


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


Поэтому одно из них если ты выносишь именно UI State, не кэш, из React в какой-то store, то у нас будет несколько стадий, как с этим интегрироваться. Первый вопрос мутабельный ли store или нет, потому что если иммутабельный, то всё прекрасно интегрируется.


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


Они необязательно будут работать с мутабельными сторами. Но у нас есть план добавить другой API если всё равно нужна мутация UI-стора вне React вместо того, чтобы просто использовать React state, мы добавим другое API, которое позволит это делать, если ты реализуешь несколько дополнительных хуков в своей библиотеке.


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


О нормализации данных на клиенте


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


Интересный вопрос, мне кажется, в нём зарыт какой-то более глубокий вопрос. Какая цель нормализации или денормализации? Их несколько.


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


Но нормализованный кэш как в Apollo или Relay, c GraphQL, если его вручную не менеджерить, это ещё как-то терпимо. Но это накладывает ограничения на структуру данных, и вносит сложные вопросы в духе как работает инвалидация вещей, связанных в этом кэше?, сколько они живут?.


В Redux issue tracker меня года четыре назад спросили: а как часто вещи будут пропадать, если я их держу в Redux. Ну, как перезагрузишь вкладку. Тогда как-то более наивно про всё думал.


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


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


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


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


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


О Redux


В твоих постах периодически видно пассивно-агрессивное отношение к Redux. Почему он тебе так не нравится, учитывая, насколько удобные обёртки написали мейнтейнеры, Redux toolkit и насколько улучшилась документация?


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


Он как тест Роршаха, каждый видит там то, что видит.



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


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


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


Люди используют его, чтобы реализовать кэш, потому что в React нет first class-концепции кэша, но каждый реализует это по-своему. Решать проблемы сложно, поэтому они обычно нерешенные. Получается мешанина из boilerplate-кода.


Я не против boilerplate-кода, который имеет какой-то смысл количество строк меня не пугает. Но это концептуальный boilerplate, когда разведены вещи, которые на самом деле тесно связаны. Короче, не нравится мне это всё!


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


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


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


Redux это как место, куда ты кладёшь то, что ты не знаешь, куда положить. Хочется, чтобы для всего нашлось место, и тогда туда будет нечего класть.


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


В Facebook разрабатывается state manager Recoil. Это просто ещё один независимый проект внутри компании или решение, на котором вы тестируете и которое когда-нибудь хотите внедрить взамен Redux?


Нет, это совершенно независимый проект. Он разрабатывался для конкретного продукта, потом некоторые другие продукты начали его использовать. Мы общаемся с автором, и есть возможность, что какие-то идеи из Recoil найдут место в React. Не то, что Recoil станет официальным Опять-таки, я не знаю, что такое state management.


Есть технические вопросы, которые Recoil решает лучше, чем то, что у нас есть. Вопрос в том, какая часть фундаментальная, что должно быть в самом React. Мы про это общаемся с автором. Возможно, он поработает над каким-то proof of concept, чтобы посмотреть, на что это похоже, если бы это было в React. Но никаких гарантий. Мы активно следим за всеми state managers, что появлялись за последние пять лет.


О прошлом React


У нас было про перспективу, про state managers, давай в ретроспективе. Была ли какая-то работа над React, куда компания вкладывала кучу сил, над чем-то работала, но оно не взлетело? Какая-то тупиковая ветка?


Мне трудно оценить количественно, но тоже вопрос, что такое тупик. Тупиков было довольно много, и они информируют Приведу дурацкую аналогию: это как Бильбо не прибил Голлума, а потом тот сожрал кольцо, и кольцо в итоге всё же упало, и он свою роль выполнил.


У нас обычно выполняют такую роль проекты, которые не срослись. Например, был прототип React, который работает как в Worker, на другом потоке, и в 2014 году все думали про многопоточность. По нашему опыту, это не особо интересно.


Потому что UI должен отвечать сразу, для многих событий должен быть preventDefault(), который должен быть синхронно. Похожая проблема в React Native: он всегда асинхронный, многие вещи трудно сделать.


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


Это была тупиковая ветка, но она отчасти информировала редизайн React Native. И некоторые вещи, связанные с дата-фетчингом, которые делаем сейчас, отчасти истоки берут из некоторых идей того прототипа.


Ещё пара людей из React-команды существенное время вложили в Prepack экспериментальный компилятор JavaScript в JavaScript, который исполняет JavaScript во время билда. Что достаточно странная идея (как и многие идеи Себастиана). Проект не выгорел слишком сложно, как вскипятить океан.


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


Отчасти это было одним из векторов вдохновления для хуков: если делать какой-то очень умный компилятор, это гораздо более разумная для него платформа. И с тем же Suspense, что мы сейчас делаем это тоже отчасти вдохновлено тем, что мы пытались делать с Prepack, но тогда поняли, что у него плохие характеристики и трейд-офф неправильный.


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


О хуках


Как раз-таки ты упомянул про хуки. Я встречала множество статей и обсуждений, что людям не нравятся хуки, что люди фрустрированы, что у них love-hate relationship. Понятно, что всегда найдутся недовольные какими-то решениями, но все же спустя время довольны ли вы своим выбором, было ли это удачное решение?


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


Нам тоже было интересно, что люди думают и как у них ощущения. Проводили опрос, собрали где-то 15 тысяч ответов. Там было два вопроса. Один вопрос предпочитаете ли вы хуки, классы, когда как или не знаю, и 70% выбрали, что предпочитают хуки, 10% когда как, 7% классы и остальные не определились.


Другой вопрос был нужно ли использовать в продакшене хуки или классы, и у хуков около 70% было, у классов 50%. И это всего спустя два года после выхода. Уже другая парадигма


Понятно, что когда мы выпускали хуки, говорили, что всё нормально, ничего не меняется. Но по сути это уже другой API, который более правдиво выражает, что такое React, и у него есть лёгкие стороны, есть сложные. Это факт, мы не пытаемся это скрыть. И сейчас переписываем документацию, думаю, будет очень прямолинейны.


Всякие вещи типа useEffect это hard mode у React. И у этого есть причина не потому что мы не можем придумать лучший API или из-за идеологии, а потому что сделать хороший мостик между декларативным и императивным миром сложно.


Приходится думать про комбинации вещей, которые могут произойти, но сходу не придут в голову о race condition, о том, что какие-то вещи могут открыться или закрыться, данные могут поменяться на середине того, как что-то делаешь. И с классами было проще писать код, который как бы работает, но ломается в edge case.


А хуки заставляют тебя обрабатывать такие случаи. И там парадигма, что мы сразу ломаем код. Это бесит, особенно если используешь низкоуровневое типа useEffect ощущение, словно решаешь уравнение. Нужно понять, как так скомпоновать, чтобы всё работало. Но когда получается, понимаешь, что реально работает и для редких случаев, и для типичных.


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


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


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


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


О холиварах


Уведу от хуков немного в холивар, потому что то, что ты говоришь о хуках и сложном варианте React очень близко и Vue.js тоже, потому что у Vue есть новый composition API hard mode Vue. Его очень часто сравнивают с хуками. Даже сейчас в чатике выступления вижу зрительский вопрос о том, что есть вещи, которые Vue утаскивает у React, есть то, что React утаскивает у Vue. Есть куча вопросов, какой фреймворк лучше использовать, кто у кого что утащил, чего нет, а что у кого есть. Тебя не утомила тема о войнах фреймворков и конкуренции?


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


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


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


Это разные трейд-оффы.


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


Это в целом. Всегда есть парадигма у каждого у Svelte, у Vue, они на самом деле похожи, у Angular и React какая-то парадигма. И всегда будет один фреймворк каждой парадигмы, потому что у каждой парадигмы есть свои сильные и слабые стороны, на которых можно что-то интересное строить.


Поэтому конкуренция будет, есть разные идеи, и у каждой есть свои сильные стороны. Холиварные статьи меня никак не колышут, мне всё равно если можем сделать React лучше, меня это устраивает.


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


Насколько я знаю, Vue уже переписали на TypeScript. Когда, может быть, React перепишут с Flow на TypeScript?


Зачем? Я не то, чтобы какой-то адский фанат Flow, мне и то и другое по большей части по барабану, но это звучит как проект, в котором я не вижу смысла. На definitions, которые публичные, это никак не влияет. Наши внутренние типы всё равно будут другие, потому что то, как ты используешь React, отличается от того, как он реализован. Поэтому это звучит как большой проект, который не даёт для нас никакого выхлопа, и главное для наших юзеров.


Для нас это может дать лучшую IDE-интеграцию, но я не пользуюсь IDE, так что мне всё равно. Контрибьюторов у нас не так много, контрибьютить в core довольно сложно, потому что нужно много контекста для понимания, почему определенные вещи сделаны таким путём. Поэтому смысла, наверное, нет. Если и будем переписывать, то как-то более фундаментально, на тот же Rust или что-то такое. Конкретных планов пока нет.


О личном


Может быть, поговорим о чем-то более личном? Я знаю, что Наташа недавно переехала в Амстердам, я недавно уехала из Лондона. Хотелось бы спросить у Дэна может, ты что-то посоветуешь людям, которые хотят переехать и работать за границей? Может, есть какие-то моменты, на которые, как ты считаешь, нужно обратить внимание?


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


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


А по коммуникации с командой, с людьми другой культуры возникали сложности?


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


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



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


Отчасти да, есть какие-то вещи, над которыми мы работаем. Они набирают обороты, мне интересно за ними следить и немного тоже помогать. Плюс мы с Рейчел (Rachel Nabors прим. ред.) переписываем с нуля документацию React.


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


А в Лондоне сейчас локдаун продолжается?


Да, сейчас собираются снимать, но вместо него вводят другой, с какими-то уровнями. Как-то всё ужасно некомпетентно, мне кажется.


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


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


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


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


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


Это очень похоже на то, как мы писали доку Vue 3. Мы тоже держали её в закрытом репозитории, так что я полностью разделяю эту идею. Хотя в какой-то момент люди начали комплейнить, но это была новая версия. А тут версия та же.


Да, только когда мы её закончим, там уже может быть новая версия (смеётся).


Это долгосрочный проект, но есть ли очень приблизительная оценка, когда планируете закончить?


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


Этот разговор состоялся на ноябрьской HolyJS а мы сейчас вовсю готовим следующую HolyJS, которая пройдёт 20-23 апреля. Как в случае с этим интервью некоторые вопросы были от зрителей, так и на новой конференции важной частью будут чаты и видеосессии, где зрители могут пообщаться со спикерами и друг с другом. Часть программы уже известна увидеть описания докладов и узнать всё остальное можно на сайте.
Подробнее..

Большой разговор с новым Kotlin Project Lead Романом Елизаровым

16.03.2021 12:09:05 | Автор: admin

В ноябре стало известно, что работу над Kotlin возглавит Роман Елизаров (elizarov). Поскольку теперь за дизайн языка отвечает он, интересно лучше понять его видение. Поэтому на онлайн-конференции Joker мы задали Роману много вопросов. А теперь для Хабра сделали текстовую версию (видеозапись тоже прикрепляем).



Для начала поговорили о его новой роли и команде Kotlin, а затем перешли к дизайну языка. Из текста можно узнать, например, такие вещи:


  • Как принимаются решения, ломающие обратную совместимость?
  • Чем философия Kotlin отличается от C# и почему?
  • Насколько приоритетна производительность?
  • Чем корутины в Kotlin отличаются от Project Loom в Java?
  • Какой тренд влияет на языки прямо сейчас?

Расспрашивали Антон Архипов (Developer Advocate в JetBrains) и Алексей Стукалов (руководитель направления DevRel в компании Haulmont, создавшей CUBA Platform). Но не все вопросы они придумали сами: поскольку дело происходило на конференции, часть была написана зрителями.



Взаимодействие Project Lead, команды Kotlin и партнеров


В чем заключается позиция Project Lead? Это бог и папа всего проекта или какая-то номинальная должность?


Номинальная. Я точка входа для JetBrains в Kotlin: разруливаю, куда, что и кому отправить, помогаю всем находить какие-то варианты, представляю Kotlin в каких-то организациях (и в случае с партнёрами, и внутри JetBrains). И название Project Lead неслучайно: это не Project Manager, мне не надо управлять кем-то.


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


И Kotlin очень открытый к сообществу проект. Мы не Swift, где Apple может в закрытую что-то пилить, а потом бац выкатить и поставить комьюнити перед фактом. У нас секретов нет, все эксперименты видны на GitHub.


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


Kotlin очень большой, у нас больше 100 человек, и они сформированы в небольшие команды, каждая занимается своим кусочком. Этот кусочек может быть технической частью (например, JVM-бэкенд), а может продуктовым направлением (например, Kotlin Multiplatform Mobile).


Есть целых несколько команд компиляторов, потому что у нас есть и Kotlin/JVM, и Kotlin/JS, и Kotlin/Native разные бэкенды. Есть общий фронтенд, которым занимается другая команда. Есть какие-то инфраструктурные проекты, IDE, библиотеки, поддержка, тестировщики, маркетинг, техписатели. Небольшая команда технических писателей обслуживает весь проект она отдельная, потому что нужны люди определенной квалификации, которым проще быть в одной команде, и они делают всё для всех.


Все эти команды независимы друг от друга, у них свой релиз-цикл и своё понимание дальнейшего развития?


В целом, конечно, нет. Основное событие Kotlin релиз компилятора, который мы сейчас делаем 2 раза в год, плюс IDE к нему. Это большие релизы, ещё есть патчи и другое.


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


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


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


Синхронизация такого количества команд и людей это отдельная задача. Это зона ответственности Project Lead, или оно само собой работает?


Само собой, конечно, ничего не работает. У нас есть несколько человек, которые занимаются этим. Есть Стас [Ерохин], который занимается всей разработкой, есть Егор [Толстой], который координирует все технические команды. Есть продакт-менеджеры, которые общаются с командами по своим продуктовым направлениям.


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


Было упомянуто взаимодействие с компаниями-партнёрами. Google большой участник экосистемы Kotlin. Как вы с ними взаимодействуете?


Роман: Google действительно крупнейший партнер, и неспроста. Они не просто поддерживают Kotlin так, как это делают другие компании. Есть анекдот про свинью и курицу, где курица говорит: Давай мы откроем ресторан под названием Яйца и Бекон. Свинья подумала: Не, так не пойдет, I'll be commited, but you'll just be participating.


Курица и свинья идут по дороге.
Слушай, Свин, я тут подумала, не открыть ли нам с тобой ресторан? говорит курица.
А какое название дадим? спрашивает свинья.
Как тебе Яичница с беконом?
Не пойдет мне тогда придется посвятить себя проекту полностью, а ты будешь задействована лишь частично!
Scrum. Революционный метод управления проектами, Джефф Сазерленд

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


Но влияние Google всё же ограничено влиянием. Понятно, что их проблемы получают приоритет, но, например, весь дизайн языка и общее направление в наших руках. Это мы разрабатываем Kotlin, а они один из контрибьюторов. У них есть инженеры, которые фиксят компилятор и присылают пулл-реквесты с какими-то конкретными багфиксами, которые специфичны для Android. Но это один из контрибьюторов, пусть и самый крупный. Kotlin в любом случае продукт JetBrains.


В каком-то плане они от вас зависят, потому что они должны быть встроены в ваш релиз-цикл. У них есть Early Access?


Мы координируемся с крупными партнерами так же, как и внутри между собой, когда все команды понимают, какие у нас общие планы и цели. Это не только Google, но и Gradle, Spring.


Образование и спортивное программирование


Бытует мнение, что котлинист это следующий шаг джависта. У новичков возникает вопрос: тогда стоит начинать с Java или можно сразу постигать Kotlin? Как вообще строится Kotlin для новичков, как вы завоевываете людей с чистого листа?


Роман: Ответ очень простой, у меня на эту тему есть блог Intentional qualities в Medium.


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


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


Как новичку объяснить, что такое public static void main? Ответ никак.


Я скажу просто не трогай, так должно быть.


Да! А Kotlin изначально задумывался как понятный и простой язык, который можно понять и которому легко научить. Поэтому учить его с нуля гораздо проще, чем Java. И мы активно работаем с вузами, у нас есть специальный человек, который только и занимается Kotlin в образовании. Очень рекомендую посмотреть ролик, который выпустили к нашему онлайн-ивенту. Там преподаватели, уже использующие Kotlin в образовании, рассказали, почему выбрали его.


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


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


К движению олимпиадного программирования, ICPC, относятся по-разному: одни считают такое пустой тратой времени, другие бесценным опытом. Два вопроса: когда Kotlin войдёт в число языков, на которых там можно писать, и как ты сам относишься к этому времяпрепровождению?


Я вижу, что ты явно отстал от спортивного программирования, потому что там давно можно писать на Kotlin. Более того, JetBrains один из спонсоров ICPC, потому что занимается инструментами для программистов и предоставляет инструменты для всех участников соревнования.


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


И благодаря ICPC появились многие IT-специалисты, которые вместо того, чтобы гулять и прожигать жизнь, день и ночь изучали алгоритмы, тренировались программировать и получались такой силы программисты, которых нигде не сыщешь.


У нас в команде много ICPC-шников, точное число не назову, но лично знаю четырех, все очень сильные инженеры. Я знаю, что если человек с ICPC это инженер, который может разобраться в проблеме и которому можно поручить любую неподъемную задачу. Потому что ICPC мотивирует людей прокачивать мозг. Даже если им конкретно эти олимпиадные алгоритмы потом не пригодятся, мозг всё равно прокачался.


Дизайн языка


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


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


Но мы это мы. Хоть нас и 100 человек, но мы пишем только библиотеки, компилятор, тулинг, IDE. Это маленькая доля того, что на Kotlin пишет наше огромное сообщество. Они пишут вагоны разных приложений, других библиотек, работают в сложных доменах, которые нам и не снились. И они нам приносят проблемы, каждый видит их в своём домене. Например, тут на бэкенде не очень удобно, приходится писать boilerplate-код, хотелось бы упростить.


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


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


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


Посматривает ли команда Kotlin в другие новые языки есть ли там что-то полезное, что стоит внести в код?


Действительно, Kotlin изначально родился как синергия трендов и тенденций современных языков программирования. У Андрея есть замечательный доклад про то, какие языки повлияли на Kotlin, там он подробно идет по фичам и рассказывает, что для них послужило вдохновением. В докладе видно, что есть и C#, и Groovy, и Java, естественно и это влияние никуда не делось.


Очень многие говорят: Kotlin очень похож на Swift, кто на кого повлиял? Интересно, что никто ни на кого. Apple разрабатывал Swift закрыто. Kotlin разрабатывался совершенно независимо, JetBrains работала над ним закрыто меньше года, а затем проект был анонсирован и работа пошла открыто. Как мы теперь знаем, Swift тогда был на финальных стадиях дизайна. То есть Kotlin не мог повлиять на Swift, потому что там уже долго работали внутри и почти финализировали дизайн.


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


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


То есть протекание фич из языка в язык это натуральная вещь, она происходит просто потому, что кому-то что-то нравится и надо реализовать в другом что-то похожее?


Даже не потому, что нравится, а потому что современное прикладное программирование, например, мы пишем бизнес-софт и решаем бизнес-задачи очень похоже. Не важно, на каком языке ты пишешь: на C#, Java или Kotlin у тебя те же самые проблемы, ты используешь похожие шаблоны проектирования, ты сталкиваешься с похожими проблемами, которые надо выразить в коде.


Ты не хочешь вагоны boilerplate, чтобы создать бизнес-решение. Ты хочешь, чтобы оно было компактное, чтобы код отражал твою бизнес-мысль. И многие языки (не только Kotlin) стремятся снизить этот барьер, нащупать какие-то решения, которые позволяют снижать барьер между мыслью и кодом.


И если это решение было хорошо нащупано, то его быстро одобрят. Например, C# придумал async/await и запустил его в массы в 2010 году. Но проблема была у всех! Увидели крутое решение раз, и оно всюду, потому что проблема всеобщая, и её нужно решить. И это было огромным вдохновением также и для Kotlin, чтобы сделать корутины. Любой современный программист сталкивается с асинхронщиной, без неё никак в современном мире.


Это может означать, что существует какой-то асимптотически самый крутой язык, к которому все стремятся, и лет через 20 все языки сольются в него?


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


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


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


Попробуем понять философию Kotlin на примере. В C# есть ключевое слово event. Оно не несёт ничего сверхъестественного, всё достаточно легко реализуется в функциональном стиле. Для шарпистов оно удобное и они спрашивают, как мы без ивентов вообще живём.


Вопрос: появятся ли в Kotlin ивенты? И какая часть философии запрещает или заставляет завести нечто подобное?


Этот пример как раз хорошо иллюстрирует отличия философии дизайна C# и Kotlin. Обычный процесс создания фич в C# такой: есть какая-то проблема, и мы её решаем. Затаскиваем какую-то новую фичу, специально делаем ключевые слова. Были у людей проблема с генераторами сделали генераторы с yield и так далее. Есть проблема с асинхронностью затащили ещё два ключевых слова async/await. Надо добавить ивенты сделали специальную фичу event. Надо конструировать объекты, пересоздавать и подменять в них какие-то поля сделали специальную конструкцию with. И это очень шарповый подход: создавать уникальную синтаксическую конструкцию для решения конкретной проблемы.


И в целом у C# тенденция целенаправленно прибивать фичи гвоздями. Ну, иногда бывает процесс обобщения, как это было с async/await, когда сначала прибили всё к конкретному типу task, а затем обобщили.


И это хорошо для Microsoft, которая строит свой мир по принципу я всё контролирую сверху вниз. Я бог, и я знаю, что вы будете делать. Я делаю не просто язык, а язык, с помощью которого вы будете писать на наших библиотеках, под наши application-сервера, деплоить в наше облако и редактировать в наших IDE.



Для Microsoft естественна такая философия дизайна. Когда они делали async/await, они специально делали его под конкретную библиотеку с конкретными задачами.


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


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


Поэтому у нас нет ивентов, но есть перегрузка оператора +=, есть лямбды. Ты можешь легко и непринужденно набросать в 10 строк DSL, который для конечного программиста будет выглядеть ровно как ивенты в C#.


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


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


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


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


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


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


Естественно, здесь компромисс, в котором нет правильного ответа. Разные языки находят себя в разных точках. Есть сознательно очень простые языки, такие как Go, которые выбрали подход мы будем максимально простыми.


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


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


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


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


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


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


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


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


Если в Web Archive поискать старые документы про Python, найдётся вступление, что Python подходит не только для создания маленьких программ, но и для больших: больше 10 тысяч строк кода. Тогда для Python программа больше 10 тысяч строк казалась дико большой.


Когда создавался Kotlin, изначально была задача, что мы на нем будет писать программы от 1 миллиона строк кода. А из этого автоматом следует тулинг: как поддерживать программу в миллион строк без него? Из этого следствие язык должен быть изначально продуман так, чтобы он был понятен инструментам.


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


Чем Kotlin принципиально отличается от Java? Или Kotlin красивая упаковка для Java, как Swift для Objective-C?


Swift и Objective-C совершенно разные языки. Хотя они интероперируемы между собой, и дизайн Swift заточен на то, чтобы хорошо оперировать с Objective-C.


То же и здесь. Kotlin это новый язык, который изначально был задуман, чтобы легко дружить с Java. Но это совсем другой язык, у него другая идеология. Он создавался для большого промышленного программирования, которым приятно пользоваться и работать в масштабе.


Изначально стояла задача решить боли Java. Огромное влияние на дизайн Kotlin возымела книга Effective Java. Там советы что в Java надо делать, а что не надо. И если вы её прочитаете, обнаружите, что половина этих вещей уже учтена в дизайне Kotlin. Если там написано не делайте этого, то в Kotlin этого просто нельзя сделать. Если там написано всегда делайте вот так, то в Kotlin просто сделана такая фича из коробки.


И Kotlin изначально задумывался как язык, учитывающий недостатки Java: он исправляет её проблемы с системой типов, добавляет null safety (потому что известная большая проблема у любого Java-проекта это NullPointerException), решает проблемы с многословностью (потому что тебе постоянно нужно делать codegen или писать геттеры и сеттеры), решает проблемы с абстракциями (потому что в Java по своему опыту сталкивался с паттерном, что каждый вызов нужно заворачивать в try из пяти строчек кода, определенным способом написанный копипастой).


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


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


Будем честны с собой. Да, сейчас для Kotlin JVM это основной рынок, и JVM самая большая платформа в мире для разработки на бэкенде. Но если посмотреть на графики, то эта платформа в целом не растёт. (Хотя и не умирает крики, что завтра Java умрёт, преувеличены.)


Новая интересная движуха происходит за пределами JVM-платформы. Появляются новые языки и технологии, фронт, cloud. То есть рост IT-рынка происходит за пределами JVM. А Kotlin задумывался не как сиюминутный язык, чтобы поиграться и бросить это игра вдолгую. Мы планируем работать над Kotlin многие десятки лет и оставаться современным языком.


Это и драйвит развиваться на другие платформы, в растущие направления. Растёт облако идём в облако, интересен фронт идём во фронт. Мы развиваем те точки роста, где в будущем будут программисты.


Когда мы начинали Kotlin, приложения в основном писались на Java, было логично таргетить JVM. Время меняется, сейчас многие приложения пишутся далеко не на JVM, поэтому Kotlin надо туда.


Джависты привыкли, что программы, написанные на Java 2, должны работать на Java 15. А в JS-мире готовы переписывать, там меньше приверженцев жёсткой обратной совместимости. Какая философия у Kotlin?


Философия Kotlin прагматичная. Понятно, что более стабилен менее стабилен это такой континуум, и разные языки по-разному себя ведут. Даже Java, которую часто приводят как образец стабильности, конечно, совсем не образец и не стесняется в каком-нибудь релизе завести ключевое слово и поломать кучу старого кода или завести новые фичи JVM.


До сих пор меньшая часть enterprise-проектов перелетела через Java 9. Многие застыли на Java 8 не от хорошей жизни, а потому что уже столько всего поломали, что хрен ты перепрыгнешь через девятку. В этом смысле там много примеров, как не надо делать.


Тема жизни после Java 8 такая болезненная, что на JPoint об этом был отдельный доклад Триши Джи.

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


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


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


Любой слом кода это недовольные люди, да?


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



Релевантный выпуск xkcd


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


В Kotlin есть хороший защитный механизм: благодаря Google есть структура Kotlin Foundation. Она во многом выполняет формальную роль, защищает торговую марку Kotlin от притязаний, но ещё выполняет важную вещь в рамках неё работает language committee.


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


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


Есть поверье, что именно комитетное управление в своё время сильно затормозило развитие Java. Как ты на это смотришь?


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


В Java и JVM появляется много новых фич, влияет ли это как-то на эволюцию Kotlin или его планы?


Безусловно влияет. Я уже сказал, что для нас JVM важная платформа, и Kotlin называется так, потому что K следующая буква после J. Он и задумывался как Java 2.0. И никуда не делась цель дать людям, пишущим на Java, возможность лёгкой миграции на более современный язык.


Что означает лёгкая миграция? Наши пользователи те, у кого миллионы строчек кода. Они не могут переписать свой код на Kotlin за один день. Поэтому есть явная цель: чтобы можно было, взяв проект на Java, адаптировать код постепенно. В большом проекте по-другому не получится.


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


В Kotlin уделяют много внимания продуктивности разработчика (чтобы он писал код быстрее), а насколько высок приоритет производительности (чтобы этот код исполнялся быстрее)?


Конечно, мы не C++, у нас нет цели zero cost abstractions. И нет цели делать системный язык для программирования низкого уровня, когда ты контролируешь каждый бит. Но перформанс нам не чужд. В любой современной разработке приложения тебе приходится работать с большими данными, списками. И важен, скорее, асимптотический перформанс.


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


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


Но в Kotlin есть задача, чтобы если потом в результате профилирования выяснилось, что в этом месте что-то подтормаживает, то были инструменты это исправить. А не как в Python, где если ты уткнулся в перформанс, то идёшь переписывать на C, другого варианта нет.


В Java давно обещают завезти Project Loom насколько это событие для Kotlin, где есть корутины? Возможно ли реализовать корутины на Loom?


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


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


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


Естественно, в основном это код, который делает I/O, поэтому он редко засыпает. Чаще всего работает без остановки, выполняет какую-то логику и иногда натыкается на то, что надо подождать I/O.


У Kotlin стояла другая задача. Мы видим, что в экосистеме Java растёт огромное число асинхронного программирования. В это вкладываются Spring, Vert.x, куча людей. Это код, который постоянно засыпает, просыпается, делает микрозадачи. Он выполняет много маленьких кусочков задачи, переключаясь между исполнением разных вещей.


И всё хорошо, но кода получается многовато, он неудобный, простому программисту сложно разобраться. Все говорят Мне ваш Rx или ещё что-то нравятся, но в этом очень сложно разобраться, слишком много всего. И понятно, почему языковой поддержки нет, получается такая callback-лапша, даже если колбэки запрятаны.


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


То есть другая цель. И другое применение. Когда начинаешь смотреть в UI, что там делает активный и асинхронный код? Он асинхронный не потому, что постоянно делает I/O, а потому что летит куча ивентов, и куча маленьких кусков работы между переключениями. Чуть-чуть поработали и уснули, чаще всего мало работы и много переключений.


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


Project Loom особо не нужен новый синтаксис, это библиотечная штука. Ты стартуешь виртуальный поток, I/O-операция в нём стоит тебе дёшево, потому что она заблокирует виртуальный поток. Мы сделаем библиотечную поддержку этого.


Сейчас в корутинах есть такое неудобство, что если пишешь асинхронный код с корутиной и хочешь вызвать блокирующую функцию (какой-нибудь парсер, который будет что-нибудь подсчитывать с сокета), неудобно, что ты должен это написать withContext(Dispatchers.IO). Чтобы выделить для этого специальный пул, который не страшно заблокировать.


То есть при работе с корутинами, если операция JVM-блокирующая (какая-нибудь сокетная), ты либо вынужден искать асинхронную альтернативу, взять Java-библиотеку, либо завернуть блокирующий вызов в специальный wrapper, обозначить, что здесь будешь блокироваться.


И Project Loom позволит не требовать такой заворот, мы сможем запустить корутину в виртуальных средах, и так как они виртуальные, их можно будет блокировать безопасно. Потому что ты блокируешь виртуальный поток.


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


На Joker 2020 о Project Loom подробно рассказал Алан Бейтман (Oracle), для Хабра был сделан текстовый перевод. А о корутинах ещё в 2018-м у нас рассказывал сам Роман, доступна видеозапись.

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


Понятия не имею, я же не футурист, и хрустального шара у меня нет. Давайте посмотрим, что происходит сейчас. Тут надо понимать, что тренд реактивщины и функциональщины пока только в начале. Например, только недавно стали популярны реактивные UI-фреймворки. React, SwiftUI, Jetpack Compose и так далее.


Они стали популярны, но некоторые ещё только в разработке Jetpack Compose ещё не дошел до релиза. Мы видим, что реактивные UI-фреймворки предъявляют к языкам новые требования, которых раньше не было.


И, например, специально для поддержки React в JavaScript чуваки в Facebook совершенно сбоку запилили JSX целое расширение языка. В SwiftUI зарелизили жирный апдейт, они затащили в язык целый паровоз фич только для того, чтобы язык смог поддерживать этот реактивный UI.


Мы в Kotlin не так делаем, но ребята из Google, которые работают над Jetpack Compose, написали плагин неслабого масштаба к компилятору. То есть фактически тоже фича в языке, но просто она прилетает плагином. Это уже не просто библиотечная функция, это целый плагин к компилятору, который всё это умеет делать. Мы видим, что это новый челлендж для языков. Не существовало языков, которые это нативно поддерживают.


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


Тем не менее, это реальность, как подростковый секс: все про него говорят, но никто не пишет.


Мы с этой реальностью в самом начале пути, со всеми этими проблемами ещё даже не стали думать: этот реактивный UI хорошо, а как нам сделать теперь реактивный бэкенд? Как это должно выглядеть?


А надо ли это? Идти же надо от запроса.


Конечно, надо. Мне очень нравится цитата Форда: Если бы я, когда делал свою Ford Model T, слушал бы людей и спросил бы у них, что вы хотите, они бы сказали мы хотим более быстрых лошадей.


Здесь такая же проблема. Ты говоришь запрос, здесь нет никакого запроса. Люди писали говнокод, 10 страниц кода UI, и ни у кого не возникало мысли, что можно написать 10 строчек, а всё остальное за тебя сделает реактивный фреймворк.


Кто-то по этому скучает.


Это пожалуйста, это далёкие от меня проблемы, мы про продуктивность. Если можно вместо 10 страниц кода написать 10 строчек мы за это. То же самое с асинхронщиной. Знаешь, какой для нас был прорыв в асинхронщине, когда мы поняли, что правильно идём с корутинами? Когда у нас был перед глазами проект Netty, где парсер HTTP-запроса это, не шучу, страниц 20 текста. Просто машина, которая парсит HTTP-запрос 20 страниц, в которых, кроме суперумного Нормана, вообще никто не разберётся. Надо быть эпическим гением вроде Нормана, чтобы написать и отладить этот код.


А у нас в Ktor страничка кода то же самое делает. Всего одна! Её может написать любой среднестатистический программист, который прочитал спеку, что такое HTTP-заголовок. Вот куда мы стремимся.


Как сказал Роман, сейчас JVM основной рынок для Kotlin. Значит, если вы прочитали этот текст, то с высокой вероятностью связаны с JVM-миром. Тогда вам будет интересно на нашей Java-конференции JPoint (13-17 апреля, онлайн).

А если вы Android-разработчик, то вам подходит конференция о мобильной разработке Mobius (13-16 апреля).

И даже если вы дотнетчик, JS-разработчик или тестировщик, для вас в апреле мы тоже проведём конференции.
Подробнее..

Отдых это непросто Андрей Бреслав о жизни после Kotlin

23.03.2021 12:19:49 | Автор: admin

Для многих в IT-сообществе имя Андрея Бреслава было почти синонимичным с названием языка программирования: мы говорим Бреслав, подразумеваем Kotlin. Неудивительно, поскольку Андрей возглавлял этот проект десять лет подряд, начиная с самого его создания. Но теперь ситуация изменилась, потому что в ноябре он объявил об уходе из проекта.

Беспокоиться за будущее языка не приходится: там всё в надёжных руках Романа Елизарова, и недавно мы опубликовали интервью с ним. Но захотелось расспросить и Андрея. Мы уже обсуждали с ним положение дел в 2017-м, 2018-м и 2019-м, а теперь решили подвести черту под этой серией ещё одним интервью:

  • Какими Андрей видит итоги своей работы над Kotlin?

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

  • Что он сейчас делает в стартапе Alter? Каково после громадного проекта заниматься менее масштабным? И почему Alter написан не на Kotlin?

Оглавление

Kotlin: итоги

В сообщении об уходе говорилось, что будет переходный период. Сейчас ты уже передал дела?

Уже передал, да. Всё, я уже не работаю в JetBrains. Там осталась отличная команда, которая замечательно справляется.

А сохранится ли у тебя какая-то вовлечённость в жизнь Kotlin-сообщества? Например, будешь ли появляться на конференции KotlinConf?

Ну, я не собираюсь навсегда закрыть дверь и никогда в жизни не посмотреть в эту сторону. Будет KotlinConf с удовольствием приеду, если ребята меня позовут. И мы договорились, что если при обсуждении стратегии нужно будет моё мнение я с удовольствием приду, обсудим.

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

Что касается места, наверное, стоит смотреть на индексы PYPL и RedMonk. Я сейчас открыл в PYPL на 11-м месте, а в RedMonk на 19-м. Получается, в двадцатку попали в обоих.

Но на самом деле, конечно, не в индексах счастье. Индексы просто способ как-то ухватить в цифрах существующую реальность. Что мы видим? У Kotlin есть куча пользователей. Среди них особенно много Android-пользователей, но и за пределами Android людей тоже много. Конечно, это успех. Мы сделали что-то, что нужно многим людям, и это классно.

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

А есть ли важная для тебя цель, которая под твоим руководством не оказалась достигнута (но, возможно, ещё будет достигнута без тебя)?

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

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

Поэтому мне сложно сказать, что была конкретная цель, к которой я стремился, и мы ещё не достигли. Целей много.

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

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

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

Крупный язык (JavaScript, Java или Python) это язык, который не перестанет быть популярным завтра просто никакими силами. Kotlin тоже не перестанет, но потому что он много кому нужен. А крупный не перестанет просто из-за того, что он крупный. И на этом уровне совершенно другая игра. Там и другая ответственность, и другие законы развития. Можно чуть меньше нервничать про конъюнктуру и чуть больше думать про долгосрочную стратегию.

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

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

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

Не то что бы на команду никогда не влиял никто, кроме меня конечно, влияли. Но я постарался, чтобы возник некий штаб из таких людей. И в нём не только Рома [Елизаров]: он руководитель проекта, но проект очень коллегиальный.

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

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

Раз перемены неизбежны, то неизбежно будут и принимать не такие решения, какие принял бы ты. Нет ли ощущения блин, я бы сделал иначе? Насколько ты по-прежнему ощущаешь Kotlin своим детищем?

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

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

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

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

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

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

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

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

Сейчас UI-фреймворк Jetpack Compose появляется не только для Android, мы экспериментируем с десктопом, есть какие-то люди в сообществе, которые экспериментируют с JS это возможность сделать единый UI-фреймворк для разных платформ. Это не обязательно значит, что сам UI должен быть полностью одинаковым на разных платформах, но единый фреймворк это большое дело.

Эта история очень перспективная. Мне кажется, что здесь огромный потенциал, но надо хорошо поработать, чтобы получилось. И с самой технологией мультиплатформенной разработки, и с UI-фреймворком.

Жизнь после Kotlin

Как выглядит сейчас твоя жизнь?

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

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

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

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

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

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

Уже понял, что очень важна физическая активность, чтобы был какой-то спорт, какая-то нагрузка. Потому что от этого сильно зависит настроение.

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

Вот, это какие-то очень простые базовые вещи. А всё остальное пока непонятно. Я полтора месяца как выключился из очень напряжённой работы, и понятия не имею, как теперь жить!

Но смотрю с оптимизмом, я уверен, что разберусь.

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

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

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

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

То есть вот три совсем разных примера: технический проект, нетехнический проект и чуть-чуть технический проект.

Существуют серийные стартаперы, которым интересно создавать новые проекты и справляться с вызовами ранней стадии, а когда проект уже встал на рельсы, становится скучно и нужен новый. Ты с нуля развивал и Kotlin, и Alter, а в твоём объявлении об уходе сказано Im not a lifetime project type of person. Ощущаешь ли ты себя серийным стартапером, интересна ли тебе именно ранняя фаза?

Совершенно не ощущаю. Эта начальная фаза, самая нервная, сейчас ощущается очень утомительной. И сейчас на ещё один проект даже не замахиваюсь, потому что у меня нет сил на эту фазу.

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

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

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

Но думаю, это не то же, что я уже должен кем-то руководить или я должен уйти из IT до 40, а немного другая история. Мне кажется, если кому-то скучно в IT (будь то в 30 или 40), то человек оттуда уходит просто потому что скучно и ничего интересного больше нет. Или разочарование, или усталость, есть какие-то причины уйти из профессии.

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

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

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

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

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

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

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

И такого достаточно много, но это всё очень близко. Это даже происходит внутри той же самой компании JetBrains. Поэтому мне странно говорить про переход туда.

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

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

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

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

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

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

Думаю, терапия вполне могла сказаться, но не совсем так. Что она помогает делать? Убирать искажения, ошибки. И без терапии я мог бы думать, что я должен тянуть Kotlin. Не что глупо его бросить, а что это мой долг.

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

Alter

Что именно сейчас входит в твою работу над Alter?

Как сооснователь я участвую во всяких стратегических разговорах, планированиях и так далее.

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

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

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

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

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

В вашей вакансии написано бэк на Django, фронт на React. Вероятно, у многих возникнет вопрос: А почему не Kotlin?

Правильный вопрос, очень понятный. На самом деле просто из-за того, что мы начинали писать эту систему, когда Kotlin ещё не был готов. Нельзя было просто взять и нанять разработчиков с рынка. Поэтому исторически код на Python.

В какой-то момент была завиральная мысль переехать с Python на Kotlin, но уж больно накладно, много кода уже. Сейчас было бы, конечно, самое то на Котлине писать. Но, к сожалению, уже нет.

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

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

Во-вторых, статическая типизация не только стандартный инструмент в языке программирования вроде Kotlin или Java, а ещё и дисциплина. В Python есть опциональная статическая типизация, мы ей пользуемся. Сначала писали без неё, а сейчас её добавляем.

Не то что бы всё становится как в Kotlin, конечно. В Котлине компилятор действительно за тебя всё проверит. В Питоне это просто подсказки, которые немножко помогают.

Но я не адепт статической типизации ради статической типизации. Я адепт прагматичных инструментов. И в своё время я выбирал, на чём писать Alter, исходя из того, каких программистов можно найти на рынке за доступные стартапу деньги. Тогдашнему стартапу, который зарабатывал совсем мало и был не готов платить большие зарплаты, как мы делаем сейчас. Из тех технологий, которые были доступны, Python был хорошим компромиссом.

Понятно, что работа над Alter во многом отличается от Kotlin. Но поскольку здесь тоже участвуешь в ответственных стратегических решениях почему в случае с Kotlin устал и ушёл, а в этом случае так не устал? Чем различаются эти случаи лично для тебя?

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

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

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

И здесь я не один самый главный, мы сооснователи с Олей Китаиной, она СЕО, то есть на ней много лидерских функций и тому подобных вещей. Я в этом смысле нахожусь в гораздо более расслабленной позиции.

Раньше ты был в проекте с большими ресурсами (человеческими, финансовыми и так далее), а теперь в куда менее масштабном. Нет ли ощущения, что пересел с танка на самокат, и для решения задач не хватает привычной мощи?

Про танк и самокат хорошо сказано: я очень люблю кататься на самокате, это одна из моих отдушин.

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

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

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

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

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

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

Последний вопрос про Kotlin

И напоследок. Ты 10 лет отвечал на бесконечные технические вопросы про Kotlin, а теперь их будут задавать другим людям. Хочется адресовать тебе вопрос в последний раз. Многие люди хотели появления тернарного оператора, и ты ранее говорил, что вы вроде бы нашли способ удачно реализовать его. Почему в итоге он не появился?

С тернарным оператором всё оказалось печально: была мысль, что мы синтаксически разобрались, но оказалось, что всё же не до конца. В общем, там очень замороченно, нет способа добавить его дёшево и элегантно. И платить за тернарный оператор какую-то осмысленную цену я не решался. А Рома просто решил, что не будет этого делать. И правильно сделал!

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

Если вы добрались до конца этого текста, с высокой вероятностью вы либо Android-разработчик, либо бэкендер с JVM-опытом. В первом случае вас может заинтересовать онлайн-конференция Mobius (13-16 апреля), а во втором JPoint (13-17 апреля). На сайте каждой уже можно узнать программу.

Но даже если вы не из мира Android/Java у нас найдётся интересное и по другим темам (JS, .NET, тестирование).

Подробнее..

Программа HolyJS нюансы DevTools, минусы GraphQL, инструменты a11y

01.04.2021 14:11:52 | Автор: admin


Осталось меньше месяца до конференции HolyJS (20-23 апреля, онлайн) пора рассказать, что именно там будет. Под катом описания докладов с разбивкой по тематическим блокам. А для начала несколько вопросов для затравки:


  • В чём недостатки GraphQL?
  • Зачем OCaml на фронтенде?
  • Чего вы не знаете о DevTools?
  • Как писать надёжные тесты для Vue?
  • Как сделать свой DSL-язык легко и непринуждённо?
  • Как добиться на дешёвом устройстве плавности дорогого?
  • Как отобразить 100500 метрик и не сойти с ума?
  • Как принести в JS типы ещё радикальнее, чем в TypeScript?

Ответы на всё это и многое другое в докладах.


Оглавление


Инструменты
Производительность
Фреймворки
Языки
Микрофронтенды
Основы
Визуал
Заключение



Инструменты


Всё, что вам нужно DevTools, Виталий Фридман


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


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


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




Сделать сайт доступным за 60 секунд, Глафира Жур и Денис Бирюк


Все вокруг говорят о доступности: она нужна, важна и должна быть добавлена в проект на самом раннем этапе. Но никто не говорит, как рядовому фронтенд-разработчику, путающему a11y и 11ty, разобраться в вопросе. Какой пакет подключить в Webpack, чтобы проблемы решались на этапе сборки? Где в консоли найти отчёт об ошибках a11y? И как проверить результат своей работы после деплоя?


Чем хороши спикеры: Два спикера два мира. Глафира эксперт по интерфейсам, Денис любит прикручивать автоматизацию к фронтенд-проектам.


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


Кому будет полезно: Знаете про доступность, но не практикуете, потому что ну это же сложно? Тут расскажут, что конкретно добавить в ваш проект, чтобы пользователям было удобнее.




WebXR в реальной жизни, Роман Пономарев


Дополненная и виртуальная реальности отличные инструменты для демонстрации продукта. А браузер всегда под рукой и позволяет визуализировать что угодно без скачивания приложений. В докладе рассмотрим существующие технологии для работы с AR и VR в вебе, стандарты, лежащие в их основе, а также затронем состояние XR-индустрии в целом, увидим демки и узнаем, как можно продать это всё бизнесу.


Чем хорош спикер: У Романа есть практический опыт в теме. Некоторые зрители также могут знать его по Фронтенд Юности.


Чем хороша тема: Даже в доковидные времена Virtual/Augmented Reality всё больше проникала в нашу жизнь, а уже теперь и подавно. Тем временем спецификация WebXR взрослеет и позволяет делать всё больше.




Make your authentication flow perfect, Anton Nemtsev


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


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


Кому будет полезно: Если вы работали с формами авторизации и регистрации, либо вам интересно, как можно сделать их удобными




Воркшоп. GitLab + CI/CD + JavaScript = любовь, Виталий Слободин


Стать передовиком производства можно при помощи настроенных процессов. Это знают все. Но как настроить процессы вопрос сложный, лучше всего начинать с машин, потому что они податливее, чем люди. Первым шагом для настройки непрерывного цикла разработки может стать CI/CD. Цель Виталия показать вам трюки и рецепты для создания CI/CD, чтобы все инженеры были довольны и счастливы.


Чем хорош спикер: Ранее на HolyJS Виталий рассказывал про headless browsers, и доклад приняли отлично: глубоко знать тему ему помог личный опыт разработки PhantomJS. А теперь он работает в GitLab так что снова поговорит о том, к чему сам причастен.


Чем хороша тема: В результате развития подхода DevOps сейчас JS-разработчикам самим приходится думать про то, как их код будет тестироваться и собираться. Виталий обещал дать набор готовых рецептов.




Serverless и Edge Computing на практике, Алексей Тактаров


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


В докладе Алексей рассмотрит практическое применение лямбд от Vercel и Cloudflare: проектирование фронтенд-микросервисов, их авторизацию, работу с CDN и особенности кеширования на пограничных (edge) серверах. Будут затронуты архитектурные приемы для построения легко масштабируемых сервисов на примере задач: рендеринг PDF/DOCX-документов, генерация above-the-fold CSS для страниц на лету в Cloudflare Workers, отрисовка OG-картинок.


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


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




Гиперавтоматизированный пайплайн, или Почему матрица должна победить, Алексей Золотых


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


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


Чем хорош спикер: Пишет на JavaScript c 2007 года, давно участвует в HolyJS в качестве участника программного комитета.


Чем хороша тема: В наше девопсовое время вопросы автоматизации пайплайнов всё актуальнее.




Воркшоп: Знакомство с MobX, Назим Гафаров


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


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



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


Быстрый и красивый веб на дешевом девайсе с голосовым, пультовым и тач-управлением, Павел Ремизов


Вот реальная инженерная задача: есть SberBox за 3 000 рублей и SberPortal за 30 000. Производительность разная, но написанный на веб-технологиях интерфейс должен работать одинаково плавно. Павел Ремизов расскажет, как решали эту задачу, как ускоряли анимации и как использовали React, как кэшировали статику на девайсах и уменьшали размеры бандла, как отлаживали и измеряли производительность.


Чем хорош спикер: Имеет очень хорошую экспертизу в производительности и в реальных аспектах ее оценки


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




Оптимизация производительности высоконагруженного поиска на стороне фронтенда, Даниил Гоник, Ян Штефанец


Даниил Гоник и Ян Штефанец работают над редактором наподобие Google Docs.


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


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


Кому будет полезно: Если вы реализовывали подсветку элементов на сайте по тексту, если вы работали когда-либо с WYSIWYG




Стабильность React Native-приложения с круглосуточным up time, Евгений Гейманен


Евгений Гейманен расскажет поучительную историю про проблемы производительности одного React Native-приложения. Из нее вы узнаете:


  • Как оптимизировать перерисовку виртуального React DOM;
  • Как правильно мутировать redux-store, коннектить к нему компоненты и описывать селекторы;
  • Как находить проблемные места в Android-версии React Native-приложения;
  • Какими инструментами стоит обзавестись для того, чтобы держать руку на пульсе приложения и начать думать как приложение.

Чем хорош спикер: Евгений Lead Developer в Bolt и ему есть что рассказать как с точки зрения технологий, так и с точки зрения истории


Чем хороша тема: Мониторинг это одна боль. Мониторинг приложений установленных на куче девайсов другая. Это реальный опыт решения некоторого количества проблем производительности на примере React Native приложения




Производительность в полевых условиях, Александр Шушунов


Вы уже прочитали все статьи Эдди Османи. Выучили, как расшифровываются аббревиатуры RAIL, PRPL, LCP и прочие. Свежий перформанс-гайд Фридмана второй месяц висит в открытой вкладке. Но как все это применить к текущему проекту? Как улучшить производительность здесь и сейчас? Александр не знает и не будет давать общие советы зато расскажет, что сделал он. И надеется, что его опыт борьбы за секунды и байты покажется вам интересным.


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


Кому будет полезно: Если вам интересна тема производительности клиентских приложений. Особенно, если вы никогда специально не работали в этом направлении, и хотите посмотреть на свой текущий проект под другим углом




Фреймворки


А нужен ли нам GraphQL?, Павел Черторогов


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


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




How we built our custom React SSR Engine, Erzhan Torokulov


Эржан Торокулов расскажет, как несколько лет назад в Toptal сделали React Rendering Engine: он уже тогда умел делать ряд вещей, которые теперь знакомы по инструментам вроде Gatsby и Next.js. Компания по-прежнему использует этот инструмент для своего сайта и продолжает его развивать.


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


Чем хороша тема: Никого не удивить сайтом на React, Vue или даже Svelte, и доклады про React стали очередными. Индустрия перестала строить велосипеды и догадалась положить рельсы. Но иногда хочется большего и это такой случай.




Как в GitLab @vue/test-utils обновляли, Илья Климов


За последний год Илья дважды обновлял @vue/test-utils в GitLab и оба раза это заняло огромное количество времени. Для того, чтобы это сделать, ему пришлось отправить более десятка pull-request'ов во @vue/test-utils, обсудить в RFC внутри GitLab возможность создания собственного форка, глубоко разобраться в механизмах реактивности Vue и нещадно воевать за качество кода и тестов как в GitLab, так и в самом @vue/test-utils.


Теперь Илья со зрителями HolyJS будет разбираться в следующих вопросах:


  • где спрятана сложность в тестировании подобных систем;
  • какие ошибки были допущены разработчиками @vue/test-utils и можно ли было их избежать;
  • как магия реактивности усложняет построение надежной системы и как с этим бороться;
  • как писать надежные тесты для Vue.

Доклад будет интересен не только тем, кто использует Vue.js в повседневной разработке, но и всем, кто верит в unit-тестирование как ключ к управляемости любого проекта.





Языки


Свой язык с поддержкой sourcemaps за полчаса, Дмитрий Карловский


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


Чем хорош спикер: Является автором собственного инструмента для работы с DSL.


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




Зачем OCaml на фронтенде, Дмитрий Коваленко


Возможно, вы слышали о таких технологиях, как ReasonML/Resсript и bucklescript. Всё это OCaml на фронтенде. Но как всё это работает? Как OCaml компилируется в JS? Почему это круто? Всё это вы узнаете в данном докладе.


Чем хорош спикер: Дмитрий один из немногих людей, использующих OCaml на практике.


Чем хороша тема: Про Ocaml много кто слышал, но мало кто представляет, что это такое и чем он замечателен. Если вы хотите разнообразить свои знания мира языков транспилируемых в JS и узнать (а возможно и подсесть на) что-то новое вам сюда.




Strict Types in JavaScript, Виктор Вершанский


Говорят, что JavaScript динамически типизированный. А если хочешь другой типизации, надо использовать что-то вроде TypeScript, который не заставляет писать без ошибок типов в runtime.


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


Чем хороша тема: Такое решение еще никто не представлял, это самый эксклюзивный доклад из всех возможных.


Кому будет полезно: Всем, кто:


  • задумывался про проблему строгой типизации в рантайме
  • устал ловить TypeError в рантайме
  • хочет поломать мозг неординарными решениями



How to outsmart time: Building futuristic JavaScript applications using Temporal, Ujjwal Sharma


Почти 25 лет JS-разработчики страдали каждый раз, когда нужно было что-то сделать со временем и датой. Было сломано много костылей, сорвано много подорожников, и прожжено много стульев. И вот, наконец, будущее наступило: появилось долгожданное Temporal API, которое позволяет работать с временем и датой, как все давно мечтали. Уджвал Шарма покажет, как это всё работает.


Чем хорош спикер: Данный спикер помогает разрабатывать этот стандарт (ТС39 делегат) и внедряет его в браузеры.


Чем хороша тема: Поскольку текущий Date API не слишком хорош, разработчики подключают внешние библиотеки для работы с датами вроде date-fns, moment, luxon и тд. Temporal предложение для самого языка, это будущий стандарт работы с датами.





Микрофронтенды


Микросервисы, которые делаем мы, Олег Сметанин


Олег представит обзор способов проектирования микросервисов, API, использования шаблонов и паттернов взаимодействия; DevOps и тестирования на основе обобщения опыта проектирования и аудирования приложений с микросервисной архитектурой для разных индустрий: финансы, ритейл, ресурсы. Вы узнаете, как использовать TypeScript/NestJS для реализации микросервисов под NodeJS для cloud-native-приложений и как применять TypeScript/React для фронта.


Чем хорош спикер: У спикера большой опыт в построении больших систем с высокой нагрузкой.


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




Микрофронтенды на модулях и веб-компонентах, Юрий Караджов


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


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


Чем хороша тема: Доклад про лёгкий и прозрачный способ внедрения микрофронтендов. Анализ преимуществ и недостатков. Практическая применимость и подводные камни для популярных фреймворков.




One logic to rule them all, from clot to wire, Владимир Минкин


Презентация концепции Strings API с библиотекой Wire для создания веб и мобильных приложений, которые повторно используют бизнес-логику в языке Dart. Цель этого доклада: продвинуть идеи создания программных систем, состоящих из плагинов отдельных компонентов и блоков, которые можно тестировать индивидуально, визуально и в модульных тестах.


Разделение ответственности это не новая идея и того же можно добиться с помощью уже существующих инструментов. Разница с Wire заключается в том, что он предоставляет простой и семантически красивый API с четким разделением задач на уровне API-библиотеки: общение в одном месте, данные в другом. В докладе будет уделено внимание архитектуре систем визуального ПО и тому, что всё это MVC, а также архитектуре Flux.


Чем хорош спикер: Владимир обладает хорошим опытов разработки приложений в ООП стиле. Автор библиотек. Имеет опыт боевой опыт разработки на Dart и Haxe. Он может помочь зрителю посмотреть на разработку под другим углом.


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





Основы


Напомни через минуту, или Как считать время в браузере, Никита Дубко


Пришла как-то к разработчику Мефодию задача: показывать через заданное пользователем время попап с напоминалкой, что время прошло. Написал Мефодий setTimeout, запушил, тикет закрыл. А пользователь так и не дождался своей напоминалки.


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


Чем хорош спикер: Обладает огромным опытом выступлений и предоставил очень интересную тему


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




Оптимизация синхронной асинхронности, Дмитрий Махнев


Дмитрий Махнёв хочет показать проблему обманчивости простоты async/await на реальном кейсе и приблизительные пути решения и профиты от этого.
Что вас ждет в докладе:
очевидное нахождение проблемы синхронной асинхронности в реальной задаче (индексе сайта);
удивительно неправильная попытка решения;
героическое ускорение примерно на порядок без переписывания на Rust;
неловкая ситуация с unhandledRejection, пролетающей сквозь try/catch;
пара полезных абстракций.


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


Чем хороша тема: async/await, которого боялись несколько лет назад, стал чем-то обыденным, повседневным, повсеместным. Раньше у нас был callback hell, then hell. Нет, await hell не выглядит чем-то страшным для читабельности кода. Он в определённых ситуациях критически влияет на производительность. Дима на собственном примере покажет, как правильно работать с async-await.




Machine Learning and JavaScript. Unleashing the power of sentiment analysis, Artur Nizamutdinov


Если вы работали с ML на Python и хотите перенести часть опыта в браузер (или не имели дела с МL, но очень хотите попробовать) приходите на доклад Артура Низамутдинова. Он покажет на живом примере, для каких задач можно использовать машинное обучение в браузере, и разберёт дообучение моделей.


Чем хорош спикер: У Артура сильная экспертиза. А еще он сильно переработал доклад конкретно для нашей аудитории. Так что тут будут не просто знания, а знания, поданные именно для слушателей конференции


Чем хороша тема: ML сейчас почти так же моден, как JavaScript. И этот доклад совмещает в себе эти два мира, показывая на практическом примере, для каких задач мы можем использовать ML в браузере.





Визуальное


Анализ больших географически распределенных данных через визуализацию на карте, Никита Швыряев


Доклад основан на опыте построения трёх систем для бизнес-анализа географически распределенных данных. В основном доклад будет построен вокруг использования библиотеки Mapbox GL JS. Рассмотрим востребованную функциональность, осветим проблемы и решения во многих задачах (от загрузки данных до кластеризации). Целевая аудитория JS-разработчики, которые работают или планируют работать с визуализацией на карте.


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


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




Как отобразить 100500 метрик распределенной системы и не сойти с ума, Андрей Гончаров


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


Чем хорош спикер: Андрей работает в компании, которая оперирует огромным количеством машин и метрик на них. Задача правильного и, что самое главное, доверительного отображения метрик первостепенная.


Чем хороша тема: Любой бизнес любит метрики продуктовые, технические. Почти любой разработчик знает что такое медиана, 75, 90, 95 перцентиль. Правда, не всегда понятно пора бежать чинить, мы горим. Андрей расскажет, как можно обрабатывать метрики, чтобы они лучше отображали существующую картину.




Браузерный игровой движок как pet-проект, Михаил Реммеле


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


Чем хорош спикер: Опыт работы в геймдеве, крайне редкий среди спикеров JS-конференций. Опыт разработки собственного игрового движка под браузер.


Чем хороша тема: Не распространённый, а возможно и редкий опыт в мире фронтенда. В докладе будет рассказано как автор с нуля написал свой игровой движок и с какими архитектурными сложностями столкнулся.





Заключение


Доклады это хорошо. Но напомним, что HolyJS не сводится только к тому, чтобы сидеть и слушать:


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


Напоминаем, HolyJS пройдет с 20 по 23 апреля в онлайне. Вся информация и билеты на сайте.

Подробнее..

Чем неудобен хабровый WYSIWYG-редактор

29.01.2021 20:06:23 | Автор: admin

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

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

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


Мой сценарий

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

Поэтому я давно радовался, что Хабр поддерживает Markdown и HTML. И поэтому полюбил писать хабрапосты в редакторе vim, он как раз про эффективную клавиатурную работу. Оформлял в основном с помощью Markdown, а где его возможностей не хватало, добавлял HTML-теги: например, чтобы текст обтекал картинку или для оглавления поста. Работа над постом может выглядеть так идеальный дзен, ничто не отвлекает, есть только я и текст:

Мне не мешало, что vim не может показывать иллюстрации. Я подбирал нужные изображения, загружал их на Habrastorage, а дальше в нужные места текста вместо картинки вставлял сразу маркдаун-ссылку на неё.

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

Во многих случаях до публикации давал текст кому-то на ревью, тогда возникала промежуточная стадия: сначала скопировать всё в Google Docs (там людям удобнее), а уже оттуда после правок на Хабр.


В чём боль

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

1) Markdown-штуки срабатывают, когда пишешь в самом редакторе, но вот если хочешь скопипастить в него готовый текст с уже проставленной разметкой это как повезёт. То есть, если хочу использовать Markdown, мне желательно писать посты прямо на Хабре, а не в vim с его великой системой хоткеев. Меньше клавиатуры и терминала, больше мышки и браузера.

2) Даже в самом редакторе работает не всё из Markdown. Выделить жирным или сделать заголовок это пожалуйста, а вот поставить ссылку или разделитель у меня не получилось, это приходится делать через стандартные меню.

3) Если для Markdown хотя бы заявили поддержку, то в случае с HTML сказали только об уходе от него. А ведь с его помощью пользователи Хабра реализовывали целый пласт возможностей. Хочешь, чтобы текст обтекал картинку не справа, а слева? Хочешь подзаголовок определённого цвета? Хочешь сделать кликабельную картинку? Хочешь вручную задать ей размеры? Markdown всего этого сделать не даёт.

4) Каким-то из пропавших HTML-возможностей сделали альтернативные реализации, но порой неудобные. Например, оглавление поста. Раньше в vim писал тег <anchor> у каждого подзаголовка. Теперь предлагается делать в хабровском редакторе так: добраться в тексте до нужного места, вызвать меню, выбрать вариант якорь, затем ткнуть мышкой в сам якорь (с клавиатуры это сделать нельзя), а затем почему-то ещё раз ткнуть мышкой над ним в поле укажите id якоря (с клавиатуры это сделать тоже нельзя).

И повторить всё это по числу подзаголовков (например, 10 раз). Объём раздражающей мышиной возни ощутимо возрос.

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

Это всё не считая мелких багов, их-то можно просто репортить. Перечисленные выше вещи выглядят не столько багами, сколько ожидаемым поведением: не знаю, решат позже изменить ситуацию или нет. И в сумме они вызывают ощущение тебе с твоими vim и Markdown тут не рады, слушай песню "Валенки" и редактируй WYSIWYG в браузере.

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

Чего хотелось бы

Кто-то тут мог бы полезть на баррикады: Это ужасно, IT-ресурс отходит от интересов IT-специалистов ради каких-то людей, которые не могут осилить HTML. Редактор постов должен быть рассчитан на ценителей кода, чтобы приманивать опытных IT-специалистов, которым есть чем поделиться. А остальные пускай отсеиваются, уровень контента только повысится. Верните всё как было.

С таким радикальным подходом я не согласен. Во-первых, на сайте есть разделы вроде дизайн и научпоп, так что польза WYSIWYG понятна: человек, далёкий от HTML, может при этом писать отличные тексты о космонавтике.

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

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

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

А ещё, по-моему, возможно было бы порадовать айтишников следующим образом: не только работать над визуальным редактором, но и возродить закрытое на реконструкцию API Хабра. Был бы очень рад, если бы после сочинения текста в vim мог прямо из терминала командой отправить его в хабрачерновики. А потом открыть на сайте и там уже при необходимости поправить что-то в WYSIWYG-режиме. И для загрузки картинок был бы рад тому же.


Нужен фидбек

Но тут у меня возник вопрос: нужно ли такое кому-то, кроме меня? Может быть, все рады новому редактору, а я со своим сочинением постов в vim выгляжу вот этим странным пользователем из комикса xkcd?

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

А напоследок поделюсь уже имеющимся фидбеком, который показывает, что грань между айтишным и не-айтишным подходами вообще не такая строгая, как кажется. Я пишу здесь для блога компании JUG Ru Group. Мы делаем конференции для разработчиков, но сами далеко не все разработчики. И среди моих коллег, постящих что-то на Хабр, часть никогда не открывали vim и пишут тексты в человеческих редакторах вроде Google Docs. Казалось бы, вот они должны быть рады новому WYSIWYG-редактору: можно в Google Docs сделать всю вёрстку визуально, а потом просто скопипастить на Хабр.

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

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

Подробнее..

Программа Mobius Android, iOS и всё, что между ними

31.03.2021 14:23:38 | Автор: admin


Совсем скоро пройдёт очередной Mobius (13-16 апреля, онлайн). И теперь, когда известна его программа, пришло время рассказать Хабру, что за доклады там представят.


У конференции будет четыре параллельных трека:
iOS
Android
кроссплатформенный (Flutter, Kotlin Multiplatform)
универсальный (мобильные темы, не зависящие от платформы)


Порой сложно нащупать грань между разными категориями: в iOS-треке есть пара докладов, которые тоже рассматривают Kotlin Multiplatform, но конкретно с iOS-стороны. Поэтому учтите, что где-то деление оказывается условным. А теперь, с этим осознанием, можно погружаться.


Оглавление




iOS


Введение в Apple Metal, Георгий Остроброд


Многих непосвящённых Metal пугает, но этот доклад поможет перестать бояться: позволит освоиться, начав с самых основ. Мы узнаем, что это такое, из чего состоит, как с этим работать и почему это несложно. Будут затронуты следующие темы: сам Metal (рендер и вычисления), MetalKit, Metal Performance Shaders, базовые инструменты для профилирования и отладки.


Чем хороша тема: Докладов и в целом информации про Metal очень мало, а на русском её вообще практически нет.


Чем хорош спикер: Георгий работает в компании, которая делает самый известный на рынке инструмент для художников/иллюстраторов на iOS Procreate. Экспертиза этой компании и лично Георгия в работе с GPU очень высока.




Оптимизация графики на Metal, Георгий Остроброд


После объяснения основ Metal в предыдущем докладе Георгий перейдёт к более продвинутым вещам. Этот доклад рассказывает о методах профилирования и оптимизации графики на Metal, возможных слабых местах и их преодолении, оптимизации подготовки данных, отрисовки и шейдеров, использование compute-шейдеров и их оптимизации.


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


Кому будет полезно: Всем, кто хочет перейти на Metal с OpenGL или с CoreGraphics, или просто хочет начать работать с Metal: любой быстро столкнётся с тем, что его код надо будет оптимизировать.




Оптимизируем размер приложения на практике, Дениз Каплан


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


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

Чем хорош спикер: Дениз является ключевым инженером в Core-команде основного приложения Сбера.


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




Как переписать сетевой слой так, чтобы не пришлось переписывать его снова, Александр Аносов


История о том, как в iOS-приложении Delivery Club переписали сетевой слой. Предпосылки совершенно банальные и до боли знакомы каждому: старое, покрытое пылью легаси, баги с разлогинами, код, запутанный похлеще, чем у Да Винчи. Решение элегантное новый сетевой слой, адаптер для старого протокола, фасад для выбора на основе feature toggle, постепенная раскатка через Firebase, мониторинг нефатальных ошибок. В итоге, без единого изменения на уровне сервисов, все запросы ходят в сеть по-новому.


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




Make widget great again, Александр Верестников


Близится год со дня, когда Apple презентовала виджеты на WWDC 2020, но по-прежнему далеко не все умеют с ними работать. Пора это исправить.


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


Чем хорош спикер: Реализовал виджет в своём приложении, поэтому знает о некоторых подводных камнях, с которыми поделится.


Чем хороша тема: Виджетов пока мало меньше, чем стоило бы. Это стоит исправить.




Отслеживание установок на iOS без эвристики и AdvertisementID, Дмитрий Куркин


Apple закрывает доступ к AdvertisementID. Все сервисы трекинга во главе с Facebook воют, что все пропало и так жить нельзя. Вероятность того, что пользователь согласится дать доступ к идентификаторам, очень низка.


Однако вместо этого Apple выдала нам возможность проследить установку приложения без всякой эвристики. Теперь мы можем использовать deeplink не только в установленном приложении, но и с учётом его установки на устройство.


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


Чем хороша тема: В марте с релизом iOS 14.5 Apple отключила доступ к IDFA без разрешения пользователя, поэтому вопрос как измерять эффективность рекламы своих приложений без этого доступа актуален для многих.




Разработка на Kotlin/Native и Swift: Новые технологии и их внутренности, Айдар Мухаметзянов


По очевидным причинам тулинг и технологии вокруг Kotlin Multiplatform несколько отставали на платформах от Apple. Все-таки Kotlin полноправный гражданин в Android-разработке, но не в iOS-мире.


Айдар (JetBrains) расскажет, что делается, чтобы это исправить. Вы узнаете про Kotlin/Native-плагин для AppCode: для чего он был создан и что умеет на данный момент. Спикер покажет свежие разработки в области Kotlin Multiplatform, которые ещё толком не оформились в конечные продукты, и расскажет, как они работают изнутри.


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


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




Kotlin Multiplatform Driven Development, Степан Мирский


Доклад об использовании библиотек, написанных на Kotlin Multiplatform (далее KMM), с точки зрения iOS-разработки. Разработку и поддержку каких бизнес-процессов можно оптимизировать с помощью KMM-библиотек? Какую эволюцию KMM-библиотеки прошли в компании спикера? Как там работают с элементами UIKit в связке с KMM?


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


Чем хороша тема: KMM растёт и развивается, всё больше людей используют технологию, и у них возникают проблемы, которые пока не освещены на конференциях.





Android


A/V Sync в Android. Как это работает, Федор Цымбал


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


Из данного доклада вы узнаете о теоретических основах A/V-синхронизации, а также о том, как это реализовано в Android.


Чем хорош спикер: Федор лично работал над синхронизацией видео и звука для созвонов в настольном видео-телефоне (название приложения под NDA)


Чем хороша тема: Решение задачи по синхронизации видео и звука и раньше было полезно, а теперь стало ещё актуальнее.




Как переписать приложение с нуля и потерпеть фиаско, Александр Агейченко


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


Чем хорош спикер: Спикер является тимлидом описанного в докладе проекта и непосредственным участником событий.


Чем хороша тема: Задумывались ли вы когда-либо о том, чтобы переписать приложение с нуля? Александр расскажет интересный кейс, как они попытались это сделать и через какие испытания они прошли на этом пути: проблемы одновременной поддержки двух приложений и разработки фич для двух кодовых баз, сложности с git, раскол и конфликты в команде. Не допускайте подобных ошибок ведь они уже совершили их за вас.




Gradle на прокачку, Сергей Лапин


Доклад про погружение в мир оптимизации Gradle-билдов на нескольких уровнях:


  • Basic для тех, у кого мало ресурсов, но хочется хоть немного оптимизировать сборку;
  • Intermediate для тех, кто готов потратить силы и разок покопаться в билд-пайплайнах;
  • Advanced тем, кто готов заниматься оптимизацией на постоянной основе.
    Всё то, о чем будет рассказано, активно применяется на проекте Vivid Money и постоянно развивается.

Чем хорош спикер: У Сергея большой практический опыт работы с Gradle, и он очень заинтересован в донесении темы до людей.


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




Воркшоп. Распознавание поз: Камасутра с CameraX, Денис Неклюдов


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


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


Чем хорош спикер: Google Developer Expert по Android, Pay и IoT.
Внес лепту в мобильные приложения нескольких стартапов в России, Азии и Европе, сейчас работает в Lyft в солнечной Калифорнии.
Денис внедряет CameraX и Camera2 в одном из проектов Lyft


Чем хороша тема: Возможности работы с CameraX и Camera2 в Android-разработки почти не раскрыты, но API используются во многих проектах




Как мы делаем Яндекс.Карты для Android: DI, Денис Загаевский


Денис расскажет, как приложение делится на модули и как внедряется DI в получившемся многомодульном приложении. Покажет несколько фишек с использованием Dagger 2.


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


Чем хорош спикер: У спикера 10 лет опыта Android-разработки.


Чем хороша тема: Денис рассказывает интересный кейс использования Dagger 2 в многомодульном проекте. Есть интересные технические решения, которые могут натолкнуть на нестандартные подходы к применению Dagger 2.




UI-тесты в вашем проекте, или Паровозик, который смог, Александр Крылов и Севастьян Жуков


Александр Крылов и Севастьян Жуков хотят рассказать про запуск и поддержку UI-тестирования Android-проекта.


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


Чем хорош спикер: Последние годы спикеры работают в infrastructure team и непосредственно занимаются настройкой Gradle, CI/CD, разработкой и поддержкой инфраструктуры. Александр возглавляет эту команду.


Чем хороша тема: Рассказывают весь путь совершенствования CI/CD инфраструктуры в компании: и почему были внедрены те или иные решения, и почему впоследствии от них отказались.




Я тебя создал, я тебя и отменю. Разбираемся как правильно работать с отменой корутин, Павел Ильичев


Корутины все чаще используются в Android-проектах, но вот тема их отмены недостаточно раскрыта. Павел Ильичёв исправит это своим докладом: он расскажет в каких случаях может потребоваться отменить рабочую корутину вручную, как это сделать правильно, а также как отловить сам момент отмены. Если вы уже пользуетесь корутинами или хотите начать, вам пригодится этот доклад.


Чем хорош спикер: У Павла наработан большой опыт эффективной работы с корутинами в различных задачах.


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




Оптимизация сборок Android-приложений: ProGuard, D8, R8. Тайны обфускации, Валерий Петров


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


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

Чем хорош спикер: Плотно работал с ProGuard, R8, D8 и написал большую статью о них.


Чем хороша тема: Каждый Android-разработчик знает о ProGuard, R8, D8. Но вот практической информации о том, как лучше работать с ними, в сети мало.





Кроссплатформа (Flutter, Kotlin Multiplatform)


Как Kotlin разрабатывает фичи на примере корутин и инлайн-классов, Ильмир Усманов


Хотите посмотреть на развитие Kotlin со стороны команды Kotlin-компилятора?


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


Чем хорош спикер: Спикер работает в Kotlin Language Research Team и непосредственно отвечает за разработку корутин и инлайн классов.


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




Kotlin Multiplatform Advanced. Делаем общий DI для iOS и Android, Анна Жаркова


Анна Жаркова расскажет, как организовать работу с DI в приложении на Kotlin Multiplatform:


  • какие нюансы платформ необходимо учитывать;
  • насколько подходят для этого нативные решения;
  • насколько эффективны и удобны существующие решения для DI в общем коде KMM и подходят ли они и для iOS, и для Android;
  • как реализовать DI в общей кодовой базе KMM-приложения без сторонних решений.

Чем хорош спикер: Анна применяет KMM в своей работе на текущей проекте, выработала решение проблемы общего DI для приложений на KMM, и ей есть что рассказать на эту тему.


Чем хороша тема: Про KMM уже были доклады (в том числе и на Mobius), но это первый Advanced-доклад, рассказывающий про построение DI на уровне мультиплатформенного кода.




Flutter for TV, или Как запилить приложение под умные телевизоры, Александр Денисов


Flutter for Mobile в релизе, Flutter for Web в бете, Flutter for Desktop (MacOS, Linux и Windows) в альфе круто, что Flutter официально поддерживает целых шесть платформ. Но что, если мы хотим запустить свое приложение на телевизоре? К сожалению, о Flutter for TV ничего не слышно, кроме чего-то подобного: Since no official support is there yet, presumably you'll be on your own if you try.


В проекте, над которым Александр сейчас работает, поддерживаются Android TV и Apple TV. Да, приложение Flutter можно запустить на ТВ. В этом выступлении спикер расскажет, как это сделать, с какими трудностями и проблемами можно столкнуться и как их решить.


Чем хорош спикер: Dart & Flutter GDE и лид Flutter-компетенции в EPAM.


Чем хороша тема: Доклад основан на уникальном опыте. Никто еще не пробовал запускать Flutter-приложения на тех платформах, про которые будет рассказывать спикер.




Jetpack Compose for Desktop: Делать UI просто и приятно, Николай Иготти


Сейчас разработчиков уже не удивить названием Jetpack Compose но в том случае, когда речь идёт об Android. А на Mobius будет кое-что новенькое: рассказ об применении этого фреймворка для десктопа и о переиспользовании кода между Android и десктопом.


Переносом Jetpack Compose для этой цели занимается компания JetBrains а Николай Иготти именно оттуда, так что информация из первых рук.


Чем хорош спикер: Николай является лидом разработки проекта Compose Desktop в JB
Чем хороша тема: Сейчас Compose Desktop новое и уникальное решение для разработки UI под десктопные платформы.




Воркшоп. Flutter app: Телеграм на минималках, Андрей Савостьянов


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


Приложения реального времени имеют несколько отличные от привычных принципы. Начнём с того, что состоянием управляет удалённый сервер и некоторая очередь событий. С учетом жизненного цикла мобильных приложений приходится не только пинговать и восстанавливать подключение, но и предусматривать механизмы back pressure, когда клиент не может справиться с лавиной данных. Под капотом мессенджера будет протокол websocket, который тоже потребует небольшого тюнинга. Как выглядит работа со всем этим во Flutter?


Чем хорош спикер: Flutter-разработчик с большим опытом. Работает в Surf, в компании, одной из первых в России собравших большую команду Flutter-разработчиков.


Чем хороша тема: К 2021-му "Hello world" на Flutter все уже освоили, а вот наглядная информация по более продвинутым сценариям полезна.




Генерация кроссплатформенной аналитики, Александр Лавриненко


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


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


В ManyChat придумали кроссплатформенное решение, которое помогает не только держать один source of truth, но еще и минимизировать ручной труд. Они живут с этим решением уже полгода, а теперь Александр Лавриненко поделится им и с вами.


Чем хорош спикер: Хорошо знаком сразу с обеими платформами (iOS и Android), ранее уже успешно выступал на Mobius.


Чем хороша тема: Каждый разработчик так или иначе сталкивался с задачей реализации аналитики в своих проектах.




Круглый стол. Очищаем Flutter от ванили. Как мы искали и нашли архитектурный Грааль, Ярослав Магин и Артем Зайцов


Ярослав и Артем расскажут о том, как две совершенно разных команды, начав изучать Flutter, решая разные архитектурные задачи, в итоге пришли практически к одному и тому же решению.
Цель представить собственный вариант реализации архитектуры для Flutter-приложений с решением некоторых концептуальных проблем, связанных с навигацией между экранами и DI. Рассказать, чем не устроили уже существующие решения, почему в итоге было сделано именно так и какая от этого выгода.


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


Чем хорош спикер: Артём и Ярослав авторы своих собственных архитектурных решений. Оба специалиста независимо пошли по одному и тому же пути.


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





Универсальное


Функционал съемки панорам в мобильном приложении от А до Прод. Пилим, внедряем, используем, Геннадий Васильков


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


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


  • Stiching 1 шт. Кроссплатформенная библиотека на C++, собственная разработка;
  • Мобильные приложения 2 шт. iOS и Android;
  • CI/CD 1 шт. Jenkins.
  • Взболтать, но не смешивать. Добавить разработчиков.



Как мониторить скорость и здоровье приложений, и спать спокойно, Александр Попсуенко


Доклад о том, как в Яндексе строили инфраструктуру для отправки метрик скорости работы приложения. А также, как ускорялись и искали причины деградаций скорости.
Целевая аудитория: middle+ Android и iOS-разработчики.
Вы узнаете:


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

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


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




Итак, вы выбрали UDF-архитектуру. Как моделировать стейт?, Михаил Левченко


С каждым днём декларативный UI набирает обороты. Android, iOS, Flutter и React Native-разработчики активно переходят на MVI/Redux/TEA/BLoC и т.д. Но при всех своих плюсах UDF требует от нас решения новых проблем. Одна из них моделирование состояния вашего приложения. И у неё есть решения!


В докладе сперва разберём Algebraic Data Types и почему для моделирования стейта нам они пригодятся больше, чем ООП. Затем мы перейдём к более сложным вещам научимся работать с нашим стейтом как с миниатюрной базой данных. Все эти трюки будут снабжены практичными примерами и корявыми комиксами докладчика для лучшего усвоения материала.
Доклад будет интересен разработчикам среднего уровня и выше. Стоит посмотреть каждому, кто считает, что мода на архитектурные доклады должна остаться в прошлом.


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


Чем хороша тема: Настало время раскрыть правду: архитектуры это не только про разделение кодовой базы на слои, но и про то, что происходит внутри этих слоёв. Управление состоянием это один из сложнейших и недооценённых аспектов этой темы.




Яндекс Go готовит Backend Driven UI, Еркебулан Абилдин


Еркебулан расскажет, как и где будет полезно использовать Backend Driven UI, и поделится решением частых кейсов с небходимостью подстраивать UI под разные группы пользователей. Также вы узнаете, как изменять интерфейс без ожидания нового релиза, когда это нужно продукту. Более того, с помощью Backend Driven UI у маркетинга будет мощный инструмент, помогающий сэкономить время разработчиков и самих маркетологов. Из доклада вы узнаете, как важно найти золотую середину между гибкостью и излишне объёмным ответом API.


Спикер надеется, что данная сессия будет интересна разработчикам, желающим сократить время работы над постоянными изменениями UI (Layout, шрифты, цвета) и уставшим от бесконечных A/B-тестов.


Чем хорош спикер: Реализовывал данную технологию у себя в приложении.


Чем хороша тема: Backend Driven UI давно знаком, но доклад позволяет посмотреть на него с другой стороны. Мы забываем, что это отличный инструмент для A/B тестирования и не только.




Разработчик-преподаватель. Стоит ли заниматься преподаванием?, Екатерина Батеева


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


  • Какие бонусы это принесёт?
  • Какие навыки можно приобрести и прокачать?
  • А какие сложности возникнут, и как их избежать?

Чем хорош спикер: У Кати большой опыт преподавания и взаимодействия с различными образовательными площадками.


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




Making life better with custom DevTools: Story of Facebook and Flipper, Timur Valiev, Michel Weststrate


Разработчикам доступно много готовых инструментов. Но в крупных проектах этого порой оказывается недостаточно и тогда приходит время создания собственных инструментов
Тимур и Мишель работают в Facebook, где кастомных решений немало. Опираясь на примеры плагинов к Flipper, они расскажут:


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

Чем хороши спикеры: Непосредственно причастны к Flipper и кастомным инструментам.


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




Как не написать пиратский корабль вместо фрегата и наоборот, Игорь Кареньков


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


В своём докладе Игорь хотел бы сравнить и оценить эффективность применения самых известных принципов и практик в Android (SOLID, Clean, Modularization, Architecture и т.д.) в зависимости от того, какой проект мы разрабатываем, чтобы каждый мог лучше понимать зачем применяются те или иные инженерные практики, и когда игра стоит свеч.


Чем хорош спикер: Игорь является опытным ведущим Android-инженером продукта Okko, где лично применяет множество инженерных подходов на практике


Чем хороша тема: Выступлений в духе мы затащили себе такую-то штуку множество а вот докладов давайте разберёмся, нужна ли вам такая штука особо не видно.




Kotlin Adoption at Scale, Сергей Рыбалкин, Сергей Рябов


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


Спикеры расскажут, как устроен процесс внедрения Kotlin в Facebook. Вы узнаете, с какими проблемами ребята столкнулись в попытке затащить Kotlin в крупнейшую мобильную кодовую базу: от поддержки языка существующей инфраструктурой до хардкорных оптимизаций JVM-байткода. В докладе присутствует кровавый DEX-код.


Чем хорош спикер: Спикеры напрямую занимаются внедрением Kotlin в крупномасштабном проекте.


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




Напоследок


Напомним, что конференция это не только доклады:


  • Онлайновый Mobius пройдёт на виртуальной площадке: там можно подойти к другому участнику и пообщаться с помощью веб-камер. Так что онлайн не означает совсем без нетворкинга.
  • Общение происходит не только между участниками, но и со спикерами: у каждого доклада есть свой чат, а после завершения доклада можно задать вопросы спикеру в дискуссионной зоне в Zoom.
  • А у компаний-партнёров есть виртуальные стенды, где можно узнать больше о самих компаниях, послушать мини-доклады и поучаствовать в активностях.

Так что ждём вас и на докладах, и на всём остальном. Полная информация и билеты на сайте.

Подробнее..

Project Loom Современная маcштабируемая многопоточность для платформы Java

19.02.2021 18:05:42 | Автор: admin


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


Ответ на эту проблему Project Loom. Он определяет и реализует в Java новые легковесные параллельные примитивы.


Алан Бейтман, руководитель проекта OpenJDK Core Libraries Project, потратил большую часть последних лет на проектирование Loom таким образом, чтобы он естественно и органично вписывался в богатый набор существующих библиотек Java и парадигм программирования. Об этом он и рассказал на Joker 2020. Под катом запись с английскими и русскими субтитрами и перевод его доклада.



Меня зовут Алан Бейтман, я работаю в группе Java Platform в Oracle, преимущественно над OpenJDK. Сегодня я буду говорить о Project Loom.


Мы занялись этим проектом в конце 2017 года (точнее, технически в начале 2018-го). Он появился как проект в OpenJDK для того, чтобы упростить написание масштабируемых многопоточных приложений. Цель в том, чтобы позволить разработчикам писать масштабируемые многопоточные приложения в так называемом синхронном стиле. Это достигается путем доведения базовой единицы многопоточности потока до такой легковесности, чтобы им можно было представлять любую параллельную задачу. Даже задачи, которые блокируются или выполняются в течение длительного времени.


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


План выступления такой:


  1. Начну с пары слов о мотивации этого проекта.
  2. Поговорю о том, как мы имплементировали эти так называемые легкие потоки.
  3. Переключусь на IDE и покажу несколько демо, напишу немного кода.
  4. Наконец, рассмотрю другие аспекты проекта.

Потоки


Платформа Java (и язык, и JVM) во многом построена на концепции потоков:


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

В общем, всё, что касается платформы и инструментов, связано с потоками.


В Java API поток означает java.lang.Thread. В реализации JDK есть только одна реализация потока, которая фактически основана на потоке операционной системы. Между java.lang.Thread и потоком ОС существует связь один-к-одному. Те из вас, кто уже давно работает с платформой Java, могут вспомнить зелёные потоки в ранних выпусках JDK. Я немного расскажу об этом позже. Но по меньшей мере последние 20 лет, когда мы говорим о java.lang.Thread, мы говорим о тонкой оболочке вокруг потока ОС.


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


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


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


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


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


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


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


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


Ладно, что нам с этим делать?


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


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


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


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


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


Это приводит нас к созданию новых API, по существу, несовместимых со старыми. Или в итоге у нас есть синхронные и асинхронные версии API.


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


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


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


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


Что приводит нас к дилемме.



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


Итак, как нам решить эту дилемму? Что, если бы мы могли снизить стоимость потоков и иметь их неограниченное количество? Тогда мы могли бы написать простой синхронный код, который гармонирует с платформой, полностью использует оборудование и масштабируется как асинхронный код. Project Loom именно об этом.


API


Давайте пойдем дальше и поговорим немного об API.


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


Один из вариантов, с которого мы начали и к которому в итоге вернулись, это
использование для легких форм потоков java.lang.Thread. Это старый API, который существует с JDK 1.0. Проблема в том, что у него много багажа. Там есть такие вещи, как группы потоков, загрузчик классов контекстов потоков. Есть множество полей и других API, которые связаны с потоками, которые просто не интересны.


Другой вариант начать все сначала и ввести совершенно новую
конструкцию или новый API. Если вы с самого начала интересовались Project Loom, возможно, вы видели некоторые из ранних прототипов, где мы представили для дешевых легких потоков совершенно новый API под названием fiber.


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


Вопрос, который часто возникает в викторинах: Сколько раз Thread.currentThread() используется при первом использовании популярной библиотеки логирования? Люди, не знающие ответа на этот вопрос, могут ответить 2 или 5. Правильный ответ 113.


Другой широко используемый аспект потока это ThreadLocals. Они используются везде, что иногда не радует. Если сломать Thread.currentThread() или ThreadLocals, то в контексте этих новых более дешевых потоков будет не запустить много уже существующего кода. Поэтому вначале, когда у нас был fiber API, нам пришлось эмулировать Thread API, чтобы существующий код запускался в контексте того, что называлось в то время fiber. Таким образом, мы могли уйти от кода, использующего Thread, не повредив нарыв.


Итак, .currentThread() и Threadlocals очень широко используются. Но в потоках есть и редко используемый багаж. И здесь нам немного помогает расширенная политика депрекации. Если некоторые из этих старых областей со временем могли бы исчезнуть, подвергувшись депрекации, окончательной депрекации и, в конечном итоге, удалению тогда, может быть, удастся жить с java.lang.Thread.


Два года исследований, около пяти прототипов и мы пришли к выводу, что избежать
гравитационного притяжения 25 лет существующего кода невозможно. Эти новые дешевые потоки будут представлены с существующим API java.lang.Thread. То есть java.lang.Thread будет представлять и потоки ОС, и новые дешевые потоки.


Мы также решили дать этим новым потокам имя. Оно появилось благодаря Брайану Гетцу, он придумал название виртуальный поток (virtual thread).


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


Как реализованы эти виртуальные потоки?



Они мультиплексируются поверх небольшого пула потоков операционной системы. Я сказал потоки во множественном числе, и вот тут уместно вспомнить уже упомянутые green threads. Ранние выпуски JDK, особенно 1.0.1.1 с классической виртуальной машиной, поддерживали модель, где потоки мультиплексировались в один-единственный поток ОС. То, что мы делаем теперь, перекликается с этим, но сейчас речь о более чем одном потоке ОС.


Итак, у нас есть набор потоков, на которые эти виртуальные потоки мультиплексируются. Под капотом виртуальная машина HotSpot была обновлена для поддержки новой конструкции: scoped stackful one-shot delimited continuations. Виртуальные потоки объединяют континуации в HotSpot с планировщиками в библиотеке Java. Когда код, выполняющийся в виртуальном потоке, блокируется, скажем, в операции блокировки или в блокирующей IO-операции, соответствующая континуация приостанавливается, стек потока, на концептуальном уровне, вымещается в кучу Java, а планировщик выберет и возобновит другой виртуальный поток в этом же потоке ОС. Исходный виртуальный поток может быть возобновлен в том же потоке ОС или в другом.


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


Пользовательский код, использующий API Java, не знает о распределении, которое
происходит под капотом, а yield и resume происходит глубоко в библиотеках JDK, поэтому мы говорим, что планирование является вытесняющим и не требует сотрудничества со стороны кода пользователя.


С точки зрения стоимости, поток ОС слева, виртуальный поток справа.



Обычно операционная система резервирует около мегабайта стека для потока операционной системы. Некоторые ядра выделяют дополнительные данные ядра, и 16КБ не редкость. Это то, что операционная система имеет на поток ОС. Кроме того, виртуальная машина HotSpot добавляет к этому пару КБ метаданных.


Виртуальные потоки намного дешевле, текущий прототип составляет около 256 байт на виртуальный поток. Еще есть стек, он уменьшается и увеличивается по мере необходимости и обычно составляет пару КБ в этом главное преимущество
виртуальных потоков перед потоками ОС.


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


Самое время перейти от слайдов к IDE и показать вам несколько примеров в коде.


Демо


У меня открыта IDE с пустым методом, и мы начнем с самого начала.


import ...public class Demo {    public static void main(String[] args) throws Exception {...}    void run() throws Exception {    }}

Я упомянул, что мы ввели новый фабричный метод, и начну с использования фабричного метода Thread.startVirtualThread().


import ...public class Demo {    public static void main(String[] args) throws Exception {...}    void run() throws Exception {        Thread thread = Thread.startVirtualThread(() -> System.out.println("hello"));        thread.join();    }}

Вывел сообщение hello, ничего особенного. Это немного отличается от использования конструкторов и метода start(), здесь всего лишь один фабричный метод.


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


void run() throws Exception {    Thread thread = Thread.startVirtualThread(Thread::dumpStack);    thread.join();}

Этот референс-метод просто вызывает дамп стека в контексте виртуального потока.



Возможно, это выглядит немного иначе, чем то, что вы видели бы с обычным java.lang.Thread, потому что фреймы, которые вы видите здесь, не те, что вы видите в обычном JDK. Это своего рода эквивалент запуска потока, потому что виртуальный поток запускает континуацию. Это дает представление о том, в чем вы можете увидеть разницу.


Давайте рассмотрим еще один из аспектов API. Что делает этот startVirtualThread()?


В дополнение к введению фабричных методов для создания виртуальных потоков, текущий прототип имеет новый билдер для создания потоков. Создадим билдер с помощью метода Thread.builder(). Мы можем при этом вызвать несколько методов, позволяющих настроить поток: является ли он потоком-демоном, какое у него имя и некоторые другие аспекты.



В числе этих методов есть virtual(). Создание виртуального потока cо startVirtualThread(), было, по сути, тем же самым. Вот длинная форма того, что я сделал минуту назад:


void run() throws Exception {    Thread thread = Thread.builder().virtual().task(() -> {        System.out.println("hello");    }).start();    thread.join();    }}

Мы снова сделали то же самое многословнее, но теперь использовали билдер потоков. А он избавляет нас от того, чтобы сначала использовать конструктор для создания потоков, а затем вызывать setDaemon() или setName(). Это очень полезно.


Это хорошее улучшение API для тех, кто в конечном итоге использует Thread API напрямую. Запускаем и получаем то же, что и в случае с startVirtualThread().


Еще мы можем создать ThreadFactory.


void run() throws Exception {    ThreadFactory factory = Thread.builder().name(prefix:"worker-", start:0).factory();}

Это создает фабрику потоков она создает потоки, которые называют себя worker-0, worker-1, worker-2 и так далее. На самом деле worker это только начальный аффикс, который добавляется к префиксу. Это еще один полезный способ создания фабрик потоков.


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


Большинство людей фактически не используют Thread API напрямую. Начиная с JDK 5, они перешли на использование ThreadExecutor и других API из java.util.concurrent.


Я хочу показать вам использование одного из этих ThreadExecutor. Мы создадим множество потоков и покажем вам, что на самом деле происходит.


Я собираюсь создать ExecutorService executor:


try (ExecutorService executor = Executors.newVirtualThreadExecutor()) {}

Этот фабричный метод для Executors создает виртуальные потоки. Обратите внимание, что здесь я использую try-with-resources. Одна из вещей, которые мы сделали в Loom, мы модернизировали ExecutorService для расширения AutoCloseable, чтобы вы могли использовать их с конструкцией try-with-resources.


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


Давайте создадим здесь миллион потоков.


import ...  public class Demo {      public static void main(String[] args) throws Exception {...}      void run() throws Exception {          try (ExecutorService executor = Executors.newVirtualThreadExecutor()) {              IntStream.range(0, 1_000_000).forEach(i -> {                  executor.submit(() -> { });              });          }      }      String fetch(String url) throws IOException {...}      void sleep(Duration duration) {...}  }

Я использую IntStream.range(), вместо цикла for. Это вызовет метод executor.submit() один миллион раз, он создаст миллион потоков, которые ничего не делают. Если это запустить, ничего интересного не произойдет Process finished with exit code 0.


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


import ...public class Demo {    public static void main(String[] args) throws Exception {...}    void run() throws Exception {        AtomicInteger counter = new AtomicInteger();        try (ExecutorService executor = Executors.newVirtualThreadExecutor()) {            IntStream.range(0, 1_000_000).forEach(i -> {                executor.submit(counter::incrementAndGet);            });        }        System.out.println(counter.get());    }    String fetch(String url) throws IOException {...}    void sleep(Duration duration) {...}  }

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


Отрабатывает быстро как видите, эти потоки очень дешевы в создании.


Давайте покажу вам, что еще мы можем делать с Executor'ами. У меня есть метод, который просто принимает байты из определенного URL-адреса, создает из него строку. Это не очень интересно разве что то, что это блокирующая операция.


String fetch(String url) throws IOExpection {    try (InputStream in = URI.create(url).toURL().openStream()) {        byte[] bytes = in.readAllBytes();        return new String(bytes, charsetName:"ISO-8859-1");    }}

Он создает сетевые подключения, HTTP-соединение будет ожидать результатов от сервера, мы просто воспользуемся этим как частью примера.


Давайте посмотрим вот на что:


void run() throws Exception {       try (ExecutorService executor = Executors.newVirtualThreadExecutor()) {           Callable<String> task1 = () -> fetch(url:"https://jokerconf.com/");           Callable<String> task1 = () -> fetch(url:"https://jokerconf.com/en");           String first = executor.invokeAny(List.of(task1, task2));           System.out.println(first.length());       }     }

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


Мы используем executor.invokeAny() и даем ему две задачи.
ExecutorService имеет несколько комбинаторов, invokeAny(), invokeAll(), они существуют уже давно. Мы можем использовать их с виртуальными потоками.


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


Я запущу два виртуальных потока. Один из них получит первую страницу, другой вторую, в зависимости от того, что вернется первым, я получу результат в String first. Другой будет отменен (прерван). Запускаем и получаем результат: 200160, то есть одна из страниц размером 200 КБ.


Итак, что произошло: были созданы два потока, один выполнял блокирующую операцию получения данных с первого URL-адреса, другой со второго URL-адреса, и я получил то, что пришло первое. Если запущу еще пару раз, буду получать разные значения: одна из страниц всего 178 КБ, другая 200 КБ.


Это один из комбинаторов. На самом деле, я бы мог хотеть обе страницы и что-то с ними сделать, в этом случае я мог бы использовать invokeAll().


void run() throws Exception {       try (ExecutorService executor = Executors.newVirtualThreadExecutor()) {           Callable<String> task1 = () -> fetch(url:"https://jokerconf.com/");           Callable<String> task1 = () -> fetch(url:"https://jokerconf.com/en");           executor.invokeAll(List.of(task1, task2)); List>Future>String>>                   .stream() Stream<Future<String>>                   .map(Future::join) Stream<String>                   .map(String::length) Stream<integer>                   .forEach(System.out.println);       }     }

Как видите, это не слишком интересно всё, что мы здесь делаем, это invokeAll(). Мы выполним обе задачи, они выполняются в разных потоках. InvokeAll() блокируется до тех пор, пока не будет доступен результат всех задач, потому что вы получаете здесь Future, которые гарантированно будут выполнены. Создаем поток, получаем результат, получаем длины, а затем просто выводим их. Получаем 200 КБ и 178 КБ. Вот что вы можете делать с ExecutorService.


Покажу вам еще кое-что. В рамках Loom мы немного поработали над CompletableFuture, чтобы вы могли делать подобное. Добавить задачу и получить CompletableFuture, а не Future, и тогда я смогу написать такой код:


void run() throws Exception {       try (ExecutorService executor = Executors.newVirtualThreadExecutor()) {           Callable<String> task1 = () -> fetch(url:"https://jokerconf.com/");           Callable<String> task1 = () -> fetch(url:"https://jokerconf.com/en");           CompletableFuture<String> future1 = executor.submitTask(task1);           CompletableFuture<String> future2 = executor.submitTask(task2);           CompletableFuture.completed(future1, future2) Stream<CompletableFuture<String>>                   .map(Future::join) Stream<String>                   .map(String::length) Stream<integer>                   .forEach(System.out.println);       }     }

Я вызываю в CompletableFuture-метод под названием completed(). Это возвращает мне стрим, который заполняется Future в ленивом режиме по мере их завершения. Это намного интереснее, чем invokeAll(), который я показал ранее, поскольку метод не блокируется, пока не будут выполнены все задачи. Вместо этого поток заполняется результатом в ленивом режиме. Это похоже на стримо-подобную форму CompletionService, если вы когда-нибудь такое видели.


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


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


import ...public class Demo {    public static void main(String[] args) throws Exception {...}    void run() throws Exception {        Thread.startVirtualThread(() ->            sleep(Duration.ofSeconds(2));        }).join();    }    String fetch(String url) throws IOException {...}    void sleep(Duration duration) {...}  }

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


void run() throws Exception {        Thread.startVirtualThread(() -> {            Object lock = new Object();            synchronized (lock) {                sleep(Duration.ofSeconds(2));            }        }).join();}

Я запускаю это с диагностическим свойством, которое даст мне трассировку стека, когда поток закреплен.



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


У меня есть более полная демонстрация, переключусь для этого на немного другой проект.


package demo;import ...@Path("/")public class SleepService {    @GET    @Path("sleep")    @Producers(MediaType.APPLICATION_JSON)    public String sleep(@QueryParam("millis") long millis) throws Exception {        Thread.sleep(millis);        return "{ \"millis\": \"" + millis + "\" };    }}

Уже существуют несколько серверов, которые работают с виртуальными потоками.
Есть сервер Helidon MP. Я думаю, MP означает MicroProfile. Helidon настроен, они недавно внесли некоторые изменения, теперь вы можете запустить его со свойством, при котором он будет запускать каждый запрос в отдельном виртуальном потоке. Мой код может выполнять операции блокировки, и они не будут закреплять базовый поток ОС. У меня может быть намного больше запросов, чем потоков, работающих одновременно и выполняющих блокирующие операции, это действительно очень полезно.


Первый сервис, который я вам покажу, что-то вроде эквивалента hello world при использовании подобных служб. Запускаем код из примера выше, переходим в окно терминала и вводим curl-команду.



Curl-команда кодирует параметр миллисекунд обратно в JSON, который возвращается.
Не слишком интересно, потому что все, что было сделано, это сон. Остановлю сервер и вставлю Thread.dumpStack():


public String sleep(@QueryParam("millis") long millis) throws Exception {    Thread.dumpStack();    Thread.sleep(millis);    return "{ \"millis\": \"" + millis + "\" };}

Снова запущу сервер. Я снова выполняю команду curl, которая устанавливает HTTP-соединение с сервером, она подключается к эндпоинту сна, параметр millis=100.


curl http://localhost:8081/sleep?millis=100


Посмотрим на вывод: печатается трассировка стека, созданная Thread.dumpStack() в сервисе.



Огромная трассировка стека, мы видим здесь кучу всего: код Helidon, код Weld, JAX-RS Довольно интересно просто увидеть это всё. Это сервер, который создает виртуальный поток для каждого запроса, что довольно интересно.


Теперь посмотрим на более сложный сервис. Я показал вам комбинаторы
invokeAny и involeAll в простом демо в самом начале, когда показывал новый ExecutorService.


import ...@Path("/")public class AggregatorServices {    @GET    @Path("anyOf")    @Produces(MediaType.APPLICATION_JSON)    public String anyOf(@QueryParam("left") String left,                        @QueryParam("right") String right) throws Exception {        if (left == null || right == null) {            throw new WebApplicationException(Response.Status.BAD_REQUEST);        }        try (var executor :ExecutorService = Executors.newVirtualThreadExecutor()) {            Callable<String> task1 = () -> query(left);            Callable<String> task2 = () -> query(right);            // return the first to succeed, cancel the other            return executor.invokeAny(List.of(task1, task2));        }    }    @GET    @Path("allOf")    @Produces(MediaType.APPLICATION_JSON)    public String allOf(@QueryParam("left") String left,                        @QueryParam("right") String right) throws Exception {        if (left == null || right == null) {            throw new WebApplicationException(Response.Status.BAD_REQUEST)        }        try (var executor :ExecutorService = Executors.newVirtualThreadExecutor()) {            Callable<String> task1 = () -> query(left);            Callable<String> task2 = () -> query(right);            // if one falls, the other is cancelled            return executor.invokeAll(List.of(task1, task2), cancelOnException: true) List<Future<String>>                    .stream() Stream<Future<String>>                    .map(Future::join) Stream<String>                    .collect(Collectors.joining(delimiter:", ", prefix:"{", suffix:" }"));        }    }    private String query(String endpoint) {...}}

Здесь у нас несколько сервисов, они находятся в этом исходном файле под названием AggregatorServices. Здесь есть две службы, два метода я бы сказал: anyOf и allOf. anyOf выполняет левый и правый запросы и выбирает тот, который возвращается первым, а другой отменяет.


Начнем с anyOf. Я вызвал curl-команду:


curl http://localhost:8081/anyOf?left=/greeting\&right=/sleep?millis=200

localhost:8081 это текущий порт, эндпоинт anyOf, и я дал ей два параметра left и right. Я выполняю это и получаю hello world:


{"message":"Hello World!"}$


Причина в том, что сервис приветствия просто выводит hello world, а сервис сна спит 200 мс. Я предполагаю, что большую часть времени hello world будет быстрее, чем 200 мс, и всегда будет возвращаться hello world.


Если я уменьшу сон до 1 мс, то, возможно, сервис сна завершится раньше, чем другой сервис.


Теперь давайте изменим запрос на allOf, который объединит два результата:


curl http://localhost:8081/allOf?left=/greeting\&right=/sleep?millis=1

Запускаю и получаю два результата.


{ {"message":"Hello World!"}, { "millis": "1" } }$


Что интересно в allOf, он делает два запроса параллельно.


private String query(String endpoint) {        URI uri = URI.create("http://localhost:8081").resolve(endpoint);        return ClientBuilder.newClient() Client                    .target(uri) WebTarget                    .request(MediaType.APPLICATION_JSON) Invocation.Builder                    .get(String.class);    }

Кстати, это блокирующий код. Он использует клиентский API JAX-RS для подключения к этому эндпойнту. Он использует вызов invokeAll(), а затем .stream (), .map для получения результата, а затем Collectors.joining(), для объединения в JSON.


Это простой пример разветвления. Интересно то, что тут invokeAll() это вариант, в котором есть параметр cancelOnException. Если вы хотите вызвать несколько задач одновременно, но если одна из них не работает, вы отменяете все остальные. Это важно сделать, чтобы не застрять в ожидании завершения всех остальных задач.


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


Ограничения


Поговорим об ограничениях.


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


Первый сценарий возникает, когда вы используете нативные фреймы. Если у вас есть код JNI, вы вызываете код JNI, он обращается обратно в код Java, а затем этот код Java пытается заблокироваться или совершить IO-операцию. На континуации есть нативный фрейм, мы мало что можем сделать. Это потенциально постоянное ограничение, но подобное должно происходить очень и очень редко.


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


На самом деле это не очень критично. По той простой причине, что всё, что сегодня использует мониторы Java, можно механически преобразовать из использования synchronized и wait-notify в использование блокировок из java.util.concurrent. Так что существуют эквиваленты мониторов в блокировках java.util.concurrent и различные формы блокировок, самый простой из которых ReentrantLock, они очень хорошо работают с виртуальными потоками.


Что вы можете сделать при подготовке к Loom?


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


Предположим, у вас миллион потоков и код, который использует много ThreadLocals. Хотя виртуальные потоки поддерживают ThreadLocals, при их большом количестве требуется много памяти. Тут есть над чем подумать. Мы уже довольно давно работаем в JDK над устранением многих из ThreadLocals, которые использовались в различных местах.


Распространенным является кэширование объектов SimpleDateFormat.
SimpleDateFormat может быть дорогостоящим в создании, они не являются потокобезопасными, поэтому люди повсеместно кэшируют их в ThreadLocals.


В JDK мы заменили кэширование SimpleDateFormats на новый неизменяемый формат даты java.date dateformatter. Он неизменяем, вы можете сохранить его
в static final поле, это достаточно хорошо. Мы удалили ThreadLocals и из некоторых других мест.


Другая сложность сводится к масштабированию приложения и обработке десятков тысяч запросов. Если у вас много данных на запрос или на транзакцию, это может занимать много места. Если у вас миллион TCP-соединений, это миллион буферов сокетов. Если вы оборачиваете каждый из них в BufferedOutputStream, PrintStream или что-то в этом роде, это много памяти. Мы работали над подобными вещами и в JDK, но я уверен, что дальше по стеку у людей будет много данных на запрос или на транзакцию.


Третье, как я уже говорил, переход от мониторов к java.util.concurrent позволяет избежать краткосрочных проблем.


Я говорил в основном о виртуальном потоке как о потоке в коде, но давайте поговорим о нескольких других вещах.


Расскажу немного об отладчике.



При отладке действительно важно, чтобы при движении по шагам, вы
работали в каком-то контексте. Обычно отладчики Java (в IntelliJ, NetBeans, Eclipse) используют интерфейс отладчика под названием JDI, где под капотом находится wire protocol, а в виртуальной машине есть интерфейс инструментов, называемый JVM Tool Interface или JVM TI, как мы его иногда называем. Это все необходимо обновить, чтобы иметь возможность поддерживать виртуальные потоки.


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


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


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


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


Перейдем к виртуальным потокам в профилировщике.


Это тоже очень важная область. Java Flight Recorder был обновлен в сборках Loom для поддержки виртуальных потоков. Я не был уверен, что во время доклада успею продемонстрировать использование JFR с виртуальными потоками, поэтому вместо этого я просто зафиксировал вывод команды print в JFR, просто чтобы показать вам, на что он способен.


В данном случае я сделал запись с JFR.



Он просто называется server.jfr, это имя файла записи. Эта конкретная запись была сделана при запуске сервера Jetty, настроенного для использования виртуальных потоков. Выходные данные показывают одно событие, чтение сокета. И оно произошло в виртуальном потоке. JFR по умолчанию имеет порог, кажется, около 200 мс, он может захватывать медленную операцию чтения, которая занимает больше времени, чем время порога.


Давайте расскажу, что именно здесь запечатлено. virtual = true указывает на то, что это виртуальный поток. Я распечатал всю трассировку стека, поэтому вы можете увидеть, что это действительно работает в виртуальном потоке, мы видим все фреймы, тут используются java.net.url и HTTP для чтения сокета, и это блокирует более чем на 200 мс. Это записано здесь в этой трассировке стека. Это то, что вы можете делать с JFR, что весьма полезно.


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


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


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


Serviceability


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


Я показал вам довольно простую распечатку трассировки стека, когда потоки закреплены. Будут и другие сценарии, значимые для разработчиков. Они не смогут идентифицировать, например, запущенные виртуальные потоки, выполняющие вычислительные задачи (упирающиеся в CPU), они никогда не блокируются. Было бы полезно идентифицировать их.


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


Текущий статус того, где мы находимся с Loom


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


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


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


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


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


Что еще нужно сделать для нашего первого Preview: нам необходимо выполнить перенос на ARM64 или Aarch64, мы были сосредоточены на 64-разрядной версии Intel на сегодняшний день; и нам нужно что-то сделать с дампом потоков.


Направления для будущего развития


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


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


Как вы видите в других моделях программирования, CSP или Actors. У других языков есть каналы, у Erlang есть почтовые ящики. В Java есть вещи, близкие к этому: есть BlockingQueues, SynchronousQueue, у которой нет емкости, LinkedTransferQueue, у которой есть емкость.


Профессор Даг Ли работал с нами над этим проектом, и он обновил реализации блокирующих очередей в java.util.concurrent, так что они дружелюбны к виртуальным потокам. Он также изучает то, что ближе к каналам. Текущее рабочее название этого проекта conduits, а не каналы, потому что у нас есть каналы в пакете java.nio.channels. Посмотрим, как это пойдет.


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


В ранних прототипах Project Loom у нас действительно были первые прототипы для исследований в этой области. И мы вернемся к этому, есть проблемные области, связанные с этим. Распространение исключений и ошибок, отмена выполнения и так далее. На данный момент мы сделали, и я показывал это в демонстрациях, модифицировали ExecutorService, чтоб он расширял AutoCloseable, чтобы, по крайней мере, мы могли иметь некоторую конструкцию, которая гарантирует, что все потоки завершатся до продолжения основного потока.


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


Последний пункт в этом списке отмена. Мы сделали несколько прототипов в этой области, несколько прототипов кооперативной отмены. В Java есть механизм прерывания, это устаревший механизм, но на самом деле он очень хорошо работает с виртуальными потоками. Для большинства разработчиков этот механизм находится на слишком низком уровне. Мы хотим понять, сможем ли мы сделать что-то лучше. У нас были механизмы отмены в ранних прототипах. Я полагаю, что основная проблема заключается в том, что наличие двух механизмов одновременно может сбивать с толку, поэтому необходимо подумать над этим немного времени, прежде чем принимать какие-то решения по этому поводу.


Главные выводы


Основные выводы из этого доклада:


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


Виртуальный поток это не оболочка вокруг потока ОС, а, по сути, просто объект Java.


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


Блокировка в виртуальном потоке стоит мало, что позволяет использовать синхронный стиль.


Немного дополнительной информации


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


Вот ссылки на сборки раннего доступа: https://jdk.java.net/loom
Список рассылки: loom-dev@openjdk.java.net
И вики-страница: https://wiki.openjdk.java.net/display/loom/Main


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


Это все, что я хотел рассказать.


Напоследок традиционный слайд Safe harbor: не верьте ничему, что я говорю.



Как можно понять по этому докладу, на наших Java-конференциях хватает хардкора: тут про Java-платформу порой рассказывают те люди, которые её и делают. В апреле мы проведём JPoint, и там тоже будет интересный состав спикеров (многие знают, например, Джоша Лонга из VMware). Часть имён уже названа на сайте, а другие позже появятся там же.
Подробнее..

Многопоточность на низком уровне

02.03.2021 14:06:25 | Автор: admin

Очень часто при обсуждении многопоточности на платформе .NET говорят о таких вещах, как детали реализации механизма async/await, Task Asynchronous Pattern, deadlock, а также разбирают System.Threading. Все эти вещи можно назвать высокоуровневыми (относительно темы хабрапоста). Но что же происходит на уровне железа и ядра системы (в нашем случае Windows Kernel)?


На конференции DotNext 2016 Moscow Гаэл Фретёр, основатель и главный инженер компании PostSharp, рассказал о том, как в .NET реализована многопоточность на уровне железа и взаимодействия с ядром операционной системы. Несмотря на то, что прошло уже пять лет, мы считаем, что никогда не поздно поделиться хардкорным докладом. Гаэл представил нам хорошую базу по работе процессора и атомным примитивам.



Вот репозиторий с примерами из доклада. А под катом перевод доклада и видео. Далее повествование будет от лица спикера.



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


У этого доклада две цели:


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

Сначала мы разберем, что происходит на уровне железа: CPU и уровни памяти, всё, что с ними происходит. Далее перейдем к ядру Windows. А после этого обсудим на первый взгляд простые вещи, такие как lock. Скорее всего, вы и сами знаете, зачем нужно это ключевое слово. Я же собираюсь объяснить, как оно реализовано.


Микроархитектура


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



Это что-то вроде Intel Core i7 своего времени. Как вы видите, за 70 лет была проделана большая работа.


К чему это все? Обычно предполагают, что байт памяти представлен где-нибудь в ячейке оперативной памяти. Но на самом деле всё иначе: он может быть представлен в нескольких местах одновременно. Например, в шести ядрах, может быть в кэшах L1 и L2, в кэше L3, наконец, может быть в оперативной памяти. Важно упомянуть: в этой архитектуре процессора есть общий кэш L3, и между ними есть Uncore. Этот компонент отвечает за координацию ядер. Также есть QPI, который отвечает за разницу между CPU. Это относится только к мультипроцессорным системам.



Почему нас беспокоит архитектура процессора? По той же причине, по которой нам нужны несколько разных уровней кэшей. У каждого уровня своя задержка. Обращаю внимание на то, что цикл CPU на этой спецификации занимает 0,3 наносекунды и сравнивается с задержкой кэшей L1, L2, L3 и DRAM. Важно заметить, что извлечение конструкции из DRAM требует около 70 циклов процессора. Если вы хотите рассчитать эти значения для своего процессора, то могу посоветовать хороший бенчмарк SiSoft Sandra.



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



Хорошая аналогия для понимания многопоточных систем распределенные системы.


Если вы знаете, что такое очередь сообщений (message queue) или шина данных (message bus), вам будет легче понять многопоточность: похожие вещи есть внутри вашего процессора. Там есть очередь сообщений, шина сообщений, буферы из-за всего этого у нас возникают проблемы с синхронизацией. При взаимодействии ядер информация распределена между L2 и L3-кэшами и памятью. Поскольку шины сообщений асинхронны, порядок обработки данных не гарантирован. Кроме кэширования процессор буферизует данные и может решить оптимизировать операции и отправлять сообщения на шину в другом порядке.


Переупорядочивание памяти


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



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



Итак, давайте рассмотрим одну архитектуру. Пусть это будет x86 или AMD64, здесь не так много Y. Это означает, что она дает нам много гарантий в разных вопросах. На самом деле, x86 изначально не разрабатывался под многоядерные процессоры, поэтому когда Intel начали добавлять новые ядра, они решили сохранить ту же семантику, что раньше.


По таблице видим, что процессор с этой архитектурой может переупорядочить операции сохранить (store), после операций загрузить (load). Когда вы хотите сохранить значение, достаточно отправить сообщение. Ждать ответа не обязательно. А когда вы загружаете данные, вам нужно отправить запрос и ждать ответа. Для этой операции процессор может применить различные оптимизации.


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


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


На Intel x86 вы можете быть уверены, что переменные A и B идут строго последовательно, то есть если удалось загрузить B, то переменная A точно инициализирована. А если всё это выполняется на ARM, то у нас нет никаких гарантий, в каком порядке все это будет выполняться и как процессор проведет оптимизацию.



Барьеры памяти



Помимо оптимизации процессора, существует ещё оптимизация компилятора и среды выполнения. В контексте данной статьи под компилятором и средой я буду понимать JIT и CLR. Среда может кэшировать значения в регистрах процессора, переупорядочить операции и объединять операции записи (coalesce writes).


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


Иногда бывает нужно, чтобы весь код был точно выполнен до какой-либо определенной инструкции, и для этого используются барьеры памяти. Барьер памяти это инструкция, которая реализуется процессором. В .NET она доступна с помощью вызова Thread.MemoryBarrier(). И что же он делает? Вызов этого метода гарантирует, что все операции перед этим методом точно завершились.


Давайте снова вернемся к программе с двумя переменными. Предположим, что эти процессоры выполняют команды сохранить A и загрузить B. Если между этими двумя командами находится барьер памяти, то процессор сначала отправит запрос сохранить B, подождет, пока она не выполнится. Барьеры памяти дают возможность поставить что-то вроде чекпоинта или коммита, как в базе данных.



Есть еще одна причина использовать барьеры памяти. Если вы пишете код на 32-битной машине с использованием long, DateTime или struct, то атомарность выполнения операций может нарушаться. Это значит, что даже когда в коде записана одна инструкция, на самом деле могут произойти две операции вместо одной, причем они могут выполняться в разное время.



Атомарные операции


Возникает вопрос: что же делать со всем этим беспорядком и неопределенностью? Как синхронизировать ядра? На помощь приходит System.Threading.Interlocked, и это единственный механизм в .NET, предоставляющий доступ к атомарным операциям и на аппаратном уровне.


Когда мы говорим, что операция атомарна, мы имеем в виду, что она не может быть прервана. Это означает, что во время выполнения операции не может произойти переключение потока, операция не может частично завершиться. О разнице между атомарностью, эксклюзивностью и изменением порядка выполнения вы можете узнать из доклада Саши Гольдштейшна Модели памяти C++ и CLR. Более подробно об атомарности рассказывал Карлен szKarlen Симонян в докладе Атомарные операции и примитивы в .NET.

Interlocked содержит такие операции, как инкрементирование (Increment), обмен (Exchange) и обмен через сравнение (CompareExchange).


Я собираюсь разобрать не очень часто используемую операцию CompareExchange, также известную как CAS. Она изменяет значение поля на новое только в том случае, если текущее поле равно какому-либо определенному значению. Эта операция необходима для всех параллельных структур.


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



Не стоит забывать про стоимость выполнения Interlocked-операций. Стоимость инкрементирования в кэше L1 примерно равна удвоенной задержке кэша L1. Вполне быстро! Но стоимость использования Interlocked-инкремента составляет уже целых 5,5 наносекунд, даже если это происходит в единственном потоке. Этот показатель близок к показателю задержки кэша L3. А если у вас два потока обращаются к одной кэш-линии, то стоимость удваивается: ядрам приходится ждать друг друга.



Проанализируем случай, когда двум ядрам приходится ждать друг друга. Для этого используем Intel Vtune Amplifier профайлер со счетчиками производительности процессора. Он подсчитывает частоту цикла процессора и частоту процессора, нужную для выполнения одной инструкции. На картинке ниже показатель подсвечен красным. Профайлер выдает дополнительную информацию, почему соответствующее значение это плохо. Если несколько ядер обращаются к одной кэш-линии, то возникнут проблемы с производительностью. Это особенно критично для приложений, которые обрабатывают большое количество транзакций в секунду.



Многозадачность


На уровне процессора нет таких понятий, как поток и процесс: всё, что мы называем многопоточностью, предоставляет операционная система. А Wait(), на самом деле, не может заставить процессор ждать. Эта команда переводит его в режим пониженного энергопотребления (lower energy state).


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


Для процессора существует только понятие задачи (Task). Вместо потоков есть сегмент состояния задачи, который позволяет сменить таск. Состояние процессора означает состояние доступа к регистрам и маппинга памяти.



Можно использовать compareExchange на примере очень простой реализации ConcurrentStack.


Весь код (финальная версия):


internal sealed class MyConcurrentStack<T>{   private volatile Node head;   public void Push(T value)   {       SpinWait wait = new SpinWait();       Node node = new Node {Value = value};       for ( ;; )       {           Node localHead = this.head;           node.Next = localHead;           if (Interlocked.CompareExchange(ref this.head, node, localHead)                == localHead)               return;           wait.SpinOnce();       }   }   public bool TryPop(out T value)   {       SpinWait wait = new SpinWait();       for ( ;; )       {           Node localHead = this.head;           if (localHead == null )           {               value = default(T);               return false;           }           if (Interlocked.CompareExchange(ref this.head,               localHead.Next, localHead) == localHead )           {               value = localHead.Value;               return true;           }           wait.SpinOnce();       }   }   #region Nested type: Node   private sealed class Node   {       public Node Next;       public T Value;   }   #endregion}

Класс Node (внутри класса MyConcurrentStack<T>) хранит значение и содержит ссылку на следующий элемент.


Давайте сначала посмотрим на неблокирующую реализацию стека:


private sealed class Node{   public Node Next;   public T Value;}

Посмотрим на неблокирующую реализацию стека, здесь мы не используем ключевое слово lock и wait-операции:


private volatile Node head;public void Push(T value){// первым делом создаем элемент стека, тут все просто   Node node = new Node {Value = value};   for ( ;; )   {// записываем ссылку на текущую верхушку стека в локальную//переменную       Node localHead = this.head;// для нового элемента указываем ссылку на следующий элемент,// которым будет являться текущая вершина стека       node.Next = localHead;// меняем верхушку стека (this.head) на новый элемент (node),// если верхушка стека уже не была изменена       if (Interlocked.CompareExchange(           ref this.head, node, localHead ) == localHead )           return;   }}

Зачем здесь нужен условный оператор? Может случиться так, что два потока пытаются запушить новое значение в стек. Эти два потока видят одно и то же поле head. Первый поток начал вставлять новое значение до того, как это начал делать второй поток. После того как первый поток вставил значение, актуальная ссылка на headесть только у этого потока. У второго потока будет ссылка на элемент, который теперь считается следующим после head, то есть head.Next. Такая ситуация показывает, насколько важно бывает иметь такие атомарные операции, как CompareExchange.


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


public bool TryPop(out T value)   {       for ( ;; )       {           Node localHead = this.head;           if (localHead == null)           {               value = default(T);               return false;           }           if (Interlocked.CompareExchange(ref this.head, localHead.Next, localHead)               == localHead )           {               value = localHead.Value;               return true;           }       }   }

Берем head и заменяем её следующим узлом, если, конечно, она уже не была изменена.


В время теста MyConcurentStack участвовало два ядра. Одно ядро выполняло операцию Push(), другое операцию Pop(), ожидание отсутствовало в течение 6 миллионов операций по обмену сообщениями между двумя ядрами.


У этой структуры данных есть два недостатка:


  1. необходимость очистки
  2. необходимость выделения памяти для элементов коллекции

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


В результате все работает гораздо быстрее: 9 миллионов транзакций в секунду.


public class TestMyConcurrentStack : TestCollectionBase{   private readonly MyConcurrentStack<int> stack =        new MyConcurrentStack<int>();   protected override void AddItems(int count)   {       for (int i = 0; i < count; i++)       {           this.stack.Push(i);       }   }   protected override void ConsumeItems(int count)   {       SpinWait spinWait = new SpinWait();       int value;       for (int i = 0; i < count; )       {           if (this.stack.TryPop(out value))           {               i++;               spinWait.Reset();           }           else           {               spinWait.SpinOnce();           }       }   }}


Операционная система (ядро Windows)



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



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


Давайте докажу это. На этом компьютере у меня четыре ядра с гипертредингом, то есть восемь логических процессоров. Всего в системе 2384 потока.Так как логических процессоров всего 8, то получается, что все 2376 потоков в данный момент ожидают, пока до них дойдет очередь выполнения. В 99,9% случаев основное занятие потоков это ожидание.



Одна из функций ядра Windows состоит в том, чтобы заставлять потоки ждать. У внутреннего ядра Windows есть граф зависимостей между потоками и объектами-диспетчерами (dispatcher objects) Среди этих объектов могут быть таймеры, объекты, семафоры и события.


В какой-то момент некоторые зависимости исчезают, и соответствующие потоки переходят из состояния ожидания (wait) в состояние готовности (ready). Потоки в состоянии готовности отправляются в очередь ожидания, а после этого они уходят на разные ядра и начинают выполняться.


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


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


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



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


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



Посмотрите на график, посмотрите на стоимость инкрементирования или доступа к кэшу L1, а затем на стоимость присваивания объекта диспетчера ядра. Две наносекунды и 295 наносекунд огромная разница! А создание объекта диспетчера вообще занимает 2093 наносекунды.


При разработке важно писать код, который не будут создавать объекты диспетчера лишний раз, иначе можно ожидать большую потерю производительности. На уровне .NET у нас есть Thread.Sleep, который использует внутренний таймер. Thread.Yield возвращает выполняющийся поток обратно в очередь ожидания потоков. Thread.Join блокирует вызывающий поток. Но сейчас я хочу более подробно рассказать про Thread.SpinWait.



SpinWait


Представьте, что у вас есть у вас есть два процессора, и на нулевом вы выполняете присваивание A = 1, потом устанавливаете барьер памяти, а затем снова выполняете присваивание B = 1.


На первом процессоре вы получаете А, равное 1, а затем вы ждете, пока B не присвоят значение 1. Казалось бы, такие операции должны быстро выполниться и в кэше L1, и даже в кэше L3. Здесь загвоздка в том, что нулевой процессор может превентивно прерваться, и между операциями из строк 1 и 3 может пройти несколько миллисекунд. SpinWait() способен решать такие проблемы.



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


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


Монитор и ключевое слово lock


Теперь давайте вернемся к ключевому слову lock. Скорее всего, вы знаете, что это просто синтаксический сахар для try/catch Monitor.Enter/Monitor.Exit.


  1. Выполняется Interlocked-операция: каждый объект в .NET имеет хедер, который говорит, на каком потоке он находится и для чего он заблокирован.


  2. Идёт SpinWait, если операция завершится неудачей.


  3. CRL создает kernel event, если выполнение SpinWait не дало ожидаемого результата. После нескольких миллисекунд ожидания создается еще один kernel event, но только в другом потоке.



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


Структуры данных из пространства имен System.Collections.Concurrent


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


Concurrent Stack


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



Concurrent Queue


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



Concurrent Bag


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



Заключение


  1. На аппаратном уровне есть только операции Interlocked и барьеры памяти.
  2. На уровне ядра Windows вводится такая абстракция, как поток. Основное предназначение потока это ожидание и хранение набора зависимостей.
  3. Также мы рассмотрели некоторые коллекции из стандартной библиотеки.

Если доклад показался вам достаточно хардкорным, загляните на сайт конференции Dotnext 2021 Piter, которая пройдёт с 20 по 23 апреля. Основными темами станут настоящее и будущее платформы .NET, оптимизация производительности, внутреннее устройство платформ, архитектура и паттерны проектирования, нетривиальные задачи и best practices. На сайте начинают появляться первые доклады, со временем список будет пополняться.
Подробнее..

Обзор программы JPoint 2021 воркшопы, Spring, игра вдолгую

24.03.2021 18:04:11 | Автор: admin


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


  • Пришла весна, то есть самое время поговорить о Spring. О нём будет четыре доклада, в том числе большое двухчастное выступление Евгения Борисова. Для него мы даже продлили JPoint на пятый день получился специальный день Борисова :)
  • Онлайн-формату подходят воркшопы. Поэтому в отдельных случаях можно будет не просто любоваться слайдами: спикер будет выполнять конкретные задачи на практике, объясняя всё происходящее и отвечая на вопросы зрителей.
  • Есть доклады не строго про Java, а про то, как успешно разрабатывать на длинной дистанции (чтобы всё радовало не только на стадии прототипа, а годы спустя): как делать проекты поддерживаемыми, не плодить велосипеды, работать с легаси.
  • Ну и никуда не девается привычное. Знакомые темы: что у Java внутри, тулинг/фреймворки, языковые фичи, JVM-языки. Спикеры, посвятившие теме годы жизни: от технического лида Project Loom Рона Пресслера до главного Spring-адвоката Джоша Лонга. Возможность как следует расспросить спикера после доклада. И уточки для отладки методом утёнка!

Оглавление


Воркшопы
VM/Runtime
Тулинг и фреймворки
Spring
JVM-языки
Люби свою IDE
Жизнь после прототипа




Воркшопы


Воркшоп: Парное программирование, Андрей Солнцев, Антон Кекс


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


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




Воркшоп: Строим Бомбермена с RSocket, Олег Докука, Сергей Целовальников


Олег Докука и Сергей Целовальников на небольшом игровом примере продемонстрируют практический опыт использования RSocket-Java и RSocket-JS.


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




VM/Runtime


CRIU and Java opportunities and challenges, Christine H Flood


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


О том, как использовать Checkpoint Restore в Java, расскажет Кристин Флад из Red Hat, которая работает над языками и рантаймами уже более двадцати лет.




Real World JFR: Experiences building and deploying a continuous profiler at scale, Jean-Philippe Bempel


JDK Flight Recorder позволяет профилировать непрерывно и прямо в продакшне. Но это же не может быть бесплатно, да? Важно понимать, чем придётся пожертвовать и какие будут накладные расходы.


Разобраться в этом поможет Жан-Филипп Бемпель он принимал непосредственное участие в реализации непрерывной профилировки в JFR.




GC optimizations you never knew existed, Igor Henrique Nicacio Braga, Jonathan Oommen


Какой JPoint без докладов про сборщики мусора! Тут выступление для тех, кто уже что-то знает по теме объяснять совсем азы не станут. Но и загружать суперхардкором с первой минуты тоже не станут. Сначала будет подготовительная часть, где Игор Брага и Джонатан Оммен рассмотрят два подхода к GC в виртуальной машине OpenJ9: balanced и gencon.


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




Adding generational support to Shenandoah GC, Kelvin Nilsen


И ещё о сборке мусора. На JPoint 2018 о Shenandoah GC рассказывал Алексей Шипилёв (Red Hat), а теперь доклад от совсем другого спикера Келвина Нилсена из Amazon, где тоже работают над этим сборщиком мусора.


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




Производительность: Нюансы против очевидностей, Сергей Цыпанов


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




Why user-mode threads are (often) the right answer, Ron Pressler


Многопоточное программирование в Java поддерживается с версии 1.0, но за 25 лет в этой части языка почти ничего не поменялось, а вот требования выросли. Серверам требуется работать с сотнями тысяч, и даже миллионами потоков, а стандартное решение в JVM на тредах операционной системы не может так масштабироваться, поэтому Project Loom это одна из самых долгожданных фич языка.


Ранее у нас уже был доклад про Loom от Алана Бейтмана (мы делали расшифровку для Хабра), а теперь и technical lead этого проекта Рон Пресслер рассмотрит разные решения для работы с многопоточностью и подход, который используется в Loom.




Тулинг и фреймворки


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


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


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




Jakarta EE 9 and beyond, Ivar Grimstad, Tanja Obradovi


Jakarta EE 9 несет множество изменений, которые затронут большое количество библиотек и фреймворков для Java. Чтобы понять, как эти изменения отразятся на ваших проектах, приходите на доклад Ивана Гримстада и Тани Обрадович.


Ивар Jakarta EE Developer Advocate, а Таня Jakarta EE Program Manager, поэтому вы узнаете о самых важных изменениях и планах на будущее из первых рук.




Чтения из Cassandra внутреннее устройство и производительность, Дмитрий Константинов


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


Об этом расскажет системный архитектор и практикующий разработчик из NetCracker Дмитрий Константинов.




The DGS framework by Netflix GraphQL for Spring Boot made easy, Paul Bakker


В Netflix разработали DGS Framework для работы с GraphQL. Он работает поверх graphql-java и позволяет работать с GraphQL, используя привычные модели Spring Boot. И, что приятно, он опенсорсный, стабильный и готов к использованию в продакшне.


Пол Баккер один из авторов DGS. Он расскажет и про GraphQL, и про то, как работать с DGS, и про то, как это используется в Netflix.




Качественный код в тестах не просто приятный бонус, Sebastian Daschner


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


Предыдущие доклады Себастьяна на наших конференциях были англоязычными, но участвуя в российских мероприятиях, он так влился в местное Java-сообщество, что теперь попробует провести доклад на русском!




Why you should upgrade your Java for containers, Ben Evans


Статистика от New Relic говорит, что примерно 62% Java на продакшне в 2021 запущено в контейнерах. Но в большинстве из этих случаев до сих пор используют Java 8 а эта версия подходит для контейнеризации не лучшим образом. Почему? Бен Эванс рассмотрит, в чём проблемы с ней, что улучшилось с Java 11, и как измерить эффективность и расходы.


Хотя в основном речь пойдёт о проблемах, актуальных именно в случае с контейнерами, часть сказанного будет применима и к Java-разработке в целом.




Разошлись как в море корабли: Кафка без Zookeeper, Виктор Гамов


Совсем скоро придет тот день, о котором грезили Kafka-опсы и Apache Kafka больше не будет нуждаться в ZooKeeper! С KIP-500 в Kafka будет доступен свой встроенный механизм консенсуса (на основе алгоритма Raft), полностью удалив зависимость от ZooKeeper. Начиная с Apache Kafka 2.8.0. вы сможете получить доступ к новому коду и разрабатывать свои приложения для Kafka без ZooKeeper.


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




Spring


Spring Data Рostроитель (Spark it!), Евгений Борисов


Товарищ, знай! Чтоб использовать Spark,
Scala тебе не друг и не враг.
Впрочем, и Spark ты можешь не знать,
Spring-data-spark-starter лишь надо создать!


Этот доклад не про Spark и не про Big Data. Его скорее можно отнести к серии потрошителей и построителей. Что будем строить и параллельно потрошить сегодня? Spring Data. Она незаметно просочилась в большинство проектов, подкупая своей простотой и удобным стандартом, который избавляет нас от необходимости каждый раз изучать новый синтаксис и подходы разных механизмов работы с данными.


Хотите разобраться, как Spring Data творит свою магию? Давайте попробуем написать свой аналог. Для чего ещё не написана Spring Data? JPA, Mongo, Cassandra, Elastic, Neo4j и остальные популярные движки уже имеют свой стартер для Spring Data, а вот Spark, как-то забыли. Давайте заодно исправим эту несправедливость. Не факт, что получится что-то полезное, но как работает Spring Data мы точно поймём.




Spring Cloud в эру Kubernetes, Алексей Нестеров


Когда-то давно, много JavaScript-фреймворков назад, когда микросервисы еще были монолитами, в мире существовало много разных инструментов для разработки Cloud Native приложений. Spring Cloud был одним из главных в реалиях Spring и объединял в себе целый набор полезных проектов от Netflix, команды Spring и многих других вендоров.


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




Reactive Spring, Josh Long


Джош Лонг расскажет про фичи Spring Framework 5.0 для реактивного программирования: Spring WebFlux, Spring Data Kay, Spring Security 5.0, Spring Boot 2.0, Spring Cloud Finchley и это только часть!


Может показаться многовато для одного доклада, но мы-то знаем, что Джош Spring Developer Advocate с 2010 года. Уж кто-кто, а он-то знает, как рассказать всё быстро и по делу.




Inner loop development with Spring Boot on Kubernetes, David Syer


Мы живем во время облачных технологий и чтобы эффективнее перейти от принципа works on my machine к works on my/dev cluster нужен набор инструментов для автоматизация загрузки кода на лету.
Доклад Дэвида Сайера будет про то, как и с помощью каких инструментов Spring Boot и Kubernetes построить этот процесс удобно.
Ускорение первой фазы доставки это тот DevOps, который нужен разработчикам, поэтому всем, кто живет в k8s или хотя бы делает системы из нескольких компонентов этот доклад пригодится.




Люби свою IDE


IntelliJ productivity tips The secrets of the fastest developers on Earth, Victor Rentea


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


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




Многоступенчатые рефакторинги в IntelliJ IDEA, Анна Козлова


В IntelliJ IDEA есть ограниченное количество основных рефакторингов: Rename, Move, Inline, Extract. Пользователи часто просят добавить еще, но чаще всего это можно сделать комбинацией уже существующих, просто это не всегда очевидно.


На JPoint 2021 вы сможете получить практические рекомендации по рефакторингу от человека, который разрабатывает рефакторинги: о самых важных приемах расскажет коммитер 1 в IntelliJ IDEA Community Edition Анна Козлова.




С какими языками дружат IDE?, Петр Громов


Разработчики всё чаще выбирают языки с выразительным и кратким синтаксисом, совсем не задумываясь на тем, как долго их код будет компилироваться, и сможет ли IDE помочь им с рефакторингом. В докладе Петра Громова будут интересные идеи о сложности современных языков программирования с точки зрения разработчиков IDE.


Рекомендуем всем, кому интересны механизмы IDE, языки, парсеры, DSL и сложные синтаксические конструкции в современных языках программирования.




Java и JVM-языки


Type inference: Friend or foe?, Venkat Subramaniam


Не все могут объяснять так, как это делает Венкат Субраманиам, поэтому мы любим приглашать его на конференции.


На JPoint 2021 он выступит с докладом про type inference. Хотя тема сама по себе не новая, нюансов в ней хватает, а развитие языков делает её лишь более актуальной (вспоминается доклад Романа Елизарова с TechTrain, где он рассматривал, как ситуация с типами и их выводом менялась со временем). Так что стоит лучше понять, в чём вывод типов помогает, а в чём мешает для этого и рекомендуем сходить на этот доклад.




Babashka: A native Clojure interpreter for scripting, Michiel Borkent


Babashka интерпретатор Clojure для скриптов. Он мгновенно запускается, делая Clojure актуальной заменой для bash. У Babashka из коробки есть набор полезных библиотек, дающих доступ из командной строки к большому количеству фич Clojure и JVM. Сам интерпретатор написан на Clojure и скомпилирован с помощью GraalVM Native Image. В докладе работу с ним наглядно покажут с помощью демо.




Getting the most from modern Java, Simon Ritter


Недавно вышла JDK 16, и это значит, что мы получили 8 (прописью: ВОСЕМЬ) версий Java менее чем за четыре года. Разработчики теперь получают фичи быстрее, чем когда-либо в истории языка.


Так что теперь попросту уследить бы за всем происходящим. Если вы до сих пор сидите на Java 8, на что из появившегося позже стоит обратить внимание и чем это вам будет полезно? В этом поможет доклад Саймона Риттера, где он поговорит о некоторых нововведениях JDK 12-15 и о том, когда их исследовать, а когда нет:


  • Switch expressions (JDK 12);
  • Text blocks (JDK 13);
  • Records (JDK 14);
  • Pattern matching for instanceof (JDK 14);
  • Sealed classes and changes to Records (JDK 15).


Про Scala 3, Олег Нижников


Обзор языка Scala 3 и грядущей работы по переходу. Обсудим, в какую сторону двигается язык, откуда черпает вдохновение, и пройдёмся по фичам.




Java Records for the intrigued, Piotr Przybyl


В Java 14 появились в превью-статусе Records, а с Java 16 они стали стандартной фичей. Для многих это было поводом сказать что-то вроде Lombok мёртв или не нужна больше кодогенерация JavaBeans. Так ли это на самом деле? Что можно сделать с помощью Records, а чего нельзя? Что насчёт рефлексии и сериализации? Разберём в этом докладе.




Жизнь после прототипа


Восстанавливаем утраченную экспертизу по сервису, Анна Абрамова


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


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




Что такое Работающий Продукт и как его делать, Антон Кекс


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


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




Enum в API коварство иллюзорной простоты, Илья Сазонов и Фёдор Сазонов


Вы уверены, что если добавить один маленький enum в API, то ничего страшного не произойдет? Или наоборот уверены, что так делать не стоит, но никто вас не слушает?
Рекомендуем вам доклад Ильи и Федора Сазоновых, пропитанный тяжелой болью по поводу бесконечных обновлений контрактов микросервисов.
Обычно подобные темы не выходят за пределы локального холивара в курилке, но нельзя же вечно добавлять новые значения в enum?




Dismantling technical debt and hubris, Shelley Lambert


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




Подводя итог


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


Напоминаем, поучаствовать во всём этом можно будет с 13 по 17 апреля в онлайне. Вся дополнительная информация и билеты на сайте.

Подробнее..

Введение в транзакционную память от Мориса Херлихи

19.05.2021 12:19:31 | Автор: admin

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

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

Содержание

Закон Мура

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

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

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

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

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

После изобретения многопоточности появились многопроцессорные системы с разделяемой памятью (shared-memory multiprocessor):

Сейчас это тоже вымирающий вид.

А сегодня популярны многоядерные процессоры, иногда их называют CMP (chip multiprocessor). И это означает, что кэш процессора (по крайней мере, часть памяти, коммуникационная среда), размещается целиком на одном кусочке кремния (кристалла).

Так в реальности выглядит довольно небольшой старомодный многоядерный процессор:

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

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

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

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

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

Но, конечно, жизнь не так проста.

В реальности так не происходит по ряду причин. И на самом деле всё выглядит примерно так:

Мы пишем программы, делаем их параллельными и многопоточными, добавляем потоки (threads), блокировки (locks), синхронизацию. И хотя железо выглядит всё более мощным и параллельным, ПО не может за ним поспеть. Блокировки вызывают борьбу за ресурсы и разные другие проблемы.

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

Закон Амдала

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

Определим ускорение программы от параллелизма как соотношение количества времени, которое потребуется для её исполнения на одном процессоре, и времени, за которое та же самая программа будет выполняться на n-поточной машине:

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

В итоге закон Амдала выглядит так:

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

А если p равно 1, то код полностью параллельный. И сколько ядер будет, во столько раз он и ускорится.

Но это крайние случаи, а в большинстве интересующих нас приложений p находится где-то между 0 и 1.

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

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

Если отвлечься от математики, моё любимое объяснение значимости закона Амдала выглядит так:

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

Пример

Представим, что вы покупаете 10-ядерную машину. А ваше приложение на 60% параллельное и на 40% последовательное. Иными словами, 60% вашего приложения могут быть распределены между разными ядрами, а 40% приходятся на части, которые нельзя так ускорить скажем, на UI и обращение к диску.

Когда вы сказали начальнику, что хотите купить 10-ядерный компьютер, он сказал: Надеюсь, мы получим десятикратное ускорение нашего приложения. Насколько это возможно с 60% параллельности и 40% последовательности? Какое ускорение вы получите по закону Амдала?

Ответ: вы получите ускорение в 2.17 раз. Итак, у вас стало 10 процессоров, но ускорилось всё только в два раза. Как-то не очень воодушевляет. Поэтому вы попросили своих программистов задерживаться после работы и работать без выходных, чтобы сократить последовательную часть и увеличить многопоточную.

Была проделана большая работа, и в программе стало 80% многопоточности. Вы запускаете программу, чтобы посмотреть, насколько вы приблизились к десятикратному ускорению. И теперь это 3,57. Итак, вы очень усердно работали, ваш код многопоточен на 80%, но вы получили ускорение всего лишь в 3,5 раза. Опять же, мы живём в очень суровом мире.

Вы снова принимаетесь за работу, работаете ещё усерднее и добиваетесь 90% многопоточности в своём коде. И получаете ускорение в 5,26 раза. То есть у вас 10 процессоров, но вы получаете эффект, как от пяти.

Если вы хотите получить ускорение, которое хоть немного похоже на рост числа процессоров у вашего железа, понадобится сделать своё приложение на 99% параллельным и на 1% последовательным. Так вы ускоритесь в 9.17 раз и приблизитесь-таки к десятикратному ускорению.

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

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

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

Варианты блокировок

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

Грубая блокировка (coarse-grained locking)

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

Однако у него очень плохая производительность.

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

Возникает вопрос: Если у нас сложная структура данных, почему бы не блокировать только те её части, которые нужны в текущий момент?

Мелкоструктурная блокировка (fine-grained locking)

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

Однако здесь свой недостаток: бывает сложно написать структуру данных, используя мелкоструктурную блокировку. Можно писать целые исследования о том, как это делать. А если о чём-то можно писать исследования, значит, это не самая удачная инженерная техника!

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

Проблемы блокировок

Блокировки не отказоустойчивы

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

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

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

Блокировки конвенциональны

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

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

When a locked buffer is visible to the I/O layer BH_Launder is set. This means before unlocking we must clear BH_Launder,mb() on alpha and then clear BH_Lock, so no reader can see BH_Launder set on an unlocked buffer and then risk to deadlock.

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

Блокировки усложняют простые задачи

Эту проблему трудно определить формально, но при встрече с ней сразу всё понимаешь.

Приведу пример простой задачи, которую сможет понять и старшеклассник. Допустим, у нас есть двухсторонняя очередь (double-ended queue): элементы можно добавлять и убирать с обеих сторон. Поэтому один поток может добавлять элемент с одной стороны, в то время как другой добавляет с противоположной. Ваша задача обеспечить здесь синхронизацию на основе блокировки (lock-based synchronization).

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

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

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

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

Блокировки не компонуются

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

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

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

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

Но давайте подумаем, каковы последствия такого решения для структуры ПО.

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

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

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

Паттерн Monitor wait and signal не компонуется

Ещё одна распространённая проблема связана с паттерном Monitor wait and signal. У нас есть поток, который ожидает задание, например, от буфера сообщений. Буфер пуст, так что процесс говорит: Мне нет смысла тратить ресурсы, когда делать нечего. Я буду спать, а ты сообщи мне, когда что-нибудь появится. Когда в буфере появляется элемент, мы можем разбудить поток, и он начнёт потреблять данные.

Меня беспокоит, что этот паттерн не компонуется.

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

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

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

Транзакционный манифест

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

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

Общий план таков:

  • Заменим блокировки на атомарные транзакции

  • Поговорим о разработке языков и библиотек

  • Внедрим эффективные реализации

Но в этом докладе мне не хватит времени на все эти темы. Придётся сконцентрироваться на более узкой повестке:

  • Транзакции против блокировок

  • Аппаратная транзакционная память

  • Использование транзакционной памяти

  • Объединение транзакций с блокировками

  • Открытые для исследования вопросы

Транзакции против блокировок

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

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

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

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

Атомарные блоки

Перед нами обычный пример атомарного блока:

atomic {    x.remove(3);    y.add(3);}atomic {    y = null;}

Представьте, что у нас есть два потока, один удаляет 3 из x и добавляет 3 к y. Скажем, что мы изобрели ключевое слово atomic, и код с ним исполняется атомарно. Тогда здесь у нас простой пример того, как переместить что-то из одного контейнера в другой. Это один из тех примеров, которые, оказывается, сложно реализовать чисто с использованием блокировок.

Ещё здесь у нас есть параллельный поток, который по сути устанавливает y равным нулю.

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

Двухсторонняя очередь

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

Первый шаг это написать последовательный код.

public void LeftEnq(item x) {    Qnode q = new Qnode(x);    q.left = left;    left.right = q;    left = q;}

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

Второй шаг заключить этот код в атомарный блок:

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

Предупреждение

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

  • Ожидание условия (conditional wait). Помните наш поток, который был заблокирован, заглянул в буфер и сказал: Здесь ничего нет, я снимаю блокировку? Требуется объяснить, как это будет работать с транзакциями.

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

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

Тем не менее, я продолжаю утверждать, что с транзакциями жизнь проще: ваш код будет проще писать, и он с большей вероятностью окажется корректным. Лучше уж упомянутые проблемы (хотя некоторые из них вполне серьёзные), чем те, что возникают с блокировками. Решить проблемы с транзакциями проще. Транзакции не волшебные, они не избавят вас от проблем, но с ними вы будете лучше спать ночью. Я обещаю.

Компоновка?

Помните наш пример с компоновкой, где мы хотели совершить атомарное перемещение элемента из одной очереди в другую?

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

public void Transfer(Queue<T> q1, q2){  atomic {    T x = q1.deq();    q2.enq(x);  }}

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

Ожидание условия

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

public T LeftDeq() {  atomic {    if (left == null)      retry;      }}

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

То есть здесь тот же результат, что и signal and wait в мониторе, но это гораздо проще использовать и, я думаю, это более понятно интуитивно. Ожидание монитора таит множество опасностей и ловушек, а retry поможет их избегать.

Компонируемое ожидание условие

Если вы хотите скомпоновать условное ожидание, можно ввести конструкцию orElse.

atomic {  x = q1.deq();} orElse {  x = q2.deq();}

Сначала я запущу свой первый метод qt.deq(), если там будет retry, он вернётся и скажет: Извините, я ничего не могу здесь сделать. Тогда вы можете вернуться и попробовать второй, q2.deq(). Если у обоих будет retry, тогда retry будет у всей конструкции она засыпает, откатывает свои результаты, ждёт изменений и затем заново запускает код. То есть снаружи это выглядит так, будто каждый раз, когда вы делаете удаление из очереди, там что-то есть.

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

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

Аппаратная транзакционная память

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

Есть два вида процессоров, которые поддерживают транзакционную память. В основном я буду говорить об Intel там, начиная с Haswell, используются расширения TSX (Transactional Synchronization Extensions). Есть и другой вид аппаратного обеспечения транзакционной памяти в семействе процессоров Power. Intel больше ориентирован на мелкоструктурную параллельность, а Power на численные вычисления.

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

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

Стандартная когерентность кэша

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

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

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

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

Аппаратная транзакционная память

Аппаратная транзакционная память работает почти по такому же принципу. Один процессор начинает считывать данные и помечает их в кэше: Я считал это от имени транзакции.

Другой процессор также помечает их как транзакционные, теперь у обоих они так помечены.

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

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

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

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

Использование транзакционной памяти

Пояснение о железе потребовалось, потому что оно объясняет некоторые странные вещи, которые происходят, когда вы пишете программы. Как я уже говорил, в процессорах Blue Gene/Q, в System Z и Power8 есть форма транзакционной памяти. Сейчас я буду говорить об Intel, потому что у меня есть время поговорить только о чём-то одном.

На TSX Intel вот так вы пишете транзакцию:

if (_xbegin() == _XBEGIN_STARTED) {  speculative code  _xend()} else {  abort handler}

Это код на C. Вы начинаете с того, что говорите, _xbegin() это системный вызов, который говорит: Я хочу начать спекулятивную транзакцию. Это немного похоже на fork() или vfork() в Unix, он возвращает код. И если вы видите в коде _XBEGIN_STARTED), это значит, что вы внутри транзакции. Если это происходит, вы выполняете свой спекулятивный код.

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

Код обычно выглядит примерно так:

if (_xbegin() == _XBEGIN_STARTED) {  speculative code} else if (status & _XABORT_EXPLICIT) {  aborted by user code} else if (status & _XABORT_CONFLICT) {  read-write conflict} else if (status & _XABORT_CAPACITY) {  cache overflow} else {  }

Какие варианты тут есть?

_XABORT_EXPLICIT: я могу вызвать _xabort(). Мне что-то не понравилось, так что я решил прервать свою транзакцию и откатиться обратно, например, некоторое ожидание условия.

_XABORT-CONFLICT: может быть, у меня с кем-то конфликт синхронизации, если такое происходит, возможно, мне стоит попробовать снова позже.

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

И есть ещё много разных кодов прерывания.

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

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

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

  • Транзакция просто не в настроении. Существует множество других причин прерывания транзакции: промах TLB, исполнение неразрешённой команды, отказ страницы. Это происходит крайне редко, но это может произойти.

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

Гибридная транзакционная память

Здесь мы объединим транзакционный подход с блокировками. Вот пример такой комбинации.

if (_xbegin() == _XBEGIN_STARTED) {  read lock state  if (lock taken) _xabort();  work;  _xend()} else {  lock->lock();  work;  lock->unlock();}

Идея следующая: я начинаю транзакцию, у меня будет блокировка, я прочитаю её состояние (read lock state). И как только я прочитаю его, это гарантирует, что если другой поток блокирует что-то, это прервёт мою транзакцию (if (lock taken) _xabort();).

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

Пропуск блокировки

Есть паттерн пропуск блокировки (Lock Elision), который поддерживается на аппаратном обеспечении Intel.

<HLF acquire prefix> lock();do work;<HLE release prefix> unlock()

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

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

Телепортация блокировки

Есть забавное использование транзакционной памяти, которую я называю lock teleportation.

Идея тут в следующем. Есть такой общий паттерн в структурах данных, который называется передающаяся блокировка (hand-over-hand locking). Представьте, я иду вниз по списку, поочередёно устанавливая и снимая блокировку на разных элементах, и я могу следовать этому паттерну по мере движения. Это даёт ограниченное количество параллельности.

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

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

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

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

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

Вы установить ограничение, насколько далеко вы можете продвинуться. И после каждого успешного раза вы говорите: Хорошо, в следующий раз я продвинусь на один дальше. Так что если вы успешно телепортировали блокировку на 5 шагов, в следующий раз вы попробуете 6, а ещё в следующий 7. В какой-то момент вы выйдете за пределы. В таком случае вы устанавливаете ограничение в половину того, что у вас было в последний раз, и готово.

Вопросы для исследования

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

Блокировки

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

Управление памятью

Одна из областей, где транзакции оказались на удивление полезными управление памятью (как ручное, так и автоматическое). Malloc, free, сборка мусора... Если вы занимаетесь управлением памятью, то стремитесь совершать операции одного стиля, и хорошо понятно, какой объём работы вам придётся проделать. Это крайне важно, потому что программы постоянно распределяют и освобождают память. И мне кажется, транзакционная память уже показала, что может быть полезна здесь, но я считаю, что предстоит ещё много работы по улучшению.

БД

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

Энергетическая эффективность

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

Графические процессоры

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

ОС

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

Структуры данных

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

Архитектура компьютера

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

Есть понятие Кривая хайпа (hype curve). Она говорит, что при появлении новой идеи люди говорят: О! Это чудесно! Это решит все проблемы! Это первый верхний выступ. Потом они говорят: Ой, погодите-ка. Появляются всевозможные новые проблемы и задачи, может, это на самом деле плохо. А в итоге люди научаются решать задачи и всё становится нормально.

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

Транзакционные конструкции поддерживаются в C++, Haskell, других языках, они введены в архитектуры Intel и, очевидно, они уже никуда не денутся. Так что транзакции останутся надолго. И нам только нужно понять, как лучше их использовать.

Если заинтересовал этот доклад, обратите внимание на Hydra 2021: там снова выступит Морис Херлихи, а также будет много других заметных людей из мира распределённых и многопоточных систем. Конференция пройдёт с 15 по 18 июня в онлайне, программу и другую информацию можно увидеть на сайте.

Подробнее..

Этот поезд в окне анонс TechTrain 2021 Spring

16.03.2021 14:17:52 | Автор: admin


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


А ещё делимся плейлистом с записями предыдущего TechTrain можете по ним лучше понять, чего ждать от следующего.




Программа


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


Из твёрдого в газообразное: Фазовый переход инженер тимлид менеджер


Евгений Кот bunopus знаком многим по конференции HolyJS. Но его новый доклад не про то, как с JavaScript перейти на Dart, а про вопрос, актуальный для разработчиков на любом языке.


Несмотря на обилие книг, статей и видео, все опрошенные эксперты путаются в показаниях и отвечают расплывчатыми формулировками на вопрос В чём самые главные, принципиальные различия между разными позициями?. И немудрено: в русскоязычном пространстве сложилось многообразие видов. Где-то тимлид это старший/старый инженер. Где-то product owner это естественная фаза развития программиста. А где-то всё плоско, но ничего непонятно.


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




Railway oriented programming


Скотт Влашин известен своим сайтом F# for fun and profit. Раз он посвятил жизнь языку из .NET-экосистемы, неудивительно, что его можно увидеть на .NET-конференциях. Но ему есть что сказать не только о F#, но и о функциональном программировании в целом и тут будет как раз такой случай.


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




Что мобильным разработчикам в IT-индустрии неведомо


Обычно Владимир Иванов dzigoro выступает на темы, близкие Android-разработчикам: например, о корутинах в Kotlin. Но в этот раз всё будет наоборот, и он поговорит о вещах, далёких от этих людей.


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


К сожалению, времени передохнуть и оглядеться что еще есть в индустрии просто не остается. А по оценкам Владимира, это где-то 95% материала!


Мобильным разработчикам редко известно про CAP-теорему, про SLA, шардирование, Kubernetes и мониторинг. А ведь это чертовски интересно!


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




Какие тулы ты бы взял на удалёнку


Российские тестировщики и разработчики могут знать Всеволода Брекелова vbrekelov и Артёма Ерошенко eroshenkoam, например, по их совместному YouTube-шоу Ошибка выжившего. И на TechTrain они тоже выступят совместно.


Чтобы объяснить, о чём речь, дадим слово им самим:


Сева: Артем, что ждет зрителей в этом докладе и кому он полезен?


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


Сева: А есть спойлер, что за инструменты?


Артём: Давай не буду говорить про все, но скажу про такие: ngrok, CodeWithMe, monosnap.


Сева: Неужели не будет про Miro и Allure?


Артём: Будет, безусловно, покажем и их.


Сева: А будет какой-то сюрприз?


Артём: Сева, вот ты все проспойлеришь так. Да, расскажем, что будет с нашим шоу "Ошибка Выжившего".


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




Code with me: Behind the scenes


Этой весной компания JetBrains добавляет своим продуктам принципиально новую функциональность: Code With Me. Это платформа для коллаборативной работы неважно, парное ли это программирование, обучение классов, воркшопы, или mob-программирование.


Какое значение это имеет для рядовых разработчиков и для IT-организаций? Как эта штука устроена внутри? Какая история и какое будущее у Code With Me? Обо всех этих вещах расскажет лид проекта, Кирилл Скрыган. В предыдущий раз он рассказывал у нас о платформенных войнах, и тот доклад оказался отлично принят.




Ловушки коллективного владения кодом


Коллективное владение кодом неотъемлемая часть методологии Agile. Оно широко используется и продвигается в большинстве современных софтверных компаний. Но действительно ли мы знаем, как ее применять? А что делать, чтобы не попасть в ловушку? Итак, давайте рассмотрим самые распространенные практики и обсудим типичные ошибки.


Сделает это Маргарита Недзельская большой фанат Котлина и Kotlin GDE. Среди посетителей конференций кто-то уже знает её и как спикера (например, у нас на JPoint она рассказывала про интероп Scala/Kotlin), и как у организатора (проводила KUG в родном Киеве). На основной работе создает инструменты статического анализа кода для Java/Kotlin/Scala и других языков.




Помимо программы


Программа ещё может пополниться новыми докладами, в таком случае они будут добавлены на сайте. Но теперь отвлечёмся от докладов: как вообще всё будет выглядеть?


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



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


И никуда не делись чаты по по каждому докладу и дискуссионные зоны в Zoom, где можно как следует расспросить каждого спикера.




Подводя итоги


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


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


  • Heisenbug (тестирование): 6-9 апреля
  • JPoint (Java), 13-17 апреля
  • Mobius (мобильная разработка): 13-16 апреля
  • HolyJS (JavaScript): 20-23 апреля
  • DotNext (.NET): 20-23 апреля
  • Hydra (параллельные и распределённые системы): летом, скоро будет анонс

Ждём вас и на TechTrain, и на конференциях!

Подробнее..

JPoint 2021 тенденции и тренды мира Java

19.04.2021 00:19:54 | Автор: admin
В третьем онлайн-сезоне конференций, проводимых JUG Ru Group, с 13 по 17 апреля 2021 года успешно прошла Java-конференция JPoint 2021.



Что было интересного на конференции? Какой тематики были доклады? Кто из спикеров и про что рассказывал? Что изменилось в организации конференции и долго ли ждать возвращение офлайн-формата? Можно ли что-то ещё придумать оригинальное при написании обзора о конференции?

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

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

Картинка в начале статьи с облаком тегов в форме логотипа JPoint была сформирована с использованием названий и описаний абсолютно всех докладов конференции. Посмотреть файл в оригинальном размере можно по следующей ссылке. Из 1685 слов в топ-3 с заметным отрывом попали: Spring (50 повторений), Java (49 повторений) и data (21 повторение). Из прочих фаворитов, но с меньшей частотой использования, можно также отметить слова session, JDK, cloud, code, Kubernetes, GraphQL и threads. Данная информация помогает понять, куда движется Java-платформа и названия каких сущностей, технологий и продуктов являются самыми актуальными сегодня.

Открытие


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



Редкий случай, когда можно было видеть одновременно трёх лидеров Java-сообществ: JUG.ru (Алексей Фёдоров), JUG.MSK (Андрей Когунь) и JUGNsk (Иван Углянский).



Основные события конференции были трёх типов:

  • доклады;
  • мини-доклады партнёров;
  • воркшопы.

Доклады


Приятной неожиданностью для русскоязычных участников конференции стало то, что Себастьян Дашнер свой доклад Качественный код в тестах не просто приятный бонус делал на русском языке. Себастьян принимает участие в качестве спикера в конференциях JUG Ru Group c 2017 года, причём, не только в Java-конференциях. Текущий доклад был посвящён интеграционному тестированию, поэтому в нём присутствовали и Java, и тесты на JUnit, и Docker. В качестве приглашённого эксперта рассказ успешно дополнил Андрей Солнцев. Отличное знание русского языка и неизменно интересный доклад от Себастьяна Дашнера.





В докладе Building scalable microservices for Java using Helidon and Coherence CE от Дмитрия Александрова и Aleksandar Seovic было продемонстрировано совместное использование двух продуктов компании Oracle Helidon (в его разработке участвует Дмитрий) и Oracle Coherence (Aleks является архитектором продукта). Митя ранее делал доклады про MicroProfile (первый доклад и второй) и написал хорошую статью на Хабре про Helidon, поэтому было любопытно посмотреть дальнейшее развитие темы. Повествование сопровождалось демонстрацией кода и запуском приложения, код которого доступен на GitHub. Докладчики, каждый из которых лучше знаком со своим продуктом, отлично дополняли друг друга. Посмотреть оказалось увлекательно и полезно.





Анна Козлова работает над созданием нашего любимого инструмента IntelliJ IDEA, внеся по количеству коммитов самый большой вклад среди всех конрибьютеров в репозиторий IntelliJ IDEA Community Edition, что вызывает огромное уважение.

В своём докладе Многоступенчатые рефакторинги в IntelliJ IDEA Анна очень доходчиво и убедительно показала, как сложные типы рефакторингов могут быть получены комбинацией более простых уже существующих рефакторингов. В препарировании рефакторингов ей ассистировал коллега по компании JetBrains Тагир Валеев. Исключительно полезен как сам доклад (определённо, стоит его пересмотреть), так и озвученная Анной и Тагиром статистика применения разного типа рефакторингов пользователями.





Type inference: Friend or foe? от Venkat Subramaniam. Венкат фантастически харизматичный спикер, которого каждый раз хочется смотреть при присутствии его докладов на конференции. Мне кажется, ценность его докладов в том числе в том, что он заставляет увидеть другую сторону каких-то вещей, ранее казавшимися простыми и очевидными. В этот раз подобной темой было выведение типов (type inference). Кроме интересной информации в очень экспрессивном исполнении наконец-то узнал, в чём Венкат показывает презентации и запускает код (ломал голову над этим при просмотре его предыдущих докладов) это редактор vi.





Доклад Антона Кекса про то, Что такое Работающий Продукт и как его делать своеобразное продолжение его выступления The world needs full-stack craftsmen двухлетней давности. Если в прошлом докладе Антон говорил о недопустимости узкой специализации разработчика, то в этот раз сфокусировал своё внимание и внимание слушателей на том, почему важно и как можно сделать качественный работающий программный продукт. Докладчик подкреплял приведённые теоретические тезисы практическими примерами, поэтому просмотр стал весьма захватывающим зрелищем.





Под доклад Spring Data Рostроитель (Spark it!) в исполнении Евгения Борисова предусмотрительно был отведён отдельный пятый день конференции. Планировалось, что демонстрация написания поддержки Spark для Spring Data займёт 6 часов (в итоге вышло почти 7). Положительным моментом онлайн-конференции является то, что в случае длинных докладов можно комфортно прерывать и продолжать просмотр позднее. Много кода, новой информации и подробных пояснений. Традиционно получилось качественно, основательно и увлекательно.



Мини-доклады партнёров


+10 к безопасности кода на Java за 10 минут стал первым из 15-минутных докладов партнёров, увиденных на конференции. Алексей Бабенко сконцентрировал в небольшом времени, отведённом на доклад, внимание на вопросах безопасности при написании кода на языке Java. Формат мини-докладов, которые показываются в перерывах между большими докладами, оказался достаточно удачным и востребованным.





Ещё один мини-доклад, 1000 и 1 способ сесть на мель в Spring WebFlux при написании высоконагруженного сервиса от Анатолия Тараканова, может пригодиться в том случае, если используете Spring WebFlux и возникли какие-либо проблемы в разработке и эксплуатации приложения. Краткое перечисление проблем и способов их решений может чем-то помочь.





В кратком докладе R2DBC. Стоит ли игра свеч? от Антона Котова даётся оценка практической применимости спецификации R2DBC в текущий момент. После ранее прослушанного вот этого доклада Олега Докуки было интересно узнать сегодняшнее положение вещей. Антон в конце доклада даёт однозначный ответ на вопрос Стоит ли игра свеч?. Через некоторое время ответ должен, вероятно, измениться.





Доклад Секретный ингредиент: Как увеличить базу пользователей в 3 раза за год в исполнении Александра Белокрылова и Алисы Дрожжиновой представил следующие новости от компании BellSoft, наиболее известной своим продуктом Liberica JDK:

  • клиентская база компании увеличилась в 3 раза за последний год;
  • появился новый инструмент Liberica Administration Center (LAC) для централизованного обновления Java на компьютерах пользователей;
  • стала доступна утилита Liberica Native Image Kit на базе GraalVM CE;
  • компания ведёт работы в области серверов приложений (на сайте доступен продукт LiberCat на основе Apache Tomcat).





Кирилл Скрыган докладом Code With Me новая платформа для удаленной коллаборативной разработки представил новую возможность продуктов компании JetBrains для парного программирования и коллективной разработки в среде разработки. Была показана базовая функциональность сервиса и перечислены получаемые преимущества.



Воркшопы


На конференции было два воркшопа: Парное программирование вместе с Андреем Солнцевым и Антоном Кексом и Строим Бомбермена с RSocket вместе с Сергеем Целовальниковым и Олегом Докукой. Для просмотра во время конференции выбор пал на воркшоп Строим Бомбермена с RSocket. Олег и Сергей убедительно продемонстрировали на примере игры взаимодействие составных частей приложения по протоколу RSocket. Код приложения доступен на GitHub для изучения и повторения действий, выполненных во время воркшопа.



Конференции, Java-митапы и игра


На сайте jugspeakers.info по-прежнему доступно приложение, состоящее из двух частей:

  • поиск и просмотр информации о конференциях и Java-митапах, организуемых JUG Ru Group, JUG.MSK и JUGNsk (спикеры, доклады, презентации, видео, статистика);
  • игра Угадай спикера.

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

  1. Начальная страница отображает теперь ближайшую или идущую в данный момент конференцию (картинка слева ниже сделана во время работы JPoint 2021). Установка значений всех фильтров поиска по умолчанию на текущую конференцию также должна помочь сделать программу максимально полезной во время идущей сейчас конференции.
  2. Приложение дополнено фильтрами по организатору конференций и митапов (средняя картинка).
  3. Добавлена информация о видео всех докладов, сделанных публично доступными (в том числе видео докладов с Joker 2020).
  4. Появились данные о конференции SnowOne, второй год проводящейся новосибирским Java-сообществом JUGNsk (см. картинку внизу справа).
  5. Стало возможным видеть учётные записи Хабра у спикеров.



Во второй части приложения (в игре Угадай спикера) появилось 2 новых режима: угадывание облака тегов по спикеру и спикера по облаку тегов. Облака тегов формируются на лету, при выборе пользователем конференций или митапов. Источником для создания облаков тегов по спикерам являются наименования и описания их докладов. Для каждого спикера создаётся одно (по тексту на английском языке) или два облака тегов (второе облако возможно, если у докладов есть описание и на русском языке). Переключением языка интерфейса можно посмотреть оба облака тегов.

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



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

  1. Выбрать только одну конференцию JPoint 2021 (Тип события JPoint, Событие JPoint 2021)
  2. Выбрать все конференции JPoint (Тип события JPoint, Событие конференции за все годы)
  3. Выбрать все Java-события (Тип события Joker, JPoint, SnowOne, JBreak, JUG.MSK, JUG.ru и JUGNsk)

Код приложения находится на GitHub, репозиторию можно ставить звёздочки.

Закрытие


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



Несмотря на вынужденное ограничение онлайн-форматом, конференция продолжает держать высокую планку: удобная платформа для просмотра докладов и взаимодействия с другими участниками, множество докладов по Java-технологиям, горячая информация о новых продуктах (Space и Code With Me), любимые спикеры с отлично дополняющими их приглашёнными экспертами.

В весенне-летнем сезоне онлайн-конференций JUG Ru Group ещё будут конференции HolyJS, DotNext (20-23 апреля 2021 года) и Hydra (15-18 июня 2021 года). Можно посетить любую из конференций отдельно или купить единый билет на все шесть конференций сезона (три уже прошедших и три оставшихся), видео докладов становятся доступными сразу же после завершения конференций.
Подробнее..

Неочевидные сложности CRDT

12.05.2021 16:20:09 | Автор: admin


Мы все так привыкли к облачной синхронизации Dropbox и совместному редактированию в Google Docs, что объединение результатов действий разных пользователей может казаться давно решённой проблемой. Но на самом деле в этом вопросе остаётся множество подводных камней, а работа над алгоритмами CRDT вовсю продолжается.


Один из людей, ведущих эту работу Мартин Клеппманн (Martin Kleppmann): исследователь в Кембриджском университете и создатель популярной библиотеки Automerge. И на конференции Hydra он рассказывал о нескольких вещах, которые исследовал буквально в последнюю пару лет. Какие действия пользователя могут заставить Google Drive выдать unknown error? Почему в CRDT метаданные о работе над документов могут занимать в сто раз больше места, чем сам документ, и как с этим бороться? А у какой проблемы сейчас даже не существует известного решения?


Обо всём этом он поведал в докладе, а теперь мы сделали для Хабра текстовый перевод. Видео и перевод под катом, далее повествование будет от лица спикера.



Содержание доклада



Сегодня я хотел бы рассказать вам о CRDT (Conflict-free Replicated Data Types, бесконфликтные реплицированные типы данных). Вначале мы вкратце выясним, что же это такое и зачем они нам нужны, но в основном будет новый материал на основе исследований, которые мы проводили последний год или два. Возможно, вы читали мою книгу. Но сегодня речь пойдёт о ПО для совместной работы (collaborative software).


ПО для совместной работы


С помощью такого ПО несколько пользователей могут участвовать в редактировании и обновлении общего документа, файла, базы данных или какого-либо другого состояния. Примеров такого ПО много. С помощью Google Docs или Office 365 можно совместно редактировать текстовые документы. Среди графических приложений ту же функциональность даёт Figma. Trello позволяет пользователям давать друг другу задания, комментировать их и тому подобное. Общим у всех этих приложений является то, что в них пользователи могут параллельно обновлять данные.


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


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



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


Алгоритмы для параллельного редактирования документов


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


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


  • Давно существует семейство алгоритмов операционального преобразования (operational transformation, OT), используемых в Google Docs и многих других современных приложениях.
  • А примерно 15 лет назад появился более новый вид: CRDT.

Оба вида алгоритмов решают примерно одну и ту же проблему, но существенно разными способами.


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



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


В какой-то момент пользователи обмениваются изменениями через сервер. Второй пользователь узнаёт, что по индексу 3 необходимо вставить букву l, и получает необходимый результат. Но в случае с первым пользователем всё несколько менее очевидно. Если просто добавить восклицательный знак по индексу 4, то получится Hell!o, поскольку первый пользователь до этого добавил новый символ по индексу 3, и из-за этого индексы всех последующих символов увеличились на единицу. Поэтому индекс 4 необходимо преобразовать в индекс 5. Отсюда название алгоритма операциональное преобразование. И отсюда же главная сложность с ним.


Большинство алгоритмов операционального преобразования исходят из предположения о том, что все взаимодействия выполняются через единый сервер. Даже если пользователи сидят в одной комнате и могли бы передать друг другу свои версии по Bluetooth, им всё равно нужно связаться с сервером (который в случае Google Docs предоставляет им Google). Этот алгоритм правилен только в том случае, если все изменения упорядочены одним сервером. А следовательно, любые другие каналы обмена информацией не допускаются, даже если технически доступны будь то Bluetooth, P2P или банальная флешка.



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


Сходимость


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


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


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


Хочу привести четыре примера конкретных проблем. Первый:


Аномалии чередования в текстовых редакторах для совместной работы


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


Я уже говорил, что алгоритмы операционального преобразования используют индекс символа. Алгоритмы CRDT же индексами не пользуются; вместо этого они присваивают каждому символу документа уникальный идентификатор. Механизмы генерации таких идентификаторов используются разные. Часто это делают с помощью дробей. Предположим, каждый символ у нас представлен числом от 0 до 1, где 0 начало документа, а 1 окончание. В примере с документом Helo позицию первого символа будет представлять число 0.2, второго 0.4, третьего 0.6, четвёртого 0.8.


Тогда при добавлении второй буквы l между первой l и o этому новому символу будет присвоено значение от 0.6 до 0.8 например, 0.7. Подобным же образом восклицательному знаку присваивается значение 0.9.



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


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


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



У нас есть документ Hello!. Один пользователь добавил Alice перед восклицательным знаком, второй пользователь добавил Charlie. Но в результате слияния этих изменений мы получили нечто совершенно непонятное. Почему это произошло? Чтобы разобраться, взглянем на схему, представленную на следующем слайде.



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


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


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


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



Проблему чередования мы нашли как минимум в двух алгоритмах Logoot и LSEQ. Из-за особенностей этих алгоритмов простого способа решить эту проблему для них нет. Нам не удалось улучшить их в этом отношении, не изменив при этом фундаментально самих алгоритмов. В Treedoc и WOOT мы этой проблемы не нашли, но, к сожалению, эти алгоритмы наименее эффективные. Собственно говоря, главной причиной, по которой был создан LSEQ, было достижение большей производительности по сравнению с Treedoc и WOOT. Возвращаться к этим неэффективным алгоритмам не хотелось бы. Далее, Astrong не алгоритм, а спецификация, формализующая требования к алгоритмам редактирования текста. К сожалению, она также допускает чередование.


RGA


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



На слайде представлен пример, в котором один пользователь добавляет слово reader между Hello и !, затем возвращает курсор к позиции непосредственно перед reader и добавляет dear. Здесь очень важно именно перемещение курсора. Второй пользователь только добавляет Alice между Hello и !. При действии алгоритма RGA возможным результатом этих операций будет чередование слов: Hello dear Alice reader!. Это тоже нежелательно, хоть и лучше, чем чередование символов.


Давайте разберёмся, почему это происходит. RGA использует структуру, показанную на слайде.



В ней текст представлен в виде очень глубокого дерева, в котором родителем каждого символа является предшествующий на момент добавления символ. Счёт идёт от положения курсора на момент ввода символа. Исходный текст Hello!. Затем у нас есть три случая перемещения курсора перед написанием reader, Alice и dear. В такой ситуации алгоритм RDA генерирует один из трёх возможных результатов, перечисленных на слайде. Из них второй, на мой взгляд, является нежелательным, поскольку в нём происходит чередование слов разных пользователей. Другие два результата вполне допустимы.


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


В Logoot и LSEQ проблема чередования, как мне кажется, нерешаема, и, к сожалению, использовать их из-за этого нельзя. Мы разработали алгоритм для RGA, который позволяет избавиться даже от этого варианта проблемы чередования, но сейчас у меня нет времени на нём останавливаться. При желании с ним можно ознакомиться в нашей статье об аномалиях чередования, представленной на семинаре PaPoC в 2019 году. Там подробно описан этот алгоритм.


Перемещение элементов в списке


Мы опубликовали статью на эту тему всего несколько месяцев назад (PaPoC 2020). На слайде проблема представлена на примере списка задач.



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


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


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



Оба пользователя одновременно выполняют одну и ту же операцию: перемещают операцию phone joe с последней позиции в списке на первую. Операция перемещения реализована как удаление с последующим добавлением, поэтому вначале необходимый элемент дважды удаляется на старой позиции. Здесь разницы с тем, чтобы удалить его один раз, нет. А вот если добавить элемент дважды, то на новой позиции появится два элемента phone joe вместо одного. Ясно, что такой вариант нам не подходит.


Давайте спросим себя: что именно должно было произойти в этой ситуации?



На слайде представлен другой пример, в котором всё тот же элемент пользователи переместили на разные итоговые позиции в списке. Первый пользователь переместил элемент phone joe в начало списка, второй на позицию после buy milk. К какому именно результату должен прийти алгоритм? Он может, как и в прошлом примере, дублировать элемент phone joe, чтобы в итоге этот элемент оказался и в начале списка, и после buy milk. Но это нежелательный вариант. Перемещаемый элемент должен оказаться лишь на одной из этих двух позиций. Чтобы этого достичь, необходимо выбрать одну из двух конфликтующих версий, причём выбрать произвольно, но детерминистично. В нашем примере выбрана версия, в которой phone joe оказывается в начале списка.


Как реализовать такой вариант? Если вы знакомы с существующими работами в области CRDT, это может выглядеть знакомо. Мы тут смотрим на ситуацию, в которой произвольно, но детерминистически выбираем одно из нескольких параллельно записываемых значений. Такое происходит к last writer wins register. Там, если значение могут обновлять несколько пользователей, при параллельной записи нескольких значений выбирается одно на основе произвольного критерия вроде метки времени.


Дальше это значение становится общим, а остальные значения удаляются. В нашем примере со списком необходимо именно такое поведение. Мы можем представить позицию каждого элемента в списке как last writer wins register, а значение, находящееся в регистре как описание позиции данного элемента. Когда первый пользователь указывает для позиции phone joe начало списка, а второй перемещает этот элемент на позицию после buy milk, нам необходимо, чтобы одно из этих значений стало общим для обоих пользователей. Таким итоговым значением может быть начало списка.


То есть для операции перемещения мы можем использовать существующий CRDT (last writer wins register). Чтобы реализовать такую операцию, необходимы две вещи. Во-первых, сам регистр, и, во-вторых, способ описать положение некоторого элемента в списке (это и будет значением, содержащимся в регистре). Но если подумать, то такой способ уже есть.


Вспомните, что я уже говорил выше. Все CRDT присваивают каждому элементу списка или символу в тексте уникальный идентификатор. Каким именно будет этот идентификатор, зависит от того, какой используется алгоритм CRDT. Например, в Treedoc применяется путь через бинарное дерево, в Logoot путь через более сложное дерево с большим фактором ветвления, в RGA метка времени и так далее. Во всех этих CRDT есть работающий механизм обращения к определённой позиции в списке. Так что позиции можно сгенерировать с помощью существующих алгоритмов. А при перемещении элемента на новую позицию можно создать новый идентификатор для позиции, на которую мы хотим переместить элемент, после чего записать этот идентификатор в наш last writer wins register.


Помимо этого, нам необходим отдельный регистр last writer wins для каждого элемента в списке. Но здесь тоже есть простое решение: это набор CRDT add-wins set (AWSet на слайде):



Можно создать add-wins set, в котором каждый элемент набора является пунктом нашего списка и состоит из пары, а именно, значения элемента (например, описание пункта списка) и регистра last writer wins с положением этого пункта. Регистр last writer wins можно поместить в набор AWSet, а идентификаторы позиции из CRDT списка можно поместить в регистр last writer wins register. Итак, мы объединили три CRDT и создали новый CRDT, а именно CRDT списка с операцией перемещения. Он позволяет атомарно переместить элемент с одной позиции в списке на другую что, согласитесь, весьма неплохо. Заметьте, что этот подход работает с любым из существующих алгоритмов CRDT списка. То есть любой существующий CRDT можно дополнить операцией перемещения.


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



Предположим, второй пользователь хочет переместить элемент Milk со второго места в списке на первое, то есть одновременно переместить последовательность символов. Параллельно этому первый пользователь изменяет Milk на Soy milk. Для этого он вначале удаляет первый символ слова Milk, а затем добавляет все остальные символы. В итоге возникло два отличающихся документа, которые теперь необходимо привести к одному состоянию. Каким должно быть это состояние? Интуитивно ожидаемый вариант переместить Milk в начало списка и изменить текст на Soy milk. К сожалению, если мы применим описанную мной выше операцию перемещения, то получим результат, показанный на следующем слайде.



Не знаю как вам, а мне на него смотреть грустно. Элемент Milk был перенесён на первую позицию в списке, но все изменения в этом элементе остались на старой позиции этого элемента. В результате документ заканчивается символами Soy m. Это произошло потому, что позиция, на которой было выполнено добавление этих символов, не изменилась. Я не думаю, что эта проблема нерешаемая, но пока что нам не удалось найти адекватного способа её преодолеть. Я буду очень рад, если кто-то из вас захочет над ней подумать.


Перемещение деревьев


Вообще деревья крайне полезная структура данных. Файловая система вашего компьютера, документ JSON или XML всё это примеры деревьев. Предположим, у вас есть приложение, которое использует то или иное дерево например, распределённую файловую систему или документ JSON с возможностью перемещать поддерева документа. Такому приложению необходима операция перемещения. И здесь сразу возникает проблема, целиком аналогичной той, с которой мы уже встречались в контексте списков: что происходит, когда несколько пользователей параллельно перемещают один и тот же узел на разные позиции?



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


Второй вариант сделать узел A одновременно дочерним и для B, и для C. Такая структура данных уже не дерево, а ориентированный ациклический граф (DAG), или граф более общего типа. На мой взгляд, этот вариант тоже неудачный. Следует выбирать между третьим и четвёртым вариантами, где A является либо дочерним для B, либо дочерним для C. Здесь, как и в ситуации с перемещением в списке, одна версия становится общей, а вторая удаляется.


Но сейчас перед нами встаёт дополнительное затруднение. Вы можете поставить эксперимент у себя на компьютере. Создайте каталог a, затем в нём создайте другой каталог b, а потом попробуйте переместить a в b. То есть попробуйте сделать a подкаталогом самого себя. Как вы думаете, что произойдёт? По идее, в результате должен возникнуть цикл. Я попробовал в macOS и получил сообщение об ошибке Invalid argument. Подозреваю, что большинство операционных систем, в которых файловая система является деревом, реагируют подобным образом, потому что если разрешить такую операцию, то система уже не будет деревом.


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



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


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


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



На одном компьютере я переместил A в B, на другом B в A, затем дал им синхронизироваться. Google Drive вернул сообщение о неизвестной ошибке. Так что как видим, даже Google не удалось найти удовлетворительное решение этой проблемы. Но для CRDT правильное решение всё-таки необходимо. С этой целью мы разработали алгоритм, который позволяет безопасно выполнять такого рода операции для деревьев, не приводя к возникновению циклических связей. О нём я сейчас и расскажу.


Схематично этот алгоритм представлен на следующем слайде:



Первый пользователь здесь выполнил операции op1, op3, mv A B и op5. Каждой операции присвоена метка времени t. Для соответствующих операций значения этих меток 1, 3, 4 и 5. У второго пользователя есть операция mv B A с меткой времени 6. Как уже говорилось, сами по себе эти операции перемещения безопасны. Сложности начинаются, когда мы пытаемся синхронизировать эти операции, как это показано на следующем слайде.



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


Давайте взглянем теперь на ситуацию с точки зрения первого пользователя. В его версии уже выполнены операции с метками времени от 1 до 5. С ними теперь нужно синхронизировать операцию op2, выполненную вторым пользователем. Поскольку значение её метки времени 2, она должна оказаться между двумя операциями первого пользователя, как это показано на следующем слайде.



Мы не можем просто добавить op2 в конец списка, потому что тогда они не будут упорядочены по метке времени. Чтобы определить, какие операции безопасные, а какие нет, важно знать порядок, в котором операции выполняются.


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



Для реализации этого алгоритма необходима возможность отмены операций. Как видим, первому пользователю нужно добавить новую операцию с меткой времени 2, в то время как у него уже есть операция с меткой времени 5. Поэтому ему необходимо отменить все операции с меткой времени больше 2, то есть операции op5, mv A B и op3. Теперь можно добавить операцию op2, а затем повторно применить все отменённые операции. При условии, что все операции полностью отменяемые, этот алгоритм генерирует список операций, применённых в порядке возрастания метки времени.


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


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



В сокращённом виде операция перемещения описывается структурой MoveOp. В каждой операции есть метка времени Timestamp. Такая метка должна быть уникальной для всего приложения; этого легко добиться с помощью, например, меток времени Лэмпорта. Далее, для операции необходим идентификатор, по которому можно обращаться к узлам дерева. Здесь child это идентификатор перемещаемого узла, а parent идентификатор узла назначения. Metadata meta это метаданные отношения родительского и дочернего узла. Например, если мы работаем с файловой системой, в качестве meta может выступать имя файла. Обратите внимание, что здесь нигде не записано старое местоположение дочернего узла. Операция просто переносит узел со старого местоположения на новое.


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


Теперь мы можем описать алгоритм перемещения. Вначале нужно определить, что значит, что a является родительским узлом b. Формально это сделано на слайде.



a является родителем b в том случае, если (a, m, b) дереву, то есть если a является прямым родителем b в дереве, или если есть узел c, такой, что a является родителем c, а c является родителем b. При таком определении операцию перемещения можно определить следующим образом. Вначале нужно проверить, не является ли перемещаемый узел родителем по отношению к своему родительскому узлу. Если да, то операция не выполняется, потому что она привела бы к возникновению цикла. То же самое в ситуации, когда дочерний узел идентичен родительскому. Если эти проверки успешно пройдены, то ситуация безопасна. Старое отношение можно удалить, а затем вставить новое отношение из дерева с новыми метаданными.


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


Сокращение издержек метаданных


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



На следующем слайде показан текст, в котором использован такой алгоритм. Если работа идёт с английским текстом, то в кодировке UTF-8 один символ занимает один байт; для текста на русском языке каждый символ, скорее всего, будет занимать два байта. Это не так много; но в дополнение к этому необходим идентификатор позиции. Чаще всего в качестве него используется какой-нибудь путь в дереве, размер которого зависит от длины пути. Он с лёгкостью может занимать несколько десятков байт, если не сотню. Помимо него необходим идентификатор узла, то есть пользователя, который добавил данный символ. В итоге для символа, занимающего один байт, необходимо 100 байт метаданных, или даже больше. Согласитесь, ситуация крайне печальная.


Хочу поговорить об экспериментах, которые мы проводили в проекте Automerge, специальной реализации CRDT. За последние несколько месяцев я посвятил довольно много усилий тому, чтобы сократить издержки CRDT. Результаты вкратце представлены на следующем слайде.



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


Проще всего этого добиться, если хранить журнал операций CRDT. Если записать такой журнал в формате JSON, он займёт около 150 мегабайт. С помощью gzip его можно сжать до примерно 6 мегабайт, но по сравнению со 100 килобайтами исходного текста без метаданных это всё равно огромный размер. Нам удалось, более эффективно закодировать данные и сократить размер файла метаданных до 700 килобайт (то есть где-то в 200 раз), при этом сохранив полную историю изменений. Если же отказаться от хранения перемещений курсора, размер можно сократить ещё на 22%. Далее, если убрать историю редактирования CRDT и оставить только метаданнные, необходимые для слияния текущей версии документа, размер сокращается до 228 килобайт. Это всего лишь в два раза больше размера исходного текста без метаданных. Если эти данные ещё и сжать в gzip, то мы получим чуть больше 100 килобайт. Далее, если избавиться от отметок полного удаления (tombstones), то мы получим 50 килобайт. Это то, что нужно для хранения уникальных идентификаторов для каждого символа. Но без отметок полного удаления мы уже не можем обеспечить слияния параллельно добавленных изменений.


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



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


Эти данные можно сжать с помощью нескольких довольно простых приёмов. Каждый столбец этой таблицы можно закодировать отдельно. Первый столбец содержит числа 1, 2, 3, 4, 5, 6. К ним можно применить дельта-кодирование, то есть вычислить для каждого числа разницу между ним и предшествующим числом. Тогда мы получим 1, 1, 1, 1, 1, 1. К этому ряду можно применить кодирование длин серий, то есть сказать, что мы храним 6 повторений числа 1. Если к этому применить кодировку переменной длины для целых чисел, результат займёт всего два байта. Эта кодировка записывает самые короткие числа одним байтом, которые подлиннее двумя, трёмя и тому подобное. Итак, для записи первого столбца нам потребовалось всего два байта, то есть байта на операцию. Для второго столбца нужно создать таблицу подстановки, в которой идентификаторам пользователей присваиваются короткие числа. Тогда второй столбец можно представить как 0, 0, 0, 0, 0, 0. Дальше опять-таки можно применить кодирование длин серий, а затем кодировку переменной длины для целых чисел, и, как и в первом случае, результат будет занимать два байта.


Взглянем на столбец с символами UTF-8. Обратите внимание, что у нас есть отдельный столбец, в котором указана длина символа в байтах. Сам этот столбец легко закодировать только что описанным способом. А символы UTF-8 можно просто объединить в строку UTF-8 размером в 6 байт. Наконец, в последних двух столбцах мы записываем только тот факт, что мы удалили один символ. В дополнение ко всему этому нам необходимо совсем немного метаданных с информацией о том, когда произошло каждое изменение, какой диапазон идентификаторов операций, какие значения счетчиков для каждого изменения. В результате можно реконструировать состояние документа в любой момент времени в прошлом. Итак, мы сохранили целиком историю документа, но сжали её так, что она занимает совсем немного места.


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


Научные статьи по теме

Logoot: Stephane Weiss, Pascal Urso, and Pascal Molli: Logoot: A Scalable Optimistic Replication Algorithm for Collaborative Editing on P2P Networks, ICDCS 2009.


LSEQ: Brice Nedelec, Pascal Molli, Achour Mostefaoui, and Emmanuel Desmontils: LSEQ: an Adaptive Structure for Sequences in Distributed Collaborative Editing, DocEng 2013.


RGA: Hyun-Gul Roh, Myeongjae Jeon, Jin-Soo Kim, and Joonwon Lee: Replicated abstract data types: Building blocks for collaborative applications, Journal of Parallel and Distributed Computing, 71(3):354368, 2011.


Treedoc: Nuno Preguica, Joan Manuel Marques, Marc Shapiro, and Mihai Letia: A Commutative Replicated Data Type for Cooperative Editing, ICDCS 2009.


WOOT: Gerald Oster, Pascal Urso, Pascal Molli, and Abdessamad Imine: Data consistency for P2P collaborative editing, CSCW 2006.


Astrong: Hagit Attiya, Sebastian Burckhardt, Alexey Gotsman, Adam Morrison, Hongseok Yang, and Marek Zawirski: Specification and Complexity of Collaborative Text Editing, PODC 2016.


Наконец, вот список наших работ по темам, которые мы сейчас обсуждали.


Interleaving anomaly: Martin Kleppmann, Victor B. F. Gomes, Dominic P. Mulligan, and Alastair R. Beresford: Interleaving anomalies in collaborative text editors. PaPoC 2019.


Proof of no interleaving in RGA: Martin Kleppmann, Victor B F Gomes, Dominic P Mulligan, and Alastair R Beresford: OpSets: Sequential Specifications for Replicated Datatypes, May 2018.


Moving list items: Martin Kleppmann: Moving Elements in List CRDTs. PaPoC 2020.


Move operation in CRDT trees: Martin Kleppmann, Dominic P. Mulligan, Victor B. F. Gomes, and Alastair R. Beresford: A highly-available move operation for replicated trees and distributed filesystems. Preprint


Reducing metadata overhead: Martin Kleppmann: Experiment: columnar data encoding for Automerge, 2019.


Local-first software: Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, and Mark McGranaghan: Local-first software: You own your data, in spite of the cloud. Onward! 2019.


Все слайды доступны в электронном виде. Спасибо большое за внимание!


Если вы дочитали этот доклад с Hydra 2020 до конца, похоже, что вы интересуетесь распределёнными вычислениями. В таком случае вам будет интересно и на Hydra 2021, которая пройдёт с 15 по 18 июня. Там будут как доклады от академиков, так и выступления людей из индустрии, которые имеют дело с параллельными и распределёнными системами в продакшне на сайте уже анонсирован ряд докладов.
Подробнее..

Модели памяти C и CLR

10.02.2021 14:12:50 | Автор: admin

Это расшифровка-перевод доклада Саши Гольдштейна, признанного лучшим на конференции DotNext 2016 Piter. С годами этот доклад стал лишь актуальнее прежнего: появление Mac на ARM-процессорах еще один пример, почему разработчикам сегодня нужно думать не только о x86-аерхитектуре.



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


То, что подходит процессорам Intel на архитектурах x86 и x86-64, может не подойти другой архитектуре. Как только вы перенесете свой код на другой процессор, например, на ARM для iPhone и Android, есть вероятность, что он перестанет работать как надо. Проблемы могут быть как очевидными (воспроизводиться с первого-второго раза), так и не очень (возникать только раз в миллион итераций). Вполне вероятно, что такие баги могут добраться до продакшна. Сегодня .NET и, конечно, C++ можно использовать не только на Windows и Intel, но и на других платформах, так что доклад будет полезен многим разработчикам.


Дисклеймер: данная статья предназначена для продвинутых читателей. Смотрите на свой страх и риск. За частое упоминание барьеров памяти и изменения порядка исполнения инструкций она получила возрастное ограничение 18+.

Вступление (My assumptions)


  • Вы C++ или C#-разработчик.
  • Вы пишете многопоточный код (а кто не пишет?).
  • Вы следите за корректностью кода и хотите, чтобы он правильно работал на различных платформах.
  • Возможно, вы привыкли к заботливой x86-архитектуре, но теперь хотите убедиться, что ваш код остался корректным в суровых и опасных условиях ARM-архитектуры.

Agenda




Атомарность


Итак, начнем с трех фундаментальных концепций атомарность, эксклюзивность и изменение порядка. Когда мы говорим, что операция атомарна, мы имеем в виду, что она не может быть прервана. Это означает, что во время выполнения операции не может произойти переключение потока, операция не может частично завершиться. На оборудовании с 64-битными процессорами Intel можно быть уверенными, что операции чтения и записи значений меньше 64 бит являются атомарными. Эти операции не могут быть прерваны и не могут частично завершиться. Проблема в том, что большинство на первый взгляд простых операций, которые мы пишем на языках высокого уровня, на самом деле не являются атомарными. Очевидный пример, с которым многие из вас наверняка знакомы увеличение значения на единицу. На C++, компилятор сгенерирует код наподобие такого:



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



Эксклюзивный доступ к памяти


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



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



Раньше префикс полностью блокировал шину памяти. То есть пока выполняется инструкция, помеченная данным префиксом, другие ядра (CPU) в принципе не могли получить доступ к памяти. Довольно высокая плата за эксклюзивность. На современных системах (начиная с Pentium 4, возможно, даже более старых) префикс больше не блокирует шину памяти полностью. Теперь блокируется только часть памяти достаточно убедиться, что линия кэша, где находится наше значение, не находится в кэше других ядер. Иначе говоря, если я обновляю значение на своем ядре, другие ядра временно не имеют доступ к этому значению. Это работает, но опять-таки, это довольно дорого, и, чем больше ядер, тем дороже обойдется операция. Резюмируя, эксклюзивность это возможность ядра получить монопольный доступ к памяти. Это вторая концепция, о которой я хотел поговорить.



Изменение порядка


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


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



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


Примеры


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



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


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



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



И мы не против подобных оптимизациях и не очень хотим о них знать, пока они не приводят к ошибкам. Это весьма лицемерный подход: нельзя просто предположить, что компьютер понимает, сломают ли оптимизации код или нет, потому что компьютер не знает о наших намерениях. Например, вы реализуете очередь на C, C++ или C#. У вас есть функция enqueue(x new_element), которая кладет новое значение в очередь и обновляет переменную locked присваивает ей значение 0.



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


Зачем нужны оптимизации?


Зачем компьютеры (компиляторы или процессоры) выполняют подобные оптимизации? Процессоры переставляют местами некоторые операции из-за конвейерной обработки.



Эта простая иллюстрация показывает, что инструкция внутри процессора проходит через несколько стадий. Так происходит у процессоров с MIPS-архитектурой сорокалетней давности. На современных процессорах Intel может быть 20 подобных фаз. Одна инструкция перемещается между фазами, пока другая инструкция находится в первой фазе, еще одна инструкция во второй и так далее. То есть множество инструкций находятся в разных фазах в одно и то же время. И результатом может быть изменение порядка: первая инструкция, записывающая значение в память, выполнится после третьей инструкции, считывающей значение из памяти, хотя изначальный порядок был другим. Наличие нескольких стадий в выполнении инструкций может привести к изменению порядка этих инструкций, если только вы не скажете компьютеру, что не допускаете подобные перестановки и хотите, чтобы инструкции выполнялись в изначальном порядке.


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


В современных процессорах Intel, например, у каждого ядра есть свой буфер небольшая очередь, которая хранит последние операции записи. Этот буфер сбрасывается в память в фоновом режиме. То есть, если процессору пришла инструкция Запиши Х в память и он ответил Сделано, на самом деле это может быть не так, и значение Х может находиться в буфере, который не видят другие ядра процессора. Буфер сбрасывается в память асинхронно. Даже после выгрузки буфера значение должно пройти несколько этапов кэширования, что также происходит довольно медленно.



Как понять, какие перестановки инструкций допустимы и что именно процессор будет считать верной оптимизацией? Есть одно правило, которое гласит, что необходимо соблюдать зависимости данных. Например, если в одном потоке сначала переменной Х присваивается значение 1, затем происходит считывание Х, последняя операция должна вернуть 1. Или если в потоке сначала какое-то значение присваивается переменной Х, затем какое-то значение присваивается переменной Y, и после этого происходит вычисление суммы Х и Y. Нет гарантий, какое присвоение произойдет раньше, но обе операции должны произойти до того, как X и Y будут считаны из памяти снова.


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



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



Примеры переупорядочивания памяти


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



Счетчик g_shared _counter лежит в общей памяти. Есть два потока, каждый из которых 20 миллионов раз ограничивает доступ к счетчику (g_protector.lock()), увеличивает его на единицу и освобождает доступ (g_protector.unlock()). К реализации lock() мы вернемся немного позже. Предположим, что кто-то, кому вы доверяете, предоставил вам реализацию.


Я запустил этот код на iPhone-симуляторе и в конце вывел значение счетчика на экран. Результат выполнения оказался верным на экране через некоторое время отобразилось сорок миллионов. Ура! Но я запускал iOS-симулятор на своем маке, внутри которого процессор Intel. А если вместо симулятора протестировать код на реальном iPhone? Результат 39999999. Близко, но не сорок миллионов. Итак, блокировка счетчика сломалась, причем только на ARM и только один раз из сорока миллионов.


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



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


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



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


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



Если коротко, два потока хотят получить доступ к критической секции. Также есть два флага, по одному на каждый поток. Если flag1 равен единице, значит, поток 1 хочет получить доступ к критической секции. То же самое для второго потока. Каждый поток кладет единицу в свой флаг, затем проверяет чужой флаг. Если другой флаг также равен единице, значит, оба потока хотят получить доступ к критической секции одновременно. Это неразрешимый спор, попробуем заново. Мы присвоим флагу значение 0 и повторим все сначала (часть с goto). Эти действия будут повторяться до тех пор, пока один из флагов во время проверки не будет равен нулю. Такая ситуация означает, что один из потоков не хочет получить доступ к критической секции. Кажется, что алгоритм должен работать, пусть и не очень эффективно, ведь раз за разом повторяется одно и то же. Это как вежливый обмен мнениями: Ты хочешь получить доступ к критической секции, я тоже хочу получить доступ, давай договоримся и найдем компромисс. Но, оказывается, на x86-архитектуре алгоритм не будет работать, так как операции чтения и записи в каждом потоке могут быть переставлены. Может получиться так, что один поток проверит флаг другого потока до того, как установит свой собственный флаг. Тогда возможна ситуация, в которой каждый поток будет думать, что другой поток не хочет получить доступ к критической секции, хотя на самом деле это не так.



Проблема в том, что подобную ситуацию не так-то просто зафиксировать, ведь такое происходит довольно редко (14 раз за 26551 повторение). Вы можете подумать, что ваш алгоритм синхронизации работает правильно, ведь вы проверили его сотни раз. Возможно, у вас есть автотесты, которые проверили его тысячи раз и не обнаружили этот редкий баг. Его сложно обнаружить, а если получилось, удачи воспроизвести его еще раз будет непросто. Что будет, если подобный алгоритм с таким небольшим, казалось бы, незначительным шансом на поломку будет внедрен в большую программу? Будет очень неприятно :(



Что такое модель памяти?


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


Следующее определение последовательная согласованность для программ без состояний гонки (sequential consistency for data-race-free programs или SC-DRF). На сегодняшний день эту модель использует большинство языков. Детали, опять-таки, довольно скучны, но в целом, состояние гонки это именно то, что вы думаете. Состояние гонки происходит, когда у вас есть несколько потоков, каждый из которых обращается к определенной переменной, то есть к определенной области в памяти, и хотя бы один из потоков производит запись в память. Если ваш код не предотвращает это, у вас произойдет состояние гонки. SC-DRF означает, что если в коде нет состояний гонки, система обеспечивает последовательную согласованность. Все будет выглядеть так же, как при выполнении в порядке, предусмотренном программой. И это, в большей степени, то, что вы можете получить от оборудования сегодня, то, что, например, ARM может предложить. Если вы предпримете необходимые шаги, чтобы гарантировать отсутствие гонок, система предоставит вам последовательную согласованность между несколькими ядрами.


Все это довольно скучно и теоретично, но это определяет модель памяти. Например, для C++11 модель памяти это SC-DRF. То есть C++, язык и библиотеки, предоставляют инструменты для избавления от состояний гонки. И если состояний гонки действительно нет в вашем коде, компилятор C++ гарантирует последовательную согласованность. И мы получаем нечто похожее от модели CLR, из официальной спецификации ECMA (the European Association of computer manufacturers). Если вы позаботитесь о состояниях гонки, то получите последовательную согласованность и отсутствие багов. Тем не менее, текущая реализация .NET, CLR, предлагает немного больше она избавляется от некоторых перестановок, не предусмотренных ECMA-спецификацией. По сути, реализация от Microsoft более дружелюбна к разработчикам, чем ECMA-спецификация. Но, даже со всем вышесказанным, пример с алгоритмом Петерсона все еще не будет работать.


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



Good fences make good neighbours (крепкие заборы дружные соседи)


Под заборами из подзаголовка мы будем понимать барьеры, которые предотвращают перемещение некоторых операций в памяти. И начнем мы с ключевого слова volatile, с которым связано некоторое недопонимание. В C++ volatile предотвращает изменения порядка следования кода, производимые компилятором. Компилятором, прошу заметить, не процессором. Это ключевое слово: не оказывается никакого эффекта на действия процессора.


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



Примеры сломанного кода и решения, как его починить


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



Добавление барьера во второй поток предотвратит перестановку операции записи и создания объекта. Это обеспечит безопасность, которой мы добивались. MemoryBarrier() это реальная функция, которую вы можете использовать в C++ на Windows и Visual Studio. Аналог этой функции в .NET называется Thread.MemoryBarrier() это статический метод класса Thread, который предотвращает перемещение операций через барьер.


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



Типы барьеров


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



Функции Monitor.Enter и Monitor.Exit гарантируют, что в одном из направлений операция точно не будет переставлена. Операции между двумя функциями не разрешено пересекать барьеры. Иными словами, операции не могут сбежать из под лока. В .NET чтение и запись переменной, помеченной ключевым словом volatile, создает однонаправленные барьеры, такие же, как на примере с Monitor. В C++11 появился довольно полезный класс, называемый std::atomic. Такая атомарная переменная дает вам полный контроль над тем, какой барьер вы в итоге получите.


Вернемся к примеру с счетчиком, лежащим в общей памяти, и посмотрим на реализацию spinlock. Она базируется на флаге, который принимает значения 0 или 1.



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



В первой версии, которая используется сейчас, обе инструкции используют memory_order_relaxed. Иначе говоря, какие-либо барьеры отсутствуют.



А это значит, что если в функции lock нет барьеров, и в функции unlock нет барьеров, операции записи, производимые внутри lock, могут быть выполнены до или после метода. Значит, блокировка не очень хорошая, она не выполняет свою основную функцию.


Сейчас возьмем вторую версию, в которой методы lock и unlock используют однонаправленный барьер.



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


Мы можем использовать std::atomic, барьеры памяти для избавления от состояний гонки. Иногда при смешивании однонаправленных барьеров результат выполнения может отличаться от ожидаемого. Например, если в алгоритме Петерсона пометить флаги ключевым словом volatile в C#, или сделать их атомарными в C++, мы получим однонаправленные барьеры для операций чтения и записи.



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


В финале хотелось бы рассказать о потокобезопасной реализации синглтона. Задача реализовать синглтон в C++ и сделать его потокобезопасным. Начнем с очень простой и очевидно нерабочей и определенно не потокобезопасной реализации, в которой несколько потоков могут инициализировать синглтон одновременно.



Прекрасно. Добавим в реализацию блокировку lock.



Это все еще не будет работать, так как два потока могут одновременно попасть внутрь блокировки и инициализировать синглтон.


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



Я делаю проверку, вызываю lock, затем снова проверяю, не инициализировал ли кто-то переменную. И это все еще не будет работать, так как мы можем получить исключение во время инициализации. Тогда не произойдет разблокировка, или вызов метода unlock, и весь процесс будет заблокирован.


Хорошо, обработаем исключения.



Но этот код также не будет работать по причине, которую мы рассматривали ранее. Операции записи, производимые конструктором, могут произойти после операции присвоения значения переменной instance. Если это произойдет, другой поток может подумать, что синглтон уже инициализирован, хотя это не так. Вы можете сказать: Хорошо, давайте добавим volatile к переменной instance.



Однако, опять-таки, volatile в C++ не оказывает никакого влияния на оптимизации процессора. Единственное, что действительно поможет, это std::atomic.



Есть еще более простой способ создания потокобезопасного синглтона.



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


Аналогичный раздел о синглтонах на C# есть в книге C# In Depth

Вывод


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


Это был доклад с DotNext. А в апреле состоится новый DotNext, и какие доклады будут там, зависит в том числе от вас: приём заявок ещё открыт.
Подробнее..

Кроссплатформенная мобильная разработка история вопроса

04.03.2021 12:16:30 | Автор: admin

Когда речь заходит о разработке сразу для Android и iOS, начинаются холивары и гадания на кофейной гуще. Что перспективнее, Flutter или Kotlin Multiplatform? За этими технологиями будущее, или завтра их обе забудут?

Уверенно делать прогнозы по принципу я так вижу занятие весёлое, но бессмысленное. Как подойти конструктивнее? Как известно, кто забывает об истории, обречён на её повторение. Поэтому я решил вспомнить, какими были предыдущие решения Android+iOS, и посмотреть, что с ними стало. Тогда вместо голых спекуляций будут суровые факты. И эти факты из прошлого полезны для понимания будущего: они показывают, где разложены грабли.

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

Оглавление


Web / PWA

В 2007-м, представляя разработчикам первый iPhone, Стив Джобс объяснял, что нативные приложения не нужны: В iPhone есть полный движок Safari. Так что можете писать потрясающие Web 2.0 и Ajax-приложения, которые выглядят и действуют на iPhone именно как приложения.

Android на тот момент ещё даже не был анонсирован. Но получается, что исторически первым единым решением для iOS и Android стал веб.

Вот только разработчики тогда не разделили энтузиазма Джобса (неудивительно, учитывая тогдашнее состояние мобильного интернета). И годом позже всё-таки появился App Store для нативных приложений. А его комиссия в 30% стала новым денежным потоком для компании. В итоге её позиция сменилась на противоположную: теперь Apple считает, что правильный подход это натив (и предпочитает не вспоминать, что там её лидер говорил в 2007-м, Океания всегда воевала с Остазией).

Однако идея веб-приложений не исчезла, а продолжила развиваться. И в 2015-м новое поколение таких приложений назвали Progressive Web Apps. Они могут хранить данные локально, работать в офлайне, а ещё их можно установить на домашний экран смартфона. Чем это тогда не настоящие мобильные приложения? Что ещё для счастья надо?

Ну, например, для счастья нужны push-уведомления. По состоянию на 2021-й iOS не поддерживает их у PWA, и это мощнейший стопор для распространения подхода. Получается, компания, которая первой хвалила веб-приложения, позже сама поставила главное препятствие на их пути. На change.org есть даже петиция, обращённая к Apple: мы всё понимаем, вы дорожите своими 30%, но эта ситуация должна измениться.

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


PhoneGap/Apache Cordova

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

PhoneGap появился в 2009-м благодаря компании Nitobi, а в 2011-м Adobe купила её и создала также опенсорсный вариант Apache Cordova. У проекта модульная архитектура, позволяющая подключать плагины. И в сочетании с опенсорсностью это означает, что если Cordova не умеет чего-то нужного (например, взаимодействовать с акселерометром смартфона), сообщество может научить. Вроде как прекрасно, да?

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

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

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

Интересно, что авторы проекта сами предвидели такое развитие событий и ещё в 2012-м написали, что итоговая цель PhoneGap прекратить своё существование. И недавно эта цель была достигнута: в 2020-м Adobe заявили о прекращении разработки, ссылаясь на то, что нишу закрыли PWA.

Что в итоге: разработка прекращена.


Qt

Проект Qt помогал людям заниматься кроссплатформенной разработкой ещё с 90-х, когда речь шла о десктопных ОС, а не мобильных. При этом он завязан на C++, который для Android и iOS не совсем чужой: даже нативные разработчики на этих платформах могут обращаться к плюсам. Так что, казалось бы, поддержка iOS/Android со стороны Qt просто напрашивалась.

Но поначалу дело осложнялось тем, что в 2008-м проект купила Nokia. Компания тогда делала ставку на Symbian и не горела желанием помогать конкурентам. В 2010-м возможностью запускать Qt-приложения на Android занимались энтузиасты, и на Хабре об этом писали:

В 2011-м Nokia отказалась от Symbian в пользу Windows Phone, а часть проекта Qt продала компании Digia. И тогда началась работа над поддержкой одновременно Windows 8, Android и iOS. Ну вот теперь-то счастье? Спустя 10 лет ясно, что тоже нет.

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

Что помешало? Я встречал жалобы на то, что недостаточно было использовать сам Qt и писать на С++/QML. Потому что средствами Qt многое было не реализовать, и приходилось-таки иметь дело с конкретной платформой (например, на Android работать с Java, увязывая это с плюсовой частью через JNI). Всё это очень неудобно и подрывает исходную идею бодренько запилим два приложения по цене одного.

При этом здесь пользователь тоже ощущает не-нативность. А к IDE Qt Creator есть нарекания, рантайм Qt увеличивает размер приложения, бесплатный вариант Qt подходит не всегда и может понадобиться недешёвый коммерческий. Кроме того, мне лично кажется, что ещё сказался язык. Кроссплатформенной разработке желательно быть такой, чтобы нативным разработчикам было попроще перекатиться туда, а с языком C++ слово попроще не ассоциируется.

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

Что в итоге: крайне нишевое решение, которое не умерло полностью, но используется очень узкой прослойкой.


Xamarin

Мигель де Икаса ещё в проекте Mono занимался тем, что притаскивал .NET на несвойственные ему платформы (начав с Linux). А когда в 2011-м он вместе со всей командой Mono попал под сокращение, основал новую компанию Xamarin, собрал прежних коллег там, и сосредоточился на мобильных платформах: мол, давайте писать мобильные приложения для них на C#, вот инструмент для этого.

Надо понимать контекст того времени. Годом ранее компания Microsoft выпустила Windows Phone и стремилась стать третьим большим игроком на мобильном рынке. И даже большая Windows лезла на мобильный рынок: готовилась к выходу Windows 8, оптимизированная для планшетов.

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

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

Ещё порой вызывали недовольство и то, что после появления новых фич в Android/iOS приходилось ждать их поддержки в Xamarin, и стоимость лицензии, и общее состояние экосистемы (документация, поддержка, библиотеки).

А тем временем компания Microsoft возлюбила опенсорс и сторонние платформы вроде Linux, так что идеи де Икасы оказались ей близки, и в итоге она купила Xamarin. Теперь его наработки вошли в .NET 5, и в прошлом году представили .NET MAUI (Multi-platform App UI) развитие тулкита Xamarin.Forms. В общем, не забросили купленное.

Что в итоге: пока всё остаётся в странноватом статусе, когда и масштабного взлёта не получилось, и полностью не прекращено.


React Native

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

Писать предлагается на JavaScript, поэтому кому-то может показаться, что это реинкарнация PhoneGap и его HTML-подхода, но нет. Когда-то Facebook действительно полагался для мобильных устройств на HTML, но ещё в 2012-м Цукерберг назвал это ошибкой. И у React Native идея не в том, чтобы с помощью HTML/CSS городить что хочешь, а в том, чтобы с помощью JSX использовать нативные элементы UI так что пользователь ощущал себя прямо как с нативным приложением.

Насколько это получилось? Шероховатости и срезанные углы существуют с ними сталкиваются и разработчики, и пользователи. Для кого-то они оказываются критичными: особенно нашумел пост Airbnb, где после React Native решили вернуться к нативной разработке. Но какие-то другие компании (вроде самой Facebook) эти ограничения не остановили, использование в индустрии есть.

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

Если зайти на HeadHunter и сравнить число вакансий по React Native с нативной разработкой, то их немного. Но вот если сравнивать с Qt/Xamarin/PhoneGap то вакансий окажется больше, чем у них всех, вместе взятых, это наиболее успешный вариант.

Что в итоге: React Native не стал так успешен, как его фронтендовый старший брат, но определённую нишу занял.


Выводы

Обычно в холиварах занимают крайние позиции: эта крутая штука всех победит или все эти глупости скоро отомрут. А у меня по итогам всех вариантов позиция получается сдержанной:

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

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

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

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

А вот про Kotlin Multiplatform сделать выводы по прошлому у меня не получилось, потому что у него нетипичная ситуация, отличающаяся от предшественников. Во-первых, идея кроссплатформа годится не для всего тут заложена прямо в фундамент: а мы и не предлагаем объединять всё, реализуйте общую бизнес-логику, а остальное на Android и iOS делайте раздельно. А во-вторых, тут играет на руку родной для Android язык: он уже знаком половине мобильных разработчиков, и такого в кроссплатформе раньше не возникало. Так что опыт предыдущих технологий тут непоказателен остаётся смотреть, что покажет время.

На последнем Mobius было сразу несколько кроссплатформенных докладов (три про Flutter, один про Kotlin Multiplatform) мы уже выложили все видеозаписи конференции на YouTube, так что можете сами их посмотреть. А мы тем временем вовсю работаем над следующим Mobius: он пройдёт 13-16 апреля в онлайне, и там без освещения кроссплатформы тоже не обойдётся. Так что, если вас заинтересовал этот пост, то вам будет интересно и там.

Подробнее..

Как ощутить интернет 2000 года

12.03.2021 16:12:58 | Автор: admin

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

Все знают, что раньше интернет был попроще. Но часто ли вы вспоминаете, что во время его использования вся семья оставалась без телефонной связи? Помните ли встроенный в Opera баннер? Программы FlashGet и eMule? Вёрстку сайтов таблицами и фреймами? Штурмовые корабли в огне на подступах к Ориону? (Извините, увлёкся.)

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


Подключение

Конечно, начать надо с этих звуков:

Запустите ролик, отвлекитесь от текста и прослушайте эти полминуты целиком, прежде чем продолжать читать. Потому что в 2000-м, чтобы выйти в интернет и что-то там читать, большинству людей требовалось сначала послушать это.

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

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

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

  • И главное. Оплаты домашнего интернета чаще всего была почасовая (можно было купить карточку, например, на 10 часов). И это было недешёво, так что время подключения попросту приходилось экономить. Карточки выглядели так:

Фото взято отсюда, там ещё много

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

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

Кстати, а что это вообще были за звуки, которые можно принять за неизданный трек Афекса Твина? Модем пользователя звонил по телефонной линии провайдеру (кстати, не факт, что дозвонишься с первого раза), после чего звуками здоровался с оборудованием провайдера, договариваясь о деталях взаимодействия. На Хабре переводили пост по этому поводу, вот познавательная схема из него:

Штуки вроде люди сохраняли страницы на диск для чтения в офлайне через Wayback Machine не поймёшь. Поэтому, если хотите прочувствовать интернет 2000-го попробуйте убрать подальше смартфон, отключить Wi-Fi на компьютере и включать его на очень ограниченные промежутки времени. И посмотрите, как изменятся ваши паттерны поведения. Как вы начнёте записывать так, вот это надо будет погуглить.


Скорость

Только позавчера все обсуждали замедление Твиттера в России. Сообщается, что замедлили до 128 кбит/сек.

И тут получается наглядное сравнение. Сегодня такая скорость это жёсткое замедление и наказание, ужасно, когда простые картинки грузятся не сразу! А если бы вы в 2000-м году вы сказали о такой скорости, вас назвали бы буржуем. 128 кбит это уже времена ADSL, середина нулевых. У модемов, использовавших телефонные линии, пределом были 56 кбит/сек (а реальные показатели оказывались и того ниже).

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

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

Если просто открыть через Wayback Machine старые сайты, этот факт пройдёт мимо. Будешь смотреть на сайты с картинками и думать, что все их тогда так и видели. Нет, далеко не все.

Как ощутить тогдашние скорости, если сегодня даже замедленный твиттер гораздо быстрее? Может помочь инструмент из Chrome DevTools. Откройте там вкладку Network. В ней есть возможность целенаправленно троттлить скорость:

По умолчанию представлены несколько пресетов вроде "Fast 3G", но даже "Slow 3G" слишком быстрый. Поэтому создайте собственный пресет в пределах 56 кбит. И попробуйте с ним открыть, например, новостной сайт (то есть чтобы текст + картинки, без хитрых интерфейсов). Получите интересные впечатления.

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

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


Браузеры

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

  • Сегодня кажется само собой разумеющимся, что браузер бесплатный софт. Но так было не всегда. Netscape был платным для корпоративного использования, а Opera предлагала пользователям либо заплатить, либо получить встроенный баннер прямо в интерфейсе браузера. И одна из причин победы Internet Explorer на том витке истории его бесплатность. А одна из причин тогдашнего успеха Opera в России тут использовали взломанную Оперу с отключенным баннером.

  • Сегодня кажется само собой разумеющимся, что у браузера есть вкладки. Но так было не всегда. В Internet Explorer они появились только в 2006 году. Если вы сейчас посмотрели на свои сто открытых вкладок и ужаснулись мысли это ж было бы сто отдельных окон, можно отчасти успокоить: во времена интернет медленный, выходят туда ненадолго, оперативки мало никто не держал открытыми сто сайтов. Паттерны поведения и здесь изменились.

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

Хабрахабро-крякозябрыХабрахабро-крякозябры

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

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

А если хочется прочувствовать интернет 2000-го, то открывать в этих браузерах надо не современные сайты, а олдскульный контент (о нём ниже).

Кстати, видите справа внизу в статусбаре Internet Explorer синюю полоску загрузки страницы? В те медленные времена люди с придыханием следили за тем, как она ползёт. Правда, она всё равно была малоинформативной: могла долго пробыть в самом начале, а потом резко скакнуть.

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


Веб-дизайн

Послушав модемные трели и запустив браузер без вкладок, можно было наконец узреть сайты. Какими они были и где теперь такое увидеть? Главный помощник по вопросу где сокровищница Wayback Machine с архивами громадной части интернета. А по вопросу какими распишу здесь главные тренды:

1. Тексты

Поскольку интернет был медленным и не особо интерактивным (никакого там обновления страницы на лету), в него шли за текстами: новостями, письмами, рефератами.

Если хотите прикоснуться старому текстовому контенту, порекомендую колонку Вечерний Интернет, которую вёл Антон Носик её заботливо сохранили даже вне Wayback Machine. Во-первых, это важный ресурс раннего рунета. Во-вторых, это способ одновременно и полазать по старому вебу, и прочитать там как раз о тогдашнем состоянии веба. Интересный документ эпохи, в которой браузеры называли гляделками.

Текстом тогда решались и задачи, для которых сейчас часто используют другие форматы. Для социализации вместо всяких инстаграмов с клабхаусами можно было использовать форумы и чаты. За юмором вместо Тиктока и мем-пабликов шли на сайт anekdot.ru (кстати, он жив и не сильно изменился). И даже порно многие тогда потребляли в виде рассказов на сайте Стульчик.нет (а то с этими JPG-файлами долго ждать: они же сверху вниз загружаются, а самое интересное внизу).

2. Минимализм

Если всё начиналось с текстов, неудивительно, что дизайн зачастую был предельно аскетичен: чёрные символы с синими ссылками на белом фоне. Сейчас сайты вроде Motherfucking Website делают только как ироничный аргумент в пользу легковесных подходов. В 2000-м такой дизайн хоть уже и не был пределом мечтаний (Артемий Лебедев вовсю демонстрировал, что можно иначе), но никого не смущал.

Главная онлайн-библиотека того времени lib.ru в 2021-м выглядит почти так же, как десятилетия назад так что можете даже без Wayback Machine сделать выводы по ней.

3. Пестрота

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

Какие средства для этого были? Гифки: пусть на главной странице куча всего двигается! Бегущая строка: пусть даже текст на месте не стоит! Разные цвета и шрифты: чем больше, тем лучше! Музыка: MP3 были слишком тяжёлыми, а вот писклявый MIDI-формат подходил. То есть можно было, зайдя на чью-то страницу, быть атакованным в глаза и уши одновременно.

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

4. Золотая середина

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

Поразительно, но спустя 25 лет после выхода фильма его сайт всё ещё доступен, так что увидеть легко. Что мы можем увидеть там показательного для тех лет?

  • Компромисс между текстами и гифками: элементами навигации служат статичные картинки.

  • Вёрстку таблицами и фреймами. Тогда миром ещё не завладели div, и всё возможное обычно делали через table. А ещё порой делили фреймами окно браузера на части и подгружали в них разные HTML-страницы (в одной могло быть меню, в другой основной контент) на этом сайте они тоже присутствуют.

  • Простой HTML-код без всех этих ваших реактов. В те времена, нажив view page source на сайте, даже не слишком опытный человек мог разбираться в происходящем. Можно было учиться делать сайты, ходя по разным и смотря, как всё сделано у них.

  • Во многих разделах сайта для навигации используется картинка, кликая по разным участкам которой, попадаешь на разные страницы. Это модный тогда изыск: интереснее простых текстовых ссылок, напоминает навигацию в adventure-играх. На таком основан например, сайт city.cyberpunk.ru, который тоже жив спустя все эти годы там это очень кстати, киберпанк же про киберпространство, и в каком-то смысле оно и воплощалось, захватывая дух.


Контекст

Ранние сайты не существовали в пустоте что тогда происходило вокруг них? И в смысле сетевых штук (не всё, что требует подключения, происходит в браузере), и в офлайне?

Например, что было со скачиванием контента вне браузера? Конечно, видео для 2000-го ещё было слишком тяжёлый, а вот с музыкой как раз разворачивался экшен. Первый популярный peer-2-peer-сервис Napster набирал обороты, Metallica подала на него в суд, и человечество чесало голову, пытаясь сформулировать позицию по онлайн-пиратству.

Конкретно Napster из-за иска был закрыт, но вслед за ним появился Soulseek, а в 2002-м eMule так что ещё до расцвета торрентов люди активно скачивали файлы друг у друга. Конкуренцию таким сервисам составляли локальные сети и FTP-сервера с загрузкой по прямой ссылке.

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

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

А что тем временем происходило в офлайне? Поскольку люди всё чаще замечали интернет и осмысляли новое явление, всё чаще возникали и офлайновые штуки, связанные с этим. Кажется, точкой всенародного единства стал суперхит Ленинград WWW (2002), а за годы до него были более локальные штуки например, выходил журнал Мир Интернет. То есть вдумайтесь: бумажное издание, где на печатных страницах рассказывали о том, что происходит в интернете. Сегодня звучит странно если это издание для людей, которым важен интернет, зачем ему физическая форма? Но тогда в этом была логика: например, бумажное издание тогда можно было почитать в метро, не то что сайты.

Статьи выкладывали и на сайт этого журнала iworld.ru, так что с помощью Wayback Machine можно ознакомиться с чаяниями того времени. Например, что с платёжными системами в интернете всё грустно. Мол, какие-то западные уже появились, но они хотят данные кредитной карточки, а откуда ж у обычного россиянина такая карточка?

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

Или вот заголовки из женского мартовского номера 2000 года, когда в интернете можно найти рецепты ещё было значимым событием:

Конечно, для атмосферы было бы клёво не только почитать тексты, но и увидеть сканы хотя бы одного номера целиком. К сожалению, они вроде бы практически не сохранились. Издательство Питер активно присутствует на Хабре, поэтому обращусь: @piter, может, поделитесь архивами?


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

А я закончу минуткой рекламы. Во-первых, 27 марта мы проводим бесплатный онлайн-фестиваль TechTrain там будет несколько докладов для самых разных айтишников.

А во-вторых, TechTrain станет разогревом перед нашим новым конференционным сезоном. Если старые сайты интересны вам, потому что вы фронтендер вам будет интересно на HolyJS. А кроме этого, проведём мероприятия по Java, .NET, тестированию, мобильной разработке увидеть все эти пять можно на общем сайте.

Подробнее..

Эволюция декларативных UI-фреймворков от динозавров к Jetpack Compose

08.04.2021 16:21:10 | Автор: admin


Проект Jetpack Compose привлёк много внимания в Android-мире, ещё когда был в альфа-версии. А недавно добрался до беты так что теперь всем Android-разработчикам пора понимать, что он собой представляет.


Поэтому мы сделали для Хабра текстовую версию доклада Матвея Малькова с нашей конференции Mobius. Матвей работает в Google UK и лично причастен к Compose, так что из доклада можно узнать кое-что о внутренностях проекта. Но доклад не ограничивается этим: внимание уделено не только Jetpack Compose, а всему декларативному подходу в целом.


Кстати, недавно появился ещё и проект Compose for Desktop от JetBrains. И скоро на Mobius о нём тоже будет рассказ из первых рук: 14 апреля об этом расскажет руководитель проекта Николай Иготти.


Далее повествование будет от лица спикера.



Я хотел бы рассказать не только про Jetpack Compose, но и поговорить в целом про декларативные UI-фреймворки: окунуться в их прошлое, настоящее и будущее.


Прошлое: о декларативном и императивном


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


Декларативное программирование это парадигма, которая позволяет нам задавать программы, не описывая control flow. Иными словами, мы описываем не как, а что мы хотим видеть на экране. Это самое большое отличие от императивного программирования.



Вот пока пример не про UI. Часть How это императивная история. Мы берем var и складываем значения у него в цикле. И у нас есть What декларативное программирование, где мы говорим, что хотим сделать: заредьюсить список с помощью какой-то операции.


Возьмём пример попроще:


val a = 4
val b = -2
val result = a + b


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


Поколения языков программирования



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



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



Ребята придумали языки высокого уровня: Java, Kotlin, C, C++. Они тоже очень декларативные по сравнению с предыдущими поколениями, потому что ты не объясняешь машине, что положить, а просто говоришь: a = 4, b = 2 и result = a + b. Но все это прошлое.



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


Очевидно, что любое новое поколение по сравнению с предыдущим более простое в использовании для более сложных задач. Наши проблемы развиваются, и нам нужно решать их проще. Когда возникают новые проблемы, мы придумываем новые абстракции, новые поколения, которые решают их более эффективно. В числе примеров четвертого поколения 1C, R, SQL, RegEx. Ещё в четвёртом поколении есть domain specific languages, то есть небольшие фреймворки, которые решают конкретные проблемы: React, RxJava, JetpackCompose.


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


Например, в C++ есть intrinsics или inline ASM. В React мы можем получать reference прямо на DOM-элементы или контролировать жизненный цикл. Отличный пример в RxJava PublishSubject. В Jetpack Compose есть imperative drawing, imperative touch-handling, onCommit. Это даёт больший контроль, а также возможность строить поверх императивного свои декларативные абстракции.


UI-фреймворки: вчера и сегодня


Говорить о фреймворках мы будем в разрезе Android и веба, потому что проблемы в UI, с которыми сталкиваются Android-разработчики, есть и в вебе, и в других UI-областях.


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


Если я заменю HTML на main_layout.xml, который есть у нас у всех в Android-приложении, ничего не изменится. Это тот же самый язык разметки, сконвертированный с HTML для Android. Это тот же подход к UI: вызываешь функцию inflate, и получаешь дерево UI.


Проблема в том, что мы не живем в 1995 году и не пишем статические сайты, статические приложения, где на каждую ссылку у нас загружается новая страница. Мы пишем сложные приложения, которые содержат много state и анимации. Это называется динамический UI, к которому мы применяем изменения на лету.


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



Когда мы нажимаем на кнопку Read All, у нас 0 непрочитанных. Чтобы разобраться, рассмотрим дерево нашего UI (будь то хоть Android с вещами вроде TextView, хоть хоть HTML-элементы вроде div). И перед нами такой человек, его будут звать Капитан Динамичность.



У нас есть 100 непрочитанных сообщений, и пользователь нажимает Read All. Капитан Динамичность берет UI, понимает, что нужно поменять, находит это, убирает лишние элементы, соединяет нового родителя с child. Молодец!



Потом пришло новое сообщение. Нужно снова создать подветку UI (надеюсь, не по частям, а одну часть), можно это делать через View.GONE или View.INVISIBLE. И мы снова получаем непрочитанные сообщения. Проблема в том, что если добавляется кнопка undo или какие-то ещё новые требования, становится сложно следить за всеми состояниями. И переходов между ними уже совсем не два.


Давайте посмотрим на реальный пример.


val unreadRoot = document.getElementsByClassName("unreadRoot"); val count = document.createElement('div'); count.textContent = 'You have $count unread messages'; unreadRoot.appendChild(count);

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


val unreadIcon = findViewById(R.id.unread) if (unreadCount == 0) { unreadIcon.setVisibility(GONE) } else if (unreadCount > 0) {           unreadIcon.setVisibility(VISIBLE)           unreadIcon.setCount(unreadCount)           var hasImportant = unreadMessages.exists(it.isImportant)           if (hasImportant) {           unreadCount.setTextColor(Red)           } else {           unreadCount.setTextColor(Blue)           } }

Я не проверил unreadIcon на null, хотя стоило. Где-то я обновил TextColor в зависимости от чего-то. Не забыть бы поменять обратно при каких-то изменениях.


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


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


UI-фреймворки: настоящее и будущее. Главные принципы


Новый UI-фреймворк:


  • Убирает переходы между состояниями (для разработчика)
  • UI это функция от состояния
  • Объединяет markup и динамическое обновление

Скорее всего, придется модифицировать наш HTML или XML или объединить это с динамическим обновлением, чтобы позволить создавать UI, как функцию от какого-то state.



Я имею в виду функцию или объект, которому мы скармливаем данные нашего приложения: например, есть ли у нас непрочитанные, будет это функция выглядеть как f(false) или f(true). Строим в этих случаях два разных дерева.


Чтобы это сделать, нам придется соединить Markup и Dynamic, которые у нас есть на третьем уровне и построить фреймворк четвертого уровня, который позволяет задавать динамический Markup. И есть понимание, что это уже сделано. Например, React объединил HTML и JavaScript в React Components. И вместе с ним идет JSX, который позволяет создавать внутри JavaScript как будто бы HTML.


В случае с Jetpack Compose аналогично: Composable-функции делают то же самое, соединяя разметку и динамику.



Разберёмся в том, что такое Compose, как он работает, чтобы понять контекст.


В его случае любой UI-элемент это функция с аннотацией Composable. Только Composable-функции могут вызвать Composable-функции.


@Composable fun UnreadSection(count: Int) {            Text(         text = "You have $count unread messages",          color = if (count > 10) Color.Red else Color.Green          )       if (count > 0) {            ReadAllButton()        }        }

Функция принимает состояние в данном случае функция UnreadSection принимает количество непрочитанных сообщений. И дальше на основании состояния я могу делать все, что угодно. Могу прямо здесь сказать, что у меня есть текст с определенным цветом. Могу вставить конструкции Kotlin: if или for. Могу добавить кнопку или убрать её. Для этого не нужно больше вручную настраивать её visibility, обновлять текст, искать его откуда-то из XML и так далее.


Вернёмся к примеру с функцией от false и true. Если мы поставим UnreadSection(count = 100), у нас получится одно дерево, а если UnreadSection(count = 0), то получится другое.



И в этом случае мы можем сказать, где чему равен count. Jetpack Compose так и работает.


Но возникает интересный вопрос: как перейти из одного дерева в другое? Простым ответом может быть: а давайте менять новое дерево на экране, у нас было одно, а бахнем другое. Это так не работает. У нас уже есть какие-то анимации, другие виджеты или другие части экрана. Если мы будем перестраивать дерево, это будет очень долго. То же самое в вебе: не получится просто так перезагрузить страницу, это занимает какое-то время. Поэтому нам нужно сделать это динамически.


Заменить экран с одного на другой дорогая история.


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


Разберемся, как воплотить идею.



У нас есть UI это функция от состояния. С этим мы разобрались, это можно сделать в нашем фреймворке. Итак, у нас есть реальный UI это то, что видит пользователь. И у нас есть виртуальная репрезентация, которую мы можем построить в фреймворке. У нас функция принимает true, пропадает количество unread count или, наоборот, появляется при false. Мы строим новую виртуальную репрезентацию. После этого мы можем посчитать diff этих виртуальных репрезентаций, на его основе построим список императивных операций. Например, это могут быть псевдокод и операции, которые построили над реальным UI. Все будет делаться внутри фреймворка.



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


Наш псевдокод называется Reconciliation, он делает за нас то, что мы делали. Если Reconciliation кажется вам сложным словом, то запомните это как Авто Капитан Динамичность.


И кажется, у нас складывается фреймворк, у нас есть виртуальная репрезентация, мы их строим, строим diff, строим последовательность операций Reconciliation и обновляем наше реальное дерево. Но как построить список этих императивных операций? Давайте разберемся, что такое Reconciliation, какая в этом задача и проблема.


Reconciliation


Базовая задача звучит так: построить минимальный набор команд для трансформации дерева А в дерево Б.


Проблема: общий алгоритм построения набора операций, необходимых для трансформации одного дерева в другое, имеет скорость O(n3).


Кубическая сложность во времени значит, что если у нас есть дерево с тысячей элементов (ну, среднестатический такой UI), то нам надо будет сделать один миллиард операций, чтобы обновить их. Так не пойдёт.


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


Упрощение 1 (очень общее упрощение, которое вместе со вторым упрощением в каком-то виде применяются в React, Compose): если мы понимаем, что обновляемый элемент/поддерево те же самые, то можем обновить дерево без пересчитывания полностью.


Упрощение 2: если входные данные этого компонента не меняются, то не меняется и поддерево, которое ему соответствует.


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


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


Давайте посмотрим, как мы это делаем внутри Jetpack Compose.


Jetpack Compose внутри


Важная оговорка: мы рассмотрим внутренности, а они имеют тенденцию меняться. Если вы знакомитесь с этим докладом в записи, что-то уже могло измениться. Но останется неизменные главное core-принципы (а изменятся параметры, которые мы генерируем, их последовательность или что-то ещё).


Мы не храним виртуальную репрезентацию как дерево, а используем Gap buffer. Это классическая, но малоизвестная структура данных, используется в текстовых редакторах вроде vim и Emacs. У неё есть, грубо говоря, список, и в этом списке есть gap. Gap один, он может быть большой или маленький, его можно увеличивать. И мы можем совершать определённые операции в этом Gap buffer. Первая логическая операция insert. Это операция добавления, и она константная.



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



Мы не можем вклиниться в середину быстро и добавлять туда элементы. Это tradeoff, который мы делаем в Compose с расчетом на то, что структура конкретно нашего UI меняется не так часто, реже, чем происходят апдейты. Например, есть текстовый View, и в нем чаще меняется строчка текста, чем он превращается в иконку.


Итак, рассмотрим на реальном примере, что же происходит в Gap buffer. Мы будем делать что-то примерно такое: у нас есть Mobius, мы кликаем обновляется счётчик.



Как это сделать по Jetpack Compose? Мы можем сделать Composable-функцию:


@Composable fun Counter(title: String) {     var count by state { 0f }     Button(        text = "$title: $count" ,        onClick = { count++ }        ) }

Мы можем задать state (позже разберёмся, как это работает главное, что есть обёртка, которую я могу обновлять по клику). И у нас есть Button, в котором $title (в данном случае Mobius) и count (сначала 0, увеличивается при клике).


Сейчас произойдет бум-момент, потому что мы посмотрим, что же делает аннотация @Composable. Она превращает код примерно в такой:


fun Counter($composer:Composer,$key: Int,title: String) {     $composer.start($key)     var count by state($composer,key=123) { 0f }     Button($composer, key=456         text = "$title: $count" ,         onClick = { count++ }      )      $composer.end }

Что здесь происходит? У нас здесь есть composer, вы можете на него ссылаться, как доступ к Gap buffer. И именно поэтому только в composable-функции могут быть composable-функции: им всем нужен Gap buffer, им нужен scope, где они сейчас находятся, нужна текущая позиция Gap buffer.


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


Также мы в начале вставляем в начале функции composer.start и в конце composer.end.


Посмотрим, что произойдёт, если возьму эту функцию и применю к UI.



Gap buffer может быть больше, а это просто отрезок, где мы сейчас находимся. Мы вызываем функцию Counter и идём по ней:
Мы говорим: у нас есть start, кладём туда такую штуку, как Group($key).
Потом идём на следующую строчку, и там есть key = 123, это еще одна Group(123).
После этого, как я уже говорил, state генерирует обёртку с классом state со значением 0.
Потом у нас есть Button, где есть key = 456, кладем туда Group = 456.
У Button есть параметры, мы кладём их в slot table.
Естественно, у Button внутри тоже есть имплементация, это же не просто компонент. Внутри него проходят операции, у него есть текст и клики.



Так будет выглядеть наш Gap buffer, когда мы покажем этот Counter. И если мы посмотрим, как я и говорил, то даже линейные виртуальные репрезентации всё равно показывают это дерево, которое у нас и является UI. У нас есть Counter это вся штука, которую мы построили, у нас есть State и остаток Button.



В каком-то плане мы строим call stack-функцию в этом случае. Причём запоминая её параметры и уходя внутрь, то есть это depth first traversal-итерация по дереву.


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


Давайте представим, что мы обновили в этом Counter Mobius на Matvei или на что-то ещё. Новый заголовок title приходит в новый Counter изолированно. Мы знаем, что title начинается с Group(key), идем туда. Смотрим, что key=123 не поменялся, значит, state не поменялся, то есть возвращаем вверх тот же самый state, который был у нас до этого. key=456 тоже не поменялся, смотрим на text он поменялся. Лямбда не поменялась, но так как у меня text, а он идет после ключа button, то есть является параметром Button, нам нужно зайти внутрь Button и тоже поменять его.



Всё связано таким образом, и мы можем понять, что у нас поменялось. Ключи дают возможность определить, та же самая это кнопка или нет. Если она та же самая, можно пойти внутрь и обновить её. И мы делаем императивный flush, перерисовывая нужные части UI, которые обновились.


Это простой пример, посмотрим на что-то сложнее. Как уже говорил, с Compose мы можем делать if else, for и прочие вещи. Мы не рассмотрели примеры, когда я куда-то двигаю Gap buffer.


@Composable fun Feeds(items: List<FeedItem>) {    if (items.isEmpty) {          Text("No feeds")      } else {         FeedList(items)     } }

У нас есть Feeds список каких-то вещей, чатов и чего угодно. Эта Composable-функция принимает List<FeedItem>, и он может быть пустой. Нужно показать какую-то нотификацию: no feeds, создать чат, какой-то UI.


Compose сгенерирует нам ключи с помощью Composable-аннотации, которые мы положим в buffer.



Элементов нет, идем в if (isEmpty == true) и попадаем в 123, ставим Group(123) и текст. Дальше не идем. Потом идем в базу данных, в интернет, и у нас появляются Item: новый чат или что-то еще. Снова вызываем функцию Feeds и попадаем в else statement, потому что у нас есть чат. И кажется, что 456 не равно 123.


Двигаем Gap за линейное время: это довольно длинная операция, но мы понимаем, что UI поменялся, нужно пересоздать дерево. Старое дерево нам уже не нужно, можем его удалить. Берем Group(456) и ставим новые Feeds. Таким образом мы можем создавать conditional-логики в нашем UI. Перестраиваем полностью под дерево, потому что if стал else, все поменялось.



Ключи это не 123 и 456, мы не берём их с неба. Они зависят от местоположения в файле вашего Composable. Если у нас будет еще один text, то у него будет другой key внутри if, и мы будем генерировать эти цифры каждый раз для новой встречи в коде компонента. Это называется positional memoization. Это core-принцип в Jetpack Compose, потому что нам нужно понимать что, как и когда перезвать.


В positional memoization главная штука называется remember. Это функция, которая может принимать набор параметров, например:


@Composable fun FilterList(items: List, query: String) {   val filteredItems = remember(items, query) {    items.filter(it.name.contains(query))   }   FeedList(items) }

В примере эта функция принимает items и query и лямбду, которую она будет вызывать. Мы делаем фильтрацию списка, у нас есть items [A, AB, BC], есть query, который мы вводим в поиске, A. Мы берем и кладем их в slot table. Она зеленая, значит, это тоже Composable-функция. И мы говорим, что в items у нас лежат [A, AB].



Если мы призовем этот FilterList, например, когда поменялись параметры, то если items, query те же самые, мы можем пропустить вычисление и вернуть сразу [A, AB].


Даже если у нас будет два разных FilterList с одинаковыми items, у них все равно будет свой remember, потому что они будут объявлены в разных местах в коде, у них будут разные ключи и разные remember.


Интересно, что у remember может не быть параметров. И мы можем сделать такую штуку:


@Composable fun Item(name: String) {      val result = remember {      calculation()      }      Text($name: $result) }

Пример, в котором calculation() является math.random(). Если я сделаю список этих item, у каждого item будет свой calculation, свой результат, и он сохранится на весь жизненный цикл этого item, даже если мы будем менять у него имя. Мы берём remember, у него нет параметров, то есть мы всегда возвращаем тот же самый calculation. Но мы можем пересчитать его только в первый раз, потому что тогда его не было в Gap buffer, а теперь он есть, и мы просто возвращаем его.


Все эти Composable-функции это примерно то же самое. То есть мы берём параметры:


  • Поменялись идем внутрь функции;
  • Не поменялись возвращаем то же самое.

Стейт


Помните, я рассказывал вам о том, что у нас есть Counter, и мы можем там создать state?


@Composable fun Counter(title: String) {     var count by state { 0f }     Button(         text = "$title: $count" ,         onClick = { count++ }     ) }

У state тогда был ключ 123, и после того, как мы возвращали, тот же самый state при апдейте counter. Все потому, что state внутри выглядит как просто remember. Это remember от функции MutableState. И функция mutableStateOf сделана примерно как MutableListOf в Kotlin.


@Composable inline fun state(init: () -> T): MutableState =      remember { mutableStateOf(init()) } fun mutableStateOf(value: T): MutableState {  }

Идея в том, что именно в функции state мы ремемберим MutableStateOf, мы можем понять, что он тот же самый. И если мы будем менять title у кнопки, state останется тем же самым. То есть я могу поменять title с Mobius на Matvei с counter = 14: title поменяется, а counter останется 14.


Интерфейс MutableState очень простой, наследуется от state.


interface MutableState<T> : State<T> {      override var value: T } interface State<T> {     val value: T }

Все, что делает MutableStateOf, создает объект MutableState. Он принимает Initial, чтобы мы могли правильно создать его. Это не просто обертка, которая содержит класс с <T>, хотя для нас он так и выглядит.


Мы создаём такой инстанс MutableState, который позволяет отслеживать ваше прочтение и потом обновлять только прочитавших. Это не нужно в идеальном мире, потому что в Compose мы умеем сами пробежать по Gap buffer и понять, что происходит. Но для важных, интересных кейсов эта вещь позволяет нам обновлять прочитавших или, по-другому говоря, совершать scoped observations, когда мы можем понять, что и в каком scope произошло.


Разберем на примере.



Вот наш Counter с нашим сгенерированным slot table. Мы не будем создавать здесь этот state с помощью функции state, а перенесем его в лист параметров. Gap buffer поменяется.



Мы не будем говорить о том, хороший этот подход или плохой. Приходит Counter, и мы можем его менять. Если вы сгенерировали и передали MutableState, мы можем понять, что где-то в text есть state.value, мы прочитали value из MutableState. Мы прочитали это внутри scope Counter. Запомним это, обновляем только Counter и ничего кроме.



Давайте посмотрим, почему это здорово. Рассмотрим такой пример:



У нас есть три кнопки, у них есть свои counter и кнопка Increase all, которая будет инкризить все counter. Один из вариантов сделать MutableState параметром counter, чтобы мы могли контролировать все. Мы можем создать App:


fun App(counterData: Map<String, MutableState<Int>>) {   Column {       counterData.forEach { (title, counterState) ->       Counter(title, counterState)       }       Button(        text = "Increase All!",        onClick = {        counterData.forEach{ (_, countState) ->             countState.value++         }        }       )     } }

После этого я говорю, что у нас есть Column в Compose, и завожу там counterData.forEach. И отдаю ему title (Mobius, Matvei или Memoization) и counterState нашему counter. После этого добавляем кнопку Button, которая будет по клику обновлять все counters, которые у нас есть. И у нас есть counters, которые работают отдельно, и Increase all, который обновляет все.


Мы помним, что у нас MutableState, и мы умеем это все обновлять скоупами. Мы построили примерно такое дерево:



Можете представить себе, что этот пример очень маленький, и помимо counter внизу, у нас может быть их много UI в середине. Increase all может быть далеко в штуке, у которой тоже есть доступ к MutableState.


В этих counters мы делаем count.value, который прочитали в каком-то скоупе этого counter. А button просто делает Increase all forEach по it.value. Запись в MutableState ни на что не обрекает тебя, тебе не нужно перерисовывать все, если ты обновляешь.


Кликаем на Increase all. В этом случае, когда мы понимаем, что value изменились, мы не бежим по всему дереву, от parent, от владельцев этих MutableState. Нам нужно понять, что все было это было прочитано внутри скоупов counter, и обновить только их. Если мы обновили только один counter, обновится только один. Если все три, то обновляются все три с помощью forEach. Мы избавимся от всех итераций по всем деревьям и просто обновим эти поддеревья, начиная с самого близкого, который прочитал.



Это возможно, потому что у нас есть compiler-плагин, который позволяет получать эти крутые штуки.


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


State<T> &MutableState<T> все от нас прячут, мы говорим state.value и получаем текущее значение. Наша Composable-функция будет сама реализована, нам ничего не нужно для этого делать.


Также очень удобно, что можно создать новое значение одной строчкой, быстро создать composition-bound state внутри Composable-функции.


Интересно, что в Compose никуда не уходят стадии layout, redraw, они спрятаны для нас, есть только композиция. Но мы можем понять, где вы это читаете. Если мы прочитали это в композиции, как мы это сделали в counter, то мы перекомпозируем вас, перезовем всю функцию. Но если вы прочитаете value только в layout, как делает скроллер, то мы только перелейаутим и не будем вызывать перекомпозицию.


Если вы, например, рисуете какой-нибудь чекбокс, и читаете value только в стадии рисовки, мы перерисуем только ваш canvas. Это позволяет делать супероптимизированные вещи, которые, возможно, вам нужны.


Важно понимать, что Shared MutableState<>, который мы сделали, не бесплатный. Если вы принимаете MutableState как параметр компонента, то компонент сможет менять state родителя. Станет очень сложно следить за общим состоянием системы, и мы рискуем напороться на то, за что боролись. Мы хотели, чтобы все стало проще с помощью фреймворков, но нас заставляют понимать, куда ты отдал свой state.


Пример:


@Composablefun Counter(    countState: MutableState<Int>) {    Button(      text = "count: ${countState.value}",      onClick = { countState.value += 1 }      ) }

И мы его вот так используем:


@Composable fun App() {     val counterState: MutableState = state { 0f }     Counter(counterState) }

И когда я отдаю этот state в Counter, я прощаюсь с этим state и не знаю, что с ним произойдет. Я не контролирую, что произойдет внутри Counter. Его могут положить куда-то, кому-то отдать. Это усложняет систему, но дает классные штуки.


Решением этого может быть state hosting, когда мы поднимаем state выше, а владельцем становится какой-то parent, например, App. Button в этом случае будет принимать State только для чтения, а не MutableState, как раньше.


Второе решение Controlled components это компоненты, у которых нет state, но есть snapshot этого state.


@Composable fun Counter(     currentCount: Int,     onCountChange: (Int) -> Unit ) {     Button(        text = "count: $currentCount",        onClick = { onCountChange(currentCount + 1) }        ) }

Мы можем зарефакторить наш Counter так, чтобы он принимал currentCount и лямбду, которую он звал бы на onCountChange. Если мой текущий Count = 7, то я попросил бы кого-нибудь обновиться на 8. Это то, что мы делаем в чекбоксах, текстфилдах и прочем. Это то, почему очень сложно иногда писать приложение на андроиде, когда есть edit text, и нам хочется что-то поменять, потому что MutableState везде шарится. Тут же counter принимает только snapshot и предлагает кому-то поменять значение.


@Composable fun App() {   val counterState: MutableState = state { 0f }   Counter(       currentCount = counterState.value,       onCountChange = { counterState.value += 1 }      ) }

У меня есть MutableState, мы говорим в App: вот тебе текущее значение. App обладает state и никому не отдает его. По взгляду на App сразу можно понять, что происходит. Именно поэтому вы можете превратить val counterState с помощью делегатов в var. И в этом случае у вас вообще нет доступа к MutableState. Если вам не нужна обертка, то это, возможно, лучший способ создавать state.


@Composable fun App() {    var counterStateValue: Int by state { 0f }    Counter(        currentCount = counterStateValue,        onCountChange = { counterStateValue += 1 }        ) }

Важно понимать, что в этом случае мы теряем возможность перелейаутить, перерисовать или переобновить какой-то scope. Но гибкость = ответственность.


MutableState<T> это классная штука, но если вы используете его в публичных API, оно ведет к разделению владения state. Альтернатива, о которой я уже говорил, это controlled components.


Вторая альтернатива принимать State<T> только для чтения. То есть, если вам все еще нужно, чтобы ваш Counter очень умно скоупом поглощал MutableState<T>, вы можете отдать ему этот State<T> и читать его там, где вам нужно. Все будет так же, мы все перекомпозируем, перелейаутим, перерисуем. Просто State<T> не дает записать это туда, потому что там val.


Вы можете сделать так и все также через control input вызывать лямбду onCounterChange, чтобы другие люди могли обновлять этот State<T>.


Другая альтернатива иммутабельные классы и структуры данных, с ними таких проблем нет. И вообще MutableState использовать необязательно. Вы можете использовать иммутабельные классы, передавать их как параметры в ваши Composable-функции.


Вместо выводов


Декларативные UI-фреймворки абстрагируют обновления UI, дают нам UI как функцию от состояния и быстро обновляют его внутри себя.


На основе этих выводов можно понять, как работает любой такой фреймворк: React, Jetpack Compose и остальные.


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


Ссылки


Главная
Туториал
Багтрекер
Kotlin slack, #compose channel


Если доклад Матвея с Mobius вам интересен, вероятно, вам будет интересно и на новом Mobius, который пройдёт 13-16 апреля.

Там будет и доклад о проекте Compose for Desktop, и много другого для Android-разработчиков: про Gradle, корутины в Kotlin и так далее. Полную программу можно посмотреть на сайте конференции.
Подробнее..

Тестирование в эпоху ИИ

09.02.2021 16:13:11 | Автор: admin

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

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

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

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

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

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

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

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

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

Я неслучайно сказал изобретена. До этого не существовало специально нанятых людей, ответственных только за тестирование. Во времена мейнфреймов им попросту не занимались. Если у пользователя падала программа значит, пользователь сам виноват. На все проблемы был один ответ: обучайте пользователей. У IBM, DEC и подобных им корпораций были огромные ресурсы, и они специально занимались этим обучением.

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

Новые баги

А потом мы столкнулись с проблемой Y2K. Многие из вас, наверное, тогда еще не родились, или же были совсем маленькими. Когда все часы перешли с 1999 года на 2000, тестировщики внезапно оказались у всех на виду, все заговорили о баге тысячелетия, хоть это был и не баг, а конструктивный недостаток. Создатели ПО в своё время приняли сознательное решение использовать для хранения даты две цифры, поэтому 2000 год компьютеры принимали за 1900. Мы сейчас не будем подробно обсуждать это событие, для нас сейчас важно вспомнить, что тестирование софта в тот момент внезапно оказалось в центре внимания всего общества и попало в новости. К тому времени бумага и чернила остались в прошлом, данные либо проходили оцифровку, либо возникали уже в цифровом виде. В соответствии с этим выросла и роль тестирования.

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

На слайде можно увидеть два таких бага. Тот, который слева, назывался The Brain. Он считается первым вирусом для MS-DOS. Он перезаписывал загрузочный сектор и замедлял компьютер до невозможности. Создатели этого бага два пакистанских программиста. Им не нравилось, что их софт копируют пираты, и в отместку они решили испортить компьютеры пользователей. Их программа была сознательно создана с целью не дать людям работать. Произошло это в 1986 году. Именно тогда стала очевидной возможность существования вирусов, уязвимостей и прочих вещей, которые сейчас кажутся данностью. Как с этим стали бороться? Вирус The Brain попал в десятки стран и заразил тысячи компьютеров. Он был доказательством того, что софт мог быть написан злоумышленниками и быть вредоносным.

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

Если я не ошибаюсь, то AOHell возник в 1994 году. Вообще, в 1990-е годы появился целый ворох новых злонамеренных штук: вирусы-черви, вредоносные программы, программы-шантажисты, интернет-травля, порноместь и многое, многое другое. А причина этого была в том, что при разработке софта не учитывались соображения качества и безопасности. Софт писали для того, чтобы с его помощью делать деньги. И именно тестировщики первыми в 1990-е годы сказали: чёрт возьми, нам всем нужно остановиться, отдышаться, и разобраться в том, что происходит технология явно развивается как-то неправильно. Конечно, остановиться и отдышаться нам никто не дал. Но возникло и стало бурно развиваться новое поле деятельности тестирование и исследование тестирования. Возникли инструменты, в которых отрасль остро нуждалась. Люди вроде меня стали писать книги и создавать новые технологии. Моя первая компания занималась именно тестированием ПО. Мы пытались убедить мир в том, что, во-первых, софт изменит планету, а во-вторых, у него есть серьёзные недостатки. Мы выступали на конференциях, которые превращались в площадки для споров, и с некоторыми из тогдашних оппонентов я спорю до сих пор. Мы понимали, что дело того стоит.

От софта к ИИ

Сегодня мы стоим на пороге ещё одного перехода: от софта к ИИ. Я думаю, что он будет не менее важен, чем переход с бумаги на софт. Сейчас повторяются многие процессы, которые уже произошли 30 лет назад. В те годы многие считали, что софт глобально ничего не изменит. IBM продолжали выпускать печатные машинки и где теперь эти машинки? DEC вообще сошли со сцены из-за своей недальновидности. Сколько было компаний, которые продолжали полагаться на старые технологии и не предвидели грядущих изменений? Sun Microsystems, Unisys, Sperry Rand.

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

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

В определённый момент мы, тестировщики, поняли, что софт, в сущности, устроен довольно просто. У него четыре основных компонента:

  1. входные данные

  2. выходные данные

  3. структуры данных

  4. вычисления

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

Я написал целых пять книг об этих вещах:

How to Break Software

How to Break Web Software

How to Break Software Security

How Google Tests Software

Exploratory Software Testing: Tips, Tricks, Tours, and Techniques to Guide Test Design

Как тестировщики анализируют область входных данных? Как тестировщики изменяют входные данные? Как они угадывают, какие из возможных входов могут привести к сбою в софте? Как они определяют, какие из возможных входов, наиболее вероятно, будут применяться в определённых условиях? Как они добиваются нужного выхода или того, чтобы произошли нужные вычисления? Об этом и написаны эти книги. Самая последняя из них вышла в 2011 году, то есть почти десятилетие тому назад, самая первая в 1999, и она по-прежнему переиздаётся. Эти книги такие долгоиграющие, потому что они представляют из себя фундаментальный анализ того, как работает софт, а следовательно и того, как он перестаёт работать.

Устройство ИИ

Существует ли на сегодняшний день подобный анализ ИИ? Ведь проделать такую работу это редчайшая возможность! Я начал заниматься софтом на такой ранней стадии, когда ещё можно было написать книгу, которая продолжала бы издаваться через 20 лет. Если бы это был автомобиль, то за ним охотились бы коллекционеры, и за него платили бы в 10 раз больше изначальной цены. Аналогичного фундаментального анализа ИИ пока нет. Давайте попытаемся к нему приступить. Давайте применим к ИИ те же методы, которые мы применили в своё время к софту.

Чем является ИИ на наиболее фундаментальном уровне? Вначале я постараюсь дать краткую формулировку, а потом приведу два примера того, как устроен ИИ.

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

Как же дело обстоит с ИИ? В каком-то смысле ИИ, возможно, и является софтом это бинарный код, он выполняется на микропроцессоре; но уже здесь мы сталкиваемся с принципиальным отличием. Если спросить у прохожего на улице, как вы себе представляете ИИ, то обычно у людей возникает образ чего-то человекоподобного. Но сейчас у нас перед глазами есть значительно более реалистичные примеры: Siri, Alexa, Google Home. И первое, на что мы обращаем внимание, это отсутствие пользовательского интерфейса. У ИИ нет GUI, который можно было бы протестировать. Мы разговариваем с ИИ. Компания, в которой я сейчас работаю DefinedCrowd занимается созданием наборов данных для ИИ, переводом текста в речь и взаимодействием ИИ с речью. ИИ начинается с данных.

И здесь кроется принципиальное отличие: если софт программируется строчка за строчкой, то ИИ обучается. Попробуйте написать код, который управлял бы автомобилем на перекрестке. У вас ничего не выйдет. Вы не сможете исчерпать все возможные сценарии операторами if-else, case и switch. Управлять автомобилем с помощью софта невозможно; а вот обучить автомобиль езде можно. Можно показать автомобилю сотни, тысячи, миллионы наборов данных, представляющих ситуации на перекрестке, и дать ему разобраться в них. Обучение ИИ похоже на обучение людей. Как научить человека водить машину? Нужно вместе сесть в автомобиль и дать возможность попрактиковаться.

Итак, первый компонент работы ИИ это данные. Второй метки на данных, помогающие машине понять эти данные. Мы даём ИИ 10 000 изображений кошек и помечаем каждое на этом изображении есть кошка. А потом мы даём 10 000 изображений без кошек, и помечаем их на этом изображении кошки нет. Со временем ИИ начинает отличать одни от других.

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

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

Мы подошли к следующей составляющей ИИ: алгоритмам. Я думаю, что в будущем в университетах будут изучать не языки программирования, а алгоритмы. Некоторые из этих алгоритмов существуют уже столетия. Всем алгоритмам, относящимся к байесовской статистике, уже 200 лет, и они относятся к наиболее фундаментальным из тех, которые используются для обучения ИИ. Аппроксимация кривой, метод наименьших квадратов, регрессия всё это довольно простые алгоритмы, которые уже давно себя зарекомендовали. Я думаю, что в будущем уже не будут важны навыки работы со структурами данных, контрольными структурами, операторами if, switch, lookup table, умение выбрать между циклом while и for. Гораздо большим спросом будут пользоваться теория вероятности и статистика. Если бы вы спросили, какая именно теория является основополагающей для ИИ, то я бы ответил статистика и стохастические процессы. У истоков истории исследования ИИ стоят имена вроде Маркова и Байеса.

Итак, работа ИИ начинается с данных, данным присваиваются метки, а затем эти данные передаются алгоритмам. Алгоритмы третий компонент работы ИИ. Я сейчас не буду подробно рассказывать об алгоритмах, потому что всё-таки мы обсуждаем тестирование, а не теорию ИИ. Скажу лишь, что есть статистические алгоритмы, и есть стохастические (то есть статистические со структурой). Ваш соотечественник Марков является одним из основателей теории стохастических процессов. Кроме того, существуют модели с уровнями, модели глубокого обучения. О них мы поговорим чуть позже. Это три основные класса моделей. И в них нужно будет очень хорошо разбираться. Умелому специалисту по анализу данных достаточно одного взгляда на данные, чтобы выбрать правильный алгоритм для них.

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

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

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

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

Обучение ИИ

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

В DefinedCrowd у нас есть команда QA, очень способная, но их деятельность ещё не прошла ту стадии документации, которую в своё время прошёл софт. Каждый день мы решаем новые проблемы и изобретаем новые способы мышления. И это очень похоже на то, что происходило в девяностые. Тогда существовало две важных конференции: Quality Week (она прекратила своё существование, кажется, в 2000 после пузыря доткомов), и STAR (она сменила собственников, но по-прежнему существует). Сейчас STAR выглядит довольно стандартно: эксперты поучают чайников. Но в девяностые всё было не так. Тогда мы постоянно делали открытия. Мы приходили на конференции со сложнейшими проблемами, и существовали специальные семинары, на которых мы совместно их решали. Мы рассказывали о чём-то, что не могли решить самостоятельно, делились опытом, а по результатам писали статью. Такие же конференции нужны сейчас в ИИ.

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

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

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

В этом ключевое отличие: софт программируется, а ИИ обучается, как человек. Как учится читать ребенок? Ему снова и снова показывают карточки с буквами, если он угадывает правильно, ему говорят молодец! Если ошибается, просят попробовать снова. Так же и с ИИ. Ему показывают ряд примеров, и если он совершает ошибку, то меняют метку, чтобы он знал, что произошла ошибка. Это обучение с подкреплением.

Другая технология, сделавшая беспилотные машины возможными сегодня, и отсутствие которой не давало создать их 10 или 20 лет назад это облака. Облачные технологии перевернули всё вверх дном в мире ИИ. Сами по себе облака существуют уже довольно давно, просто раньше они назывались дата-центрами. У Google было проприетарное облако, которое занималось одним только поиском, у Amazon коммерцией, у Facebook хранением социальных данных. И именно в этих облаках родился современный ИИ.

Облачные хранилища смогли удовлетворить ненасытную потребность ИИ в данных. Представьте систему, которой для обучения нужно 100 миллионов изображений кошек. Или ей нужны видеозаписи со множества камер, на которых записаны автомобили на перекрестках. До появления облачных технологий алгоритмам просто негде было взять все эти данные для обучения. Поэтому неудивительно, что первыми ИИ, вошедшими в широкое употребление, были ИИ Google, Facebook и Amazon. Причём тогда никто не знал, что это ИИ, все думали, что это просто софт. Amazon были первыми, кто решил, что ИИ можно использовать не только для их специализированной области, но и для решения других проблем. Так и вышло, что они сделали первый шаг в будущее. За это нам следует быть благодарными им. Без облачных технологий ИИ бы не существовало, потому что негде было бы хранить данные.

Давайте теперь поговорим о проблеме подсчёта числа человек в комнате. Мне кажется, что решение этой проблемы во многом показательно. Я работал над ней в прошлом году в стартапе Biscuit Labs, занимающемся IoT. Сейчас эта проблема привлекла особенное внимание из-за пандемии, потому что были введены ограничения на допустимое количество человек в помещении. Давайте подумаем, как бы мы использовали ИИ для решения этой проблемы мне кажется, это очень полезное упражнение.

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

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

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

Выглядело это следующим образом. Мы передали алгоритму данные датчика движения, и сказали: в этот момент в комнате было 4 человека; передали данные точки доступа и сказали: 18 человек; передали запись с микрофона и так далее. В качестве данных мы также использовали показания из мест, где собираются тысячи человек, и в качестве меток использовали число проданных билетов. Другие данные мы собирали в офисах, где редко было больше 12 человек, и где люди постоянно перемещались с места на место. И на размеченных таким образом данных мы обучали систему.

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

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

Как этому алгоритму удалось, чёрт возьми, определить, что в комнате 19 человек? Лично я понятия не имею. Тут дело не в коде, а в данных, потому что это ИИ, а не софт. Я не могу указать на определённый участок в коде и сказать: вот что позволило ему решить проблему. С софтом это всегда можно сделать. Всегда можно найти определённое место, в котором возник баг. Можно понять, почему именно алгоритм работает, или не работает. С ИИ мы пока так делать не умеем. Но, если я не ошибаюсь, 25 лет тому назад мы этого не умели делать и с софтом. С того времени возникли соответствующие инструменты. Такие же инструменты, обеспечивающие контролируемость, нам теперь необходимо разработать для ИИ. Потому что ИИ уже демонстрирует те же симптомы, что и софт в 1986 году, когда появился вирус Brain.

Проблемы ИИ

Приведу два примера этих симптомов.

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

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

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

Тут непочатое поле работы для тестировщиков. Мы, тестировщики, по природе своей люди с деструктивными наклонностями. Мы ищем прорехи, уязвимости, о которых никто другой ещё не подумал. Именно поэтому мы идеально подходим для этой работы. Наше воображение настроено на то, чтобы генерировать сценарии подобного рода катастроф. Мы прекрасно умеем разматывать клубки причинно-следственных связей, чтобы понять, почему, чёрт возьми, они происходят. Нам это нужно как воздух; я до сих пор помню первый найденный мною баг и первый написанный мною код. Я был рождён, чтобы ломать вещи. Итак, как определить, есть ли систематическая ошибка в данных? Пока что я не могу дать вам ответа на этот вопрос, точно так же, как в начале девяностых я не мог бы ответить на фундаментальные вопросы о тестировании софта.

Взглянем теперь на второй пример на слайде. Ещё один заголовок: Чатбот от Microsoft пропагандирует расизм и геноцид в Twitter. Бот называется Tay, создан в Microsoft Research. Здесь мы сталкиваемся с другой проблемой. Tay обучался на человеческой речи. Он учился не просто говорить, а вести диалог с людьми. То есть это чертовски сложная система, гибрид стохастического процесса и алгоритма глубокого обучения. На её тренировку было потрачено очень много времени. И всего через 20 минут после того, как боту дали в распоряжение аккаунт в Twitter, он объявил, что Гитлер был героем, и стал пропагандировать расизм и геноцид.

В случае с системой распознавания лиц баг был в том, как эту систему обучали; здесь искажения в исходных данных не было. Проблема крылась в цикле обратной связи. Видите ли, ИИ продолжает обучаться после того, как работа над ним завершена. Этот процесс похож на воспитание ребенка если у вас есть дети, вы поймёте, о чём я. Вы можете пытаться привить ребёнку какие-то ценности, научить делать правильный выбор, но после того, как ваше чадо покинуло дом, вы его уже не контролируете. Оно попадает под воздействие своих сверстников, телевизора, чёртовых Кардашьян и иже с ними. Как вы думаете, чему человек у них научится? В общем, выяснилось, что Tay, несмотря на хорошее воспитание, вырос порядочным негодяем, и его пришлось отключить.

А ведь мы сталкиваемся с подобного рода проблемами при тестировании софта. Например, в одной из моих книг говорится об атаках, манипулирующих состоянием (stateful attacks). Софт со временем может накапливать определённое состояние. Все мы пользовались сочетанием Ctrl+Alt+Delete, сбросом системы. Аббревиатура SRE означает simply reset everything (просто всё сбросить). Таким образом мы избавляемся от текущего состояния.

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

Где используется ИИ

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

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

Что дальше?

На дворе 2020 год. Мы уже давно должны были научиться тестировать системы ИИ. Нам нужно уметь обеспечивать справедливость, этичность и точность искусственных адвокатов. Мы должны уметь проверять квалифицированность искусственных врачей, проверять, что они знают что делают. До 2030 года всего 10 лет. К этому времени ИИ достигнет такого уровня, на котором возникнут качества, до сих пор принадлежавшие только людям. Ведь мы сознательно приближаем ИИ к человеку, учим выполнять работу человека.

Настанет день, когда мы не сможем отличить людей от ИИ. Alexa, Google Home и подобные им системы достигнут небывалого уровня, потому что каждую минуту они совершенствуются. И все эти бесчисленные Алексы, Сири и прочие умные колонки обладают единым мозгом. Они все взаимосвязаны. Что знает одна система, то знают все. Далёк ли тот день, когда мы не сможем отличить голос умной колонки от голоса реального человека? Думаю, этот день настанет где-то в 2030-е годы.

А самое интересное, как мне кажется, произойдёт ещё позже, в 2040-е. Быть может, настанет день, когда эта сеть, этот искусственный мозг, станет настолько сложным, что он обретёт сознание. На мой взгляд, из всех существующих в нейробиологии теорий о том, как и почему существует сознание, наиболее убедительной является та, которая связывает сознание со сложностью нейронных связей. Когда целое становится больше суммы составляющих его частей, возникает сознание. Может ли это произойти с машинами? Будут ли эти машины выглядеть, как мы? Будут ли они обладать правами? Правом голоса? Такие машины будут бессмертными. Это будет новый вид, который превзойдёт людей. Признают ли они в нас своих создателей? Станут ли поклоняться нам? Лично я в этом сомневаюсь. Скорее всего, они будут обладать самостоятельным разумом.

Меня зовут Джеймс Уиттакер. Это был необычный опыт. Представьте: говорить с маленьким огоньком в камере, и знать, что вас слушает множество людей в России и по всему миру. Но я надеюсь, вам понравилось. Мир вам.

Мы сейчас выложили на YouTube видеозаписи не только этого выступления, а вообще всех докладов с последнего Heisenbug так что, если вы связаны с тестированием, наверняка найдете в плейлисте что-то подходящее вам. А следующая конференция Heisenbug 2021 Piter пройдет с 6 по 9 апреля.

Подробнее..

Jenkins, покрытие кода, байткод и девопс что будет на Luxoft TechFest 4

28.05.2021 10:14:13 | Автор: admin

3 июня пройдёт Luxoft TechFest #4: бесплатное онлайн-мероприятие с тремя докладами по Java и DevOps. Под катом полные описания докладов и другая информация о мероприятии, а сначала суть вкратце для тех, кто торопится:

  • Олег Ненашев разрабатывает Jenkins. И расскажет, как в 2021-м правильнее анализировать code coverage с его помощью.

  • Евгений Мандриков разрабатывает JaCoCo (инструмент, связанный опять же с code coverage). А поговорит о том, как компиляторы Java, Scala и Kotlin преобразуют исходный код, и в чём разница полученного от них байткода.

  • Александр Селезнев уже четыре года вовлечён в DevOps. Но у него будет не тысячный доклад как надо применять DevOps, а наоборот: речь пойдёт о том, как НЕ надо и к каким провалам это может приводить.

Программа

18:00 Приветственное слово

18:05 Scala, Kotlin, Java и Code Coverage: показать все, что скрыто (Евгений Мандриков)

Знаете ли вы, что Scala-компилятор помещает внутрь ваших class-файлов? А чем отличается байт-код, производимый Scala-компилятором, от байт-кода, производимого Java и Kotlin компиляторами? А готовы поспорить?

В этом докладе Евгений поделится исследованием конструкции байт-кода и изучит, как Scala-компилятор и другие преобразуют исходный код. За основу будет взята реализация самого популярного инструмента для анализа покрытия байт-кода тестами JaCoCo.

О спикере: Участник проектов с открытым исходным кодом. Спикер международных конференций. Ведущий разработчик в проекте JaCoCo и лид в отмеченном наградами проекте EclEmma, который интегрирует JaCoCo в Eclipse IDE. В SonarSource Евгений разрабатывает статический анализ исходного кода для Java, C/C++, C#, JavaScript.


18:55 Продвинутый анализ Code Coverage с Jenkins (Олег Ненашев)

В 2016 на конференции Heisenbug Олег рассказывал, как с помощью Jenkins Pipeline, библиотек и сторонних плагинов анализировать тестовое покрытие исходного кода. Сейчас многие типовые задачи решаются плагинами из коробки. В этом докладе речь пойдет о современных подходах к code coverage в Jenkins.

Олег рассмотрит Jenkins Pipeline, Code Coverage API Plugin, поддержку форматов Cobertura, JaCoCo и gcov, параллелизацию тестов и интеграцию с GitHub Checks API и другими сервисами. Он расскажет, как анализировать code coverage в Jenkins, как он помогает с code review и возможно ли использовать преимущества code coverage в Jenkins без самого Jenkins.

О спикере: Разработчик в CloudBees. Состоит в core-команде проекта Jenkins. C 2008 года занимается автоматизацией, инфраструктурой и фреймворкостроением для крупных программно-аппаратных проектов с помощью Jenkins и десятков других инструментов. Пишет код, поддерживает ядро и плагины Jenkins, организует митапы в Питере и других городах.


19:45 Карго-культ вокруг DevOps: Как навредить проекту из лучших побуждений (Александр Селезнев)

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

Доклад затронет:

  • Что такое DevOps на самом деле и кому он нужен (а кому нет);

  • Провал 1 Infrastructure as Code;

  • Провал 2 Kubernetes;

  • Провал 3 CI/CD/CT.

О спикере: Работает в Luxoft. Прошел путь от инженера компьютерного класса до релиз-менеджера. Последние 4 года активно занимается DevOps во всех его аспектах: от решения технических задач до трансформации проектов.

20:35 Закрытие


Как принять участие

Митап пройдет онлайн 3 июня, 18:00 (МСК).

Чтобы принять участие, нужно:

  • Зарегистрироваться на сайте;

  • Перейти по ссылке в письме от Личного Кабинета JUG Ru Group;

  • Войти в личный кабинет (или создать новый);

  • Перейти по ссылке на митап.

Будем рады вас видеть!

Подробнее..

Microsoft Coffee первоапрельский ответ на Java

04.04.2021 14:14:58 | Автор: admin

На днях в интернете появился любопытный пост. Там утверждается, что в 1996 году несколько сотрудников Microsoft втайне от руководства устроили первоапрельский розыгрыш. Они распространили по магазинам Сиэтла коробки с несуществующим продуктом Microsoft Coffee, намекающим на Java. Возник резонанс, на местном телеканале вышли новости, но PR-отдел и Билл Гейтс шутку не оценили. Поэтому компания пыталась вычислить организаторов, а публично всё отрицала и замяла историю, так что все эти 25 лет о ней никто не вспоминал.

История забавная, но есть нюанс. Опубликована она 1 апреля 2021 года. И возникает вопрос, как же всё на самом деле: действительно ли в 1996 году происходили описанные события, или подобного не было, а сам этот новый пост и есть первоапрельская шутка?

Если и шутка, то удивительно проработанная. Я собрал тут все подробности: и что сказано в посте, и что говорит за/против его подлинности.

Содержание поста

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

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

Незадолго до этого, в январе 1996-го, компания Sun Microsystems выпустила первую версию Java. А у Microsoft была репутация компании, копирующей успешные чужие решения. И это решили обыграть, придумав майкрософтовскую Java. Поскольку слово Java в США используют для обозначения кофе, вымышленный продукт незамысловато назвали Microsoft Coffee и сделали ему похожий логотип с кофейной чашкой.

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

Происходило всё так. 1 апреля 1996-го коробки с выдуманным продуктом поступили в несколько магазинов Сиэтла, а из офиса Microsoft местным СМИ разослали его пресс-релиз. В местных теленовостях обратились с уточнениями к пресс-службе (которая была не в курсе происходящего), получили ответ нет, мы такого не выпускали и осветили это именно как первоапрельскую шутку:

Казалось бы, раз в СМИ это не приняли за чистую монету, то и урона компании не нанесено ну, шутка и шутка. Но официальная позиция Microsoft оказалась не в том, чтобы поддержать розыгрыш публично (да, это сделали мы или это не действия компании, но кто-то из наших сотрудников, и это забавно). Вместо этого компания повела себя противоположным образом: изъяла из магазинов все коробки, заявила прессе, что это сделали какие-то посторонние люди не из компании, и начала внутри поиск виновников.

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

Правда или нет?

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

Аргументы против:

  • Коробки не всплывали на eBay. Пусть компания изъяла и уничтожила большинство, но раз в новостном сюжете такую показывали в кадре значит, сколько-то всё же попало в руки людям. И даже в самом этом сюжете произносят, что у них должна появиться коллекционная ценность.

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

  • Автор поста вроде как решился спустя 25 лет рассказать всё, но по-прежнему скрывает своё имя.

  • Ну и, понятно, дата публикации поста.

Аргументы за:

  • Новостной видеоролик: если написать текст несложно, то вот сымитировать новостной выпуск 25-летней давности задача куда более ресурсоёмкая.

  • На Hacker News пара пользователей из Сиэтла узнала в ролике местного телеведущего. Один дал ссылку на более современный ролик с ним чтобы показать, что спустя годы он уже выглядит несколько иначе, так что всё действительно похоже именно на выпуск из 90-х. И пишет, что всё правда визуально похоже на то, как канал KOMO выглядел в те годы.

Подводя итог

Теоретически возможно, что сиэтлские пользователи с Hacker News, якобы узнавшие старую стилистику телеканала, на самом деле тоже подыгрывают. И, наверное, ведущего можно было подделать с помощью дипфейка. Так что история из 1996-го это такой розыгрыш Шрёдингера, мы не знаем точно, был ли он. Будем надеяться, что когда-нибудь Билл Гейтс расскажет в интервью.

Но мне лично история PR-отдел неожиданно нервно реагировал на шуточный анонс кажется правдоподобной по следующей причине. В конце того же 1996-го Microsoft выпустил свою Java-имплементацию Visual J++ то есть на момент розыгрыша в компании наверняка уже вовсю всерьёз думали про собственную Java. Получается, задумавшие розыгрыш сотрудники не понимали, что своими действиями они преждевременно анонсируют реальный проект и при этом высмеивают его. Как потом компании выходить на рынок с продуктом, когда даже её собственные сотрудники уже выставили его как что-то, над чем можно только смеяться?

И хотя Visual J++ не прижилась, позже появились C# и .NET, и их многие тоже называли ответом Microsoft на Java. Как можно заметить, ответом куда более успешным и активно развивающимся по сей день.

Напоследок минутка рекламы: если вы открыли текст про майкрософтовскую Java из-за того, что вы Java-разработчик, вам может быть интересна наша конференция JPoint. А если вы открыли его, потому что вы .NET-разработчик и интересует экосистема Microsoft вам прямая дорога на DotNext. Вот там в докладах много хардкорного контента для опытных разработчиков, тут уж никаких первоапрельских шуток.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru