Это внутренне обрабатывает создание и выполнение Proc
, что позволяет сделать код гораздо более читаемым. Использование методов с блоками, например 4.times { |idx| spawn { puts idx } }
, работает как положено. Этот сценарий представляет собой проблему только при ссылке на одну и ту же локальную переменную, переменную класса или экземпляра во время итерации. Это также яркий пример того, почему совместное использование состояния непосредственно внутри волокон считается плохой практикой. Правильный способ сделать это — использовать каналы, которые мы рассмотрим в следующем разделе.
Использование каналов для безопасной передачи данных
Если совместное использование переменных между волокнами не является правильным способом взаимодействия между волокнами, то что? Ответ – каналы. Канал — это способ связи между волокнами без необходимости беспокоиться об условиях гонки, блокировках, семафорах или других специальных структурах. Давайте посмотрим на следующий пример:
input_channel = Channel(Int32).new
output_channel = Channel(Int32).new
spawn do
output_channel.send input_channel.receive * 2
end
input_channel.send 2
puts output_channel.receive
В предыдущем примере создаются два канала, содержащие входные и выходные значения Int32
. Затем он порождает волокно, которое сначала получает значение из входного канала, удваивает его и отправляет в выходной канал. Затем мы отправляем входному каналу начальное значение 2
и, наконец, печатаем результат, который получаем обратно из выходного канала. Как упоминалось в предыдущем разделе, само волокно не выполняется ни при его создании, ни при отправке ему значения. Ключевой частью этого примера является последний 4
.
Давайте посмотрим на другой пример, который сделает поведение более понятным:
channel = Channel(Int32).new
spawn do
loop do
puts "Waiting"
sleep 0.5
end
end
spawn do
sleep 2
channel.send channel.receive * 2
sleep 1
channel.send channel.receive * 3
end
channel.send 2
puts channel.receive
channel.send 3
puts channel.receive
Запуск программы приводит к следующему выводу:
Waiting
Waiting
Waiting
Waiting
4
Waiting
Waiting
9
Первые результаты отправки и получения во втором волокне выполняются первыми. Однако первая строка — это sleep 2
, поэтому она делает именно это. Поскольку спящий режим является блокирующей операцией, планировщик Crystal выполнит следующее ожидающее волокно, то есть то, которое печатает Waiting
, а затем в цикле ожидает полсекунды. Это сообщение выводится четыре раза, что соответствует двухсекундному спящему режиму, за которым следует ожидаемый результат 4
. Затем выполнение возвращается ко второму волокну, но сразу же переходит к первому волокну из-за sleep 1
, что печатает Ожидание еще дважды, прежде чем отправить ожидаемый вывод 9
обратно в канал.
В обоих примерах мы работали с небуферизованными каналами. Небуферизованный канал продолжит выполнение на волокне, ожидающем получения отправленного значения из канала. Другими словами, именно поэтому выполнение программы возвращается к основному волокну для печати значения вместо продолжения выполнения второго волокна.
С другой стороны, буферизованный канал не будет переключаться на другое волокно при вызове отправки, если буфер не заполнен. Буферизованный канал можно создать, передав размер буфера конструктору канала. Например, взгляните на следующее:
channel = Channel(Int32).new 2
spawn do
puts "Before send 1"
channel.send 1
puts "Before send 2"
channel.send 2
puts "Before send 3"
channel.send 3
puts "After send"
end
3.times do
puts channel.receive
end
Это выведет следующее:
Before send 1
Before send 2
Before send 3
After send
1
2
3
Теперь, если мы запустим тот же код с небуферизованным каналом, результат будет следующий:
Before send 1
Before send 2
1
2
Before send 3
After send
3