Шрифт:
Интервал:
Закладка:
channel1.send 1
end
spawn do
puts "Starting fiber 2"
sleep 1
channel2.send 2
end
select
when v = channel1.receive
puts "Received #{v} from channel1"
when v = channel2.receive
puts "Received #{v} from channel2"
end
Этот пример выводит следующее:
Starting fiber 1
Starting fiber 2
Received 2 from channel2
Здесь оба волокна начинают выполняться более или менее одновременно, но поскольку у второго волокна более короткий период сна и он завершается первым, это приводит к тому, что ключевое слово select печатает значение из этого канала и затем завершает работу. Обратите внимание, что ключевое слово select действует аналогично одиночному каналу channel.receive в том смысле, что оно блокирует основное волокно, а затем продолжает работу после получения значения из любого канала. Кроме того, мы могли бы обрабатывать несколько итераций, поместив ключевое слово select в цикл вместе с методом timeout, чтобы избежать вечной блокировки. Давайте расширим предыдущий пример, чтобы продемонстрировать, как это работает. Во-первых, давайте добавим переменную channel3, аналогичную двум другим, которые у нас уже есть. Далее давайте создадим еще одно волокно, которое отправит значение в наш третий канал. Например, взгляните на следующее:
spawn do
puts "Starting fiber 3"
channel3.send 3
end
Наконец, мы можем переместить наше ключевое слово select в цикл:
loop do
select
when v = channel1.receive
puts "Received #{v} from channel1"
when v = channel2.receive
puts "Received #{v} from channel2"
when v = channel3.receive
puts "Received #{v} from channel3"
when timeout 3.seconds
puts "Nothing left to process, breaking out"
break
end
end
Эта версия ключевого слова select аналогична первой, но мы добавили к ней два новых предложения. Один считывает значение из третьего канала, а другой выйдет из цикла, если ни по одному каналу в течение трех секунд не будут получены данные. Вывод этой программы следующий:
Starting fiber 1
Starting fiber 2
Starting fiber 3
Received 3 from channel3
Received 2 from channel2
Received 1 from channel1
Nothing left to process, breaking out
Волокна начинают работать по порядку, но заканчивают в другом порядке из-за разной продолжительности сна. Через три секунды выполняется последнее предложение if, поскольку ничего не получено, а затем программа завершает работу.
Ключевое слово select не ограничивается только получением значений. Его также можно использовать при их отправке. Возьмем эту программу в качестве примера:
spawn_receiver = true
channel = Channel(Int32).new
if spawn_receiver
spawn do
puts "Received: #{channel.receive}"
end
end
spawn do
select
when channel.send 10
puts "sent value"
else
puts "skipped sending value"
end
end
Fiber.yield
Запуск этого как есть дает следующий результат:
sent value
Received: 10
Установка флага spawn_receiver в значение false и его повторный запуск приводит к пропущенному значению отправки. Причина разницы в выводе связана с поведением send в сочетании с предложением else ключевого слова select. select проверит каждое предложение if на наличие того, которое не будет блокироваться при выполнении. Однако в этом случае отправляйте блоки, поскольку нет волокна, ожидающего значения, поэтому предложение else будет выполнено, поскольку ни одно другое предложение не может быть выполнено без блокировки. Поскольку принимающее волокно не было создано, выполняется последний путь, что приводит к пропуску сообщения. В другом сценарии ожидающий получатель не позволяет блокировать отправку.
Хотя использование каналов и волокон для сигнализации о завершении единицы работы является одним из вариантов их использования, это не единственный вариант использования. Эти две концепции, а также select, можно объединить для создания довольно мощных шаблонов, таких как разрешение одновременного выполнения только определенного количества волокон, координация состояния между несколькими волокнами и каналами или обработка нескольких независимых фрагментов данных. работать одновременно. Последний имеет дополнительное преимущество: скорее всего, он уже настроен для обработки многопоточных рабочих процессов, поскольку каждое волокно может обрабатываться в отдельном потоке.
На этом этапе мы рассмотрели практически все основные концепции параллелизма в Crystal. Следующим шагом будет применение этих концепций, а также того, что было изучено в предыдущих главах, к нашему приложению CLI для поддержки одновременной обработки нескольких файлов.
Преобразование нескольких файлов одновременно
На данный момент приложение поддерживает файловый ввод, но только из одного файла. Допустимым вариантом использования может быть предоставление нескольких файлов и создание нового файла с преобразованными данными для каждого из них. Учитывая, что логика преобразования привязана к IO, одновременное выполнение этого имеет смысл и должно привести к повышению производительности.
Причина, по которой логика, связанная с IO, и параллелизм так хорошо сочетаются друг с другом, заключается в планировщике Crystal. Когда волокно достигает точки своего выполнения, когда оно зависит от некоторой части данных из IO, планировщик может легко отложить это волокно в сторону до тех пор, пока не поступят эти данные.
Более конкретным примером этого в действии было бы рассмотрение того, как функционирует стандартная библиотека HTTP::Server. Каждый запрос обрабатывается в отдельном волокне. Из-за этого, если во время обработки запроса необходимо выполнить еще один HTTP-запрос, например, для получения данных из внешнего API, Crystal сможет продолжать обрабатывать другие запросы, ожидая возвращения данных через IO сокет.
Параллелизм не сильно поможет, если часть работы связана с процессором. Однако в нашем случае чтение/запись данных в/из файлов является проблемой, связанной с вводом-выводом, что делает его идеальным кандидатом для демонстрации некоторых функций параллелизма.
Возвращаясь к нашей логике обработки нескольких файлов, давайте сначала создадим непараллельную реализацию, а затем реорганизуем ее, чтобы использовать функции параллелизма, описанные в последних двух разделах.
Прежде чем мы перейдем непосредственно к делу, давайте потратим немного времени на то, чтобы спланировать, что нам нужно сделать, чтобы поддержать это:
• Найдите способ сообщить CLI, что он должен обрабатывать файлы в режиме нескольких файлов.
• Определить новый метод, который будет обрабатывать каждый файл из ARGV.
Первое требование можно удовлетворить, поддерживая опцию CLI --multi, которая переведет его в правильный режим. Второе требование также простое,