Выбрать главу

Метод 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 } }, работает как положено. Этот сценарий представляет собой проблему только при ссылке на одну и ту же локальную переменную, переменную класса или экземпляра во время итерации. Это также яркий пример того, почему совместное использование состояния непосредственно внутри волокон считается плохой практикой. Правильный способ сделать это — использовать каналы, которые мы рассмотрим в следующем разделе.

Использование каналов для безопасной передачи данных

Если совместное использование переменных между волокнами не является правильным способом взаимодействия между волокнами, то что? Ответ – каналы. Канал — это способ связи между волокнами без необходимости беспокоиться об условиях гонки, блокировках, семафорах или других специальных структурах. Давайте посмотрим на следующий пример:

input_channel = Channel(Int32).new

output_channel = Channel(Int32).new

spawn do