Язык Crystal: понятная асинхронная модель, простота запуска программы и понятные сообщения компилятора. Пример асинхронной программы.
Crystal не поддерживает полную параллельность, как Go. В нем реализована асинхронная модель, под капотом работающая, похоже на asyncio в Python, но в отличие от Python, все сделано так, что программисту намного легче понять и использовать ее. Дело в том, что в Crystal в любой программе автоматически имеется неявный цикл событий. Это значит, что там, где в Python нужно импортировать asyncio, создавать цикл, еще какие-то пассы руками, а потом любую функцию, в которой предполагается делать неблокирующие ожидания await оборачивать в async - в Crystal всего этого делать не нужно: цикл уже есть, уже имеется одно основное волокно (как поток в Python) и в стандартной библиотеке все операции ввода- вывода, сетевые операции, работа с базами данных - все, что по определению будет чего-то ожидать - все уже написано в асинхронной манере. Что остается сделать программисту: ему остается выделять в нужных местах программы новые волокна через spawn, обмениваться между волокнами данными с помощью каналов Channel (выглядящих так же как в Go) и следить, чтобы основное волокно не закончило свою работу до того, как закончится какая-то работа в выделенных волокнах.
Для создания заготовки проекта выполним
crystal init app crystal_prog_1
Открываем в редакторе файл crystal_prog_1.cr и пишем программу. Пусть программа каждые пять секунд выводит (puts) в консоль приглашение написать что-нибудь, а как только пользователь ввел что-то (gets) и нажал Enter - печатает в ответ введенную пользователем строку. При вводе символа q программа завершит (exit) выполнение.
Понятно, что здесь требуется асинхронность: если писать программу линейно - она будет блокироваться на пять секунд. Нам же нужно, чтобы она реагировала на пользовательский ввод сразу.
Находим в документации ( https://crystal-lang.org/reference/guides/concurrency.html ) главу про конкурентное программирование (и конечно смотрим и в другие главы) и пишем код.
Нам нужны два канала, через которые будут проходить сообщения из двух разных отспавненных волокон, в каждом из которых крутится свой цикл. Внешне модель похожа на то, как это делается в Elixir (Erlang) и Go, с разницей в том, что здесь нет распараллеливания по процессорам. Зато есть небольшой бинарник на выходе, чего нет в Elixir и приятный синтаксис, чего не скажешь про Go. Итак, работают два созданных программистом волокна: одно каждые пять секунд посылает в предварительно созданный канал ticks единички, а второе в цикле ждет ввода от пользователя, и посылает введенный текст в заранее созданный канал user_input. В основном волокне нужно в цикле читать из обоих этих каналов, но так, чтобы ожидание чтения из одного канала не блокировало чтение из второго. Это можно сделать с помощью функции receive_first. Она принимает список каналов, реализована в стандартной библиотеке так: последовательно проверяет каждый из каналов на наличие данных для чтения и как только такой канал нашелся, читает из него одно значение и возвращает.
# Внимание! Возможно, вообще неидиоматичный код, написанный новичком.
module CrystalProg1
VERSION = "0.1.0"
user_input = Channel(String).new
spawn do
while true
a = gets()
if a.is_a?(String)
user_input.send a
end
end
end
ticks = Channel(Int32).new
spawn do
while true
ticks.send(1)
sleep 5
end
end
while true
res = Channel.receive_first([user_input, ticks])
if res.is_a?(String)
if res == "q"
exit
end
puts "You type:'#{res}''"
end
if res.is_a?(Int32)
puts "Type something"
end
end
end
Программа, написана, действительно, с наскока, и она запускается командой
crystal run src/crystal_prog_1.cr
из корня проекта.
Выводы.
Синтаксис - очень интуитивный. Очень простая модель конкурентного выполнения. Несравнимо проще, чем asyncio в Python. С другой стороны - стандартная библиотека, хоть и выглядит очень мощно, но как получить справку по тем же Channel - непонятно. В целом, факт того что задача оказалась по силам новичку - мотивирует на дальнейшие исследования.
2020-01-05