Crystal Programming. Введение на основе проекта в создание эффективных, безопасных и читаемых веб-приложений и приложений CLI - Джордж Дитрих
Шрифт:
Интервал:
Закладка:
К счастью для нас, JSON и, как следствие, YAML являются форматами потоковой сериализации. Другими словами, вы можете переводить один формат в другой по одному символу за раз, не загружая все данные заранее. Как упоминалось ранее, это одно из основных преимуществ создания нашего приложения на основе IO. Мы можем использовать это, обновив нашу логику преобразования для вывода преобразованных выходных данных, одновременно анализируя входные данные. Начнем с метода .deserialize в src/yaml.cr. Код этого метода довольно длинный, его можно найти на Github по адресу https://github.com/PacktPublishing/Crystal-Programming/blob/main/Chapter05/yaml_v2.cr.
Здесь много всего происходит, поэтому давайте немного разберем алгоритм:
1. Мы начинаем использовать некоторые новые типы в модуле каждого формата вместо того, чтобы оба они полагались на метод .parse:
• YAML::PullParser позволяет использовать входной токен YAML токеном по требованию, поскольку данные доступны из типа входного IO. Он также предоставляет метод, который возвращает тип токена, который он анализирует в данный момент.
• JSON::Builder, с другой стороны, используется для создания JSON с помощью объектно-ориентированного API, записывая JSON в выходной тип IO.
2. Мы используем эти два объекта совместно для одновременного анализа YAML и вывода JSON. По сути, алгоритм начинает чтение потока данных YAML, запуская цикл, который будет продолжаться до конца документа YAML, переводя соответствующий токен YAML в его аналог JSON.
Метод .serialize следует той же общей идее: код также доступен на Github в том же файле.
Однако в этом случае алгоритм существенно обратный. Мы используем анализатор JSON и построитель YAML. Давайте проведем тест и посмотрим, насколько это помогло.
Тестирование производительности
Для тестирования я буду использовать реализацию GNU утилиты time с опцией -v для подробного вывода. В качестве входных данных я буду использовать файл invItems.yaml, который можно найти в папке этой главы на GitHub. Входные данные не имеют особого значения, если они представлены в формате YAML, но я выбрал эти данные, потому что они довольно большие — 53,2 МБ. Чтобы выполнить тест, мы выполним следующие шаги:
1. Начните со старой версии кода, поэтому обязательно вернитесь к старому коду, прежде чем продолжить.
2. Соберите двоичный файл в режиме выпуска с помощью shards build --release. Поскольку мы хотим протестировать производительность нашего приложения, а не jq, мы просто будем использовать идентификационный фильтр, чтобы не загружать jq дополнительной работой.
3. Запустите тест через /usr/bin/time -v ./bin/transform . invItems.yaml > /dev/null. Поскольку нас не волнует фактический вывод, мы просто перенаправляем вывод в /dev/null. Эта команда выведет довольно много информации, но нас действительно волнует одна строка — Максимальный размер резидентного набора (кбайт), который представляет общий объем памяти, используемой процессом в килобайтах. В моем случае это значение было 1 432 592, а это значит, что наше приложение потратило почти 1,5 ГБ на преобразование этих данных!
Затем восстановите новый код и снова выполните предыдущие шаги, чтобы увидеть, приведут ли наши изменения к улучшению использования памяти. На этот раз у меня получилось 325 352, что более чем в 4 раза меньше, чем раньше!
До сих пор во входном IO находились данные для обработки либо из входного файла, либо из STDIN. Однако что произойдет, если наше приложение ожидает входные данные, но данных для обработки нет? В следующем разделе мы собираемся изучить, как ведет себя ввод-вывод в этом сценарии.
Объяснение поведения IO
Если вы создадите и запустите приложение как ./bin/transform ., оно просто будет зависать на неопределенный срок. Причина этого связана с тем, как большая часть операций ввода-вывода работает в Crystal. Большая часть операций IO является блокирующей по своей природе, то есть будет ожидать поступления данных через тип входного IO, в данном случае STDIN. Лучше всего это можно продемонстрировать с помощью этой простой программы:
print "What is your name? "
if (name = gets).presence
puts "Your name is: '#{name}'"
else
puts "No name supplied"
end
Метод get используется для чтения строки из STDIN и будет ждать, пока она не получит данные или пользователь не прервет команду. Такое поведение также справедливо для IO, не связанного с терминалом, например для тел ответов HTTP. Причины и преимущества такого поведения будут объяснены в следующей главе.
Резюме
В этой главе мы добились фантастического прогресса в работе над приложением. Мы не только сделали его действительно удобным для использования, поддержав терминальный IO, но и сделали его еще более гибким, чем раньше, разрешив использовать любой IO. Мы также значительно повысили эффективность нашей логики преобразования, выполнив потоковое преобразование. Наконец, мы немного узнали о блокирующей природе IO, что подготовило почву для следующей главы.
IO является основной частью любого приложения, которое выполняет чтение/запись данных. Наличие знаний о том, когда их использовать и, что более важно, как ими воспользоваться, в конечном счете приведет к созданию более эффективных программ. В этой главе также был затронут вопрос о правильном дизайне приложения, рассмотренный в предыдущей главе, и приведено несколько примеров того, как небольшие изменения могут значительно повысить общую полезность приложения.
В следующей главе мы рассмотрим концепцию параллелизма и то, как она может позволить нашему приложению обрабатывать множество файлов.
6. Параллелизм (Concurrency)
В некоторых сценариях программе может потребоваться обработка нескольких фрагментов работы, например суммирование количества строк в серии файлов. Это прекрасный пример проблемы, которую может помочь решить параллелизм, позволяя программе выполнять фрагменты работы, ожидая выполнения других. В этой главе мы узнаем, как работает параллелизм в Crystal, и рассмотрим следующие темы:
• Использование волокон для одновременного выполнения работы
• Использование каналов для безопасной передачи данных
• Одновременное преобразование нескольких файлов
К концу этой главы вы сможете понять разницу между параллелизмом и параллелизмом, как использовать волокна для выполнения нескольких одновременных задач и как использовать каналы для правильного обмена данными между волокнами. Вместе эти концепции позволяют создавать многозадачные программы, что приводит к повышению производительности кода.
Технические требования
Прежде чем мы углубимся в эту главу, в вашей системе должно быть установлено следующее:
• Рабочая установка Crystal