Шрифт:
Интервал:
Закладка:
Инструкции по настройке Crystal можно найти в Главе 1 «Введение в Crystal». Обратите внимание, что jq, скорее всего, можно установить с помощью менеджера пакетов в вашей системе. Однако вы также можете установить его вручную, загрузив с https://stedolan.github.io/jq/download.
Все примеры кода, использованные в этой главе, можно найти в папке Chapter 6 на GitHub по адресу https://github.com/PacktPublishing/Crystal-Programming/tree/main/Chapter06.
Использование волокон (fibers ) для одновременного выполнения работы
Волокно представляет собой часть работы, которая должна выполняться либо одновременно с другими волокнами, либо в какой-то момент в будущем, когда появятся свободные циклы. Они похожи на потоки операционной системы, но более легкие и управляются изнутри Crystal. Прежде чем мы углубимся, важно отметить, что параллелизм — это не то же самое, что параллелизм, но они связаны между собой.
В параллельном коде на различные фрагменты работы тратится немного времени, при этом в определенный момент времени выполняется только часть работы. С другой стороны, параллельный код позволяет одновременно выполнять несколько фрагментов работы. На практике это означает, что по умолчанию одновременно выполняется только одно волокно. В Crystal есть поддержка параллелизма, который позволяет одновременно выполнять более одного волокна, но он все еще считается экспериментальным. По этой причине мы собираемся сосредоточиться на параллелизме.
Мы уже использовали волокна под капотом во всем коде, с которым работали до сих пор. Весь код Crystal выполняется внутри собственного основного волокна. Кроме того, мы можем создавать собственные волокна с помощью метода spawn, который принимает блок, представляющий работу, которую необходимо выполнить в этом волокне. В качестве примера возьмем следующую программу:
puts "Hello program!"
spawn do
puts "Hello from fiber!"
end
puts "Goodbye program!"
Если бы вы запустили это приложение, оно выдало бы следующее:
Hello program!
Goodbye program!
Но подождите! Что случилось с сообщением в fiber, которое мы создали? Ответ можно найти в начале главы, в разделе "Определение fiber". Ключевые слова появятся в какой-то момент в будущем. Создание fiber не приводит к немедленному выполнению fiber. Вместо этого он запланирован для выполнения планировщиком Crytal. Планировщик выполнит следующий поставленный в очередь fiber при первой возможности. В этом примере такой возможности никогда не возникает, поэтому fiber никогда не выполняется.
Это важная деталь для понимания того, как работает параллелизм в Crystal, а также того, почему природа IO, рассмотренная в Главе 5 "Операции ввода/вывода", может быть настолько полезной. К числу факторов, которые могут привести к выполнению другого fiber, относятся следующие:
• Метод sleep
• Fiber.yield метод
• Операции, связанные с IO, такие как чтение/запись в файл или сокет
• Ожидание получения значения из канала
• Ожидание отправки значения в канал
• Когда текущее волокно завершит выполнение
Все эти параметры блокируют волокно, в результате чего другие волокна получают возможность выполниться. Например, добавьте sleep 1 после блока появления и перезапустите программу. Обратите внимание: на этот раз Hello from fiber! действительно печатается. Метод sleep сообщает планировщику, что он должен продолжить выполнение основного волокна через одну секунду. Тем временем он может свободно выполнить следующее волокно в очереди, которое в данном случае печатает наше сообщение.
Метод Fiber.yield, или sleep 0, даст тот же результат, но означает немного другое. При использовании метода sleep с целочисленным аргументом планировщик знает, что он должен вернуться к этому волокну в какой-то момент в будущем после того, как он достаточно отоспался. Однако использование Fiber.yield или sleep 0 позволит проверить, есть ли волокна, ожидающие выполнения, и если да, выполнить их. В противном случае это будет продолжаться без переключения. Такое поведение наиболее распространено, когда вы выполняете некоторую логику в узком цикле, но все же хотите дать возможность другим волокнам выполниться. Однако Fiber.yield просто сообщает планировщику, что вы можете запустить другое волокно, но не гарантирует, когда и если выполнение переключится обратно на это исходное волокно.
В обоих случаях единственная причина, по которой выполнение вообще переключается обратно на основное волокно, заключается в том, что что-то внутри волокна выполняет одно из действий, которые могут вызвать выполнение другого волокна. Если бы вы удалили путы и волокно состояло бы только из бесконечного цикла, это заблокировало бы волокно навсегда, и программа никогда бы не завершила работу. Если вы хотите разрешить выполнение других файберов и навсегда заблокировать основной файбер, вы можете использовать sleep без каких-либо аргументов. Это будет держать основное волокно в режиме ожидания и выполнять другие волокна по мере их появления.
Продолжая предыдущий пример, вы можете захотеть использовать переменные внутри волокна, которые были определены за его пределами. Однако это плохая идея, поскольку она приводит к неожиданным результатам:
idx = 0
while idx < 4
spawn do
puts idx
end
idx += 1
end
Fiber.yield
Вы могли бы ожидать, что предыдущий код напечатает числа от одного до четырех, но на самом деле он печатает число четыре четыре раза. Причина этого двоякая:
• Волокна не выполняются немедленно.
• Каждое волокно ссылается на одну и ту же переменную.
Поскольку волокна не выполняются немедленно, они создаются при каждой итерации цикла while loop. После четырех раз значение idx достигает четырех и выходит из цикла while loop. Затем, поскольку каждое волокно ссылается на одну и ту же переменную, все они печатают текущее значение этой переменной, равное 4. Эту проблему можно решить, переместив порождение каждого волокна в отдельный процесс, который создаст замыкание, фиксирующее значение переменная на каждой итерации. Однако это далеко не идеально, поскольку в этом нет необходимости и ухудшается читаемость кода. Лучший способ справиться с этим — использовать альтернативную форму spawn, которая принимает вызов в качестве аргумента:
idx = 0
while idx < 4
spawn puts idx
idx += 1
end
Fiber.yield
Это внутренне обрабатывает создание и выполнение Proc, что позволяет сделать код гораздо более читаемым. Использование методов с блоками, например 4.times { |idx| spawn { puts idx } }, работает как положено. Этот сценарий представляет собой проблему только при ссылке на одну и ту же локальную переменную, переменную класса или экземпляра во время итерации. Это также яркий пример того, почему совместное использование состояния непосредственно