класс Fiber
Волокна — это примитивы для реализации лёгкой кооперативной конкурентности в Ruby. По сути, они представляют собой способ создания блоков кода, которые могут приостанавливаться и возобновляться, как потоки. Основное различие заключается в том, что они никогда не прерываются, а планирование должно выполняться программистом, а не виртуальной машиной.
В отличие от других моделей бесстековой лёгкой конкурентности, каждое волокно имеет стек. Это позволяет волокну приостанавливаться из глубоко вложенных вызовов функций внутри блока волокна. Обратитесь к справке ruby(1) для настройки размера стека волокна(ов).
Когда волокно создаётся, оно не запускается автоматически. Вместо этого его необходимо явно запустить, используя метод Fiber#resume. Код, выполняющийся внутри волокна, может передать управление, вызвав Fiber.yield, в этом случае он передаёт управление обратно вызывающей стороне (вызывающему методу Fiber#resume).
При передаче управления или завершении волокно возвращает значение последнего выполненного выражения
Например:
fiber = Fiber.new do Fiber.yield 1 2 end puts fiber.resume puts fiber.resume puts fiber.resume
производит
1 2 FiberError: dead fiber called
Метод Fiber#resume принимает произвольное количество параметров; если это первый вызов resume, они будут переданы в качестве аргументов блока. В противном случае они будут значением возвращаемым вызовом Fiber.yield
Пример:
fiber = Fiber.new do |first| second = Fiber.yield first + 2 end puts fiber.resume 10 puts fiber.resume 1_000_000 puts fiber.resume "The fiber will be dead before I can cause trouble"
производит
12 1000000 FiberError: dead fiber called
Неблокирующие волокна
Концепция неблокирующего волокна была введена в Ruby 3.0. Неблокирующее волокно, достигая операции, которая обычно блокирует волокно (например, sleep, или ожидания другого процесса или ввода/вывода), передаёт управление другим волокнам и позволяет планировщику обрабатывать блокировку и пробуждение (возобновление) этого волокна, когда оно сможет продолжить.
Чтобы волокно Fiber работало как неблокирующее, его необходимо создать в Fiber.new с blocking: false (что является значением по умолчанию), и Fiber.scheduler должен быть установлен с помощью Fiber.set_scheduler. Если Fiber.scheduler не установлен в текущей нити, поведение блокирующих и неблокирующих волокон идентично.
Ruby не предоставляет класс планировщика: ожидается, что он будет реализован пользователем и будет соответствовать Fiber::SchedulerInterface.
Также существует метод Fiber.schedule, который ожидается, что немедленно выполнит предоставленный блок кода в отдельном неблокирующем волокне.
Общедоступные методы класса
static VALUE
rb_fiber_s_blocking_p(VALUE klass)
{
rb_thread_t *thread = GET_THREAD();
unsigned blocking = thread->blocking;
if (blocking == 0)
return Qfalse;
return INT2NUM(blocking);
} Возвращает false, если текущее волокно неблокирующее. Fiber является неблокирующим, если оно было создано путём передачи blocking: false в Fiber.new или через Fiber.schedule.
Если текущее Fiber блокирующее, метод возвращает 1. Будущие разработки могут позволить возвращать целые числа большего размера.
Обратите внимание, что даже если метод возвращает false, Fiber ведет себя иначе только если Fiber.scheduler установлен в текущей нити.
См. раздел «Неблокирующие волокна» в документации класса для получения подробной информации.
static VALUE
rb_fiber_s_current(VALUE klass)
{
return rb_fiber_current();
} Возвращает текущее волокно. Если вы не работаете в контексте волокна, этот метод вернёт корневое волокно.
static VALUE
rb_fiber_current_scheduler(VALUE klass)
{
return rb_fiber_scheduler_current();
} Возвращает планировщик Fiber, который был последним установлен для текущей нити с помощью Fiber.set_scheduler, если и только если текущее волокно неблокирующее.
static VALUE
rb_fiber_initialize(int argc, VALUE* argv, VALUE self)
{
return rb_fiber_initialize_kw(argc, argv, self, rb_keyword_given_p());
} Создаёт новое Fiber. Изначально волокно не работает и может быть возобновлено с помощью resume. Аргументы первого вызова resume будут переданы в блок:
f = Fiber.new do |initial|
current = initial
loop do
puts "current: #{current.inspect}"
current = Fiber.yield
end
end
f.resume(100) # prints: current: 100
f.resume(1, 2, 3) # prints: current: [1, 2, 3]
f.resume # prints: current: nil
# ... and so on ...
Если blocking: false передаётся в Fiber.new, и в текущей нити определён Fiber.scheduler, то Fiber становится неблокирующим (см. раздел «Неблокирующие волокна» в документации класса).
static VALUE
rb_fiber_s_schedule(int argc, VALUE *argv, VALUE obj)
{
return rb_fiber_s_schedule_kw(argc, argv, rb_keyword_given_p());
} Метод должен немедленно выполнить предоставленный блок кода в отдельном неблокирующем волокне.
puts "Go to sleep!" Fiber.set_scheduler(MyScheduler.new) Fiber.schedule do puts "Going to sleep" sleep(1) puts "I slept well" end puts "Wakey-wakey, sleepyhead"
Предполагая, что MyScheduler реализован корректно, эта программа выдаст:
Go to sleep! Going to sleep Wakey-wakey, sleepyhead ...1 sec pause here... I slept well
…например, при первой блокирующей операции внутри Fiber (sleep(1)), управление передаётся внешнему коду (главному волокну), и по завершении этого выполнения планировщик позаботится о надлежащем возобновлении всех заблокированных волокон.
Обратите внимание, что описанное выше поведение является тем, как метод должен работать, фактическое поведение зависит от реализации текущего планировщика метода Fiber::SchedulerInterface#fiber. Ruby не накладывает никаких ограничений на поведение этого метода.
Если планировщик не задан, метод вызывает исключение RuntimeError (No scheduler is available!).
static VALUE
rb_fiber_s_scheduler(VALUE klass)
{
return rb_fiber_scheduler_get();
} Returns the Fiber scheduler, that was last set for the current thread with Fiber.set_scheduler. Returns +nil+ if no scheduler is set (which is the default), and non-blocking fibers'
# поведение такое же, как и для блокирования.
(see "Non-blocking fibers" section in class docs for details about the scheduler concept).
static VALUE
rb_fiber_set_scheduler(VALUE klass, VALUE scheduler)
{
return rb_fiber_scheduler_set(scheduler);
} Устанавливает планировщик Fiber для текущей нити. Если планировщик установлен, неблокирующие волокна (созданные с помощью Fiber.new с blocking: false, или с помощью Fiber.schedule) вызывают методы обработки потенциальных блокирующих операций в этом планировщике, а текущая нить вызовет метод close планировщика при завершении (разрешая планировщику надлежащим образом управлять всеми незавершенными волокнами).
scheduler может быть объектом любого класса, соответствующего Fiber::SchedulerInterface. Его реализация зависит от пользователя.
См. также раздел «Неблокирующие волокна» в документации класса.
static VALUE
rb_fiber_s_yield(int argc, VALUE *argv, VALUE klass)
{
return rb_fiber_yield_kw(argc, argv, rb_keyword_given_p());
} Возвращает управление контексту, который возобновил волокно, передавая любые аргументы, которые были ему переданы. Волокно возобновит обработку в этой точке, когда resume будет вызван в следующий раз. Любые аргументы, переданные в следующий вызов resume, будут значением, к которому вычисляется это выражение Fiber.yield.
Методы публичного экземпляра
VALUE
rb_fiber_alive_p(VALUE fiber_value)
{
return FIBER_TERMINATED_P(fiber_ptr(fiber_value)) ? Qfalse : Qtrue;
} Возвращает true, если волокно всё ещё может быть возобновлено (или передано). После завершения выполнения блока волокна этот метод всегда вернёт false.
static VALUE
rb_fiber_backtrace(int argc, VALUE *argv, VALUE fiber)
{
return rb_vm_backtrace(argc, argv, &fiber_ptr(fiber)->cont.saved_ec);
} Возвращает текущий стек выполнения волокна. start, count и end позволяют выбрать только части стека вызовов.
def level3
Fiber.yield
end
def level2
level3
end
def level1
level2
end
f = Fiber.new { level1 }
# It is empty before the fiber started
f.backtrace
#=> []
f.resume
f.backtrace
#=> ["test.rb:2:in `yield'", "test.rb:2:in `level3'", "test.rb:6:in `level2'", "test.rb:10:in `level1'", "test.rb:13:in `block in <main>'"]
p f.backtrace(1) # start from the item 1
#=> ["test.rb:2:in `level3'", "test.rb:6:in `level2'", "test.rb:10:in `level1'", "test.rb:13:in `block in <main>'"]
p f.backtrace(2, 2) # start from item 2, take 2
#=> ["test.rb:6:in `level2'", "test.rb:10:in `level1'"]
p f.backtrace(1..3) # take items from 1 to 3
#=> ["test.rb:2:in `level3'", "test.rb:6:in `level2'", "test.rb:10:in `level1'"]
f.resume
# It is nil after the fiber is finished
f.backtrace
#=> nil
static VALUE
rb_fiber_backtrace_locations(int argc, VALUE *argv, VALUE fiber)
{
return rb_vm_backtrace_locations(argc, argv, &fiber_ptr(fiber)->cont.saved_ec);
} Как backtrace, но возвращает каждую строку стека выполнения как Thread::Backtrace::Location. Принимает те же аргументы, что и backtrace.
f = Fiber.new { Fiber.yield }
f.resume
loc = f.backtrace_locations.first
loc.label #=> "yield"
loc.path #=> "test.rb"
loc.lineno #=> 1
VALUE
rb_fiber_blocking_p(VALUE fiber)
{
return RBOOL(fiber_ptr(fiber)->blocking != 0);
} Возвращает true если fiber блокирует, и false в противном случае. Fiber неблокирующее, если оно было создано путём передачи blocking: false в Fiber.new или через Fiber.schedule.
Обратите внимание, что даже если метод возвращает false, поведение волокна отличается только если Fiber.scheduler задан в текущем потоке.
Подробности см. в разделе «Неблокирующие волокна» в документации по классу.
static VALUE
rb_fiber_m_raise(int argc, VALUE *argv, VALUE self)
{
return rb_fiber_raise(self, argc, argv);
} Выбрасывает исключение в волокне в тот момент, когда был вызван последний Fiber.yield. Если волокно ещё не запущено или уже выполнено до конца, выбрасывает FiberError. Если волокно уступает управление, оно возобновляется. Если оно передаёт управление, оно передаёт управление. Но если оно возобновляется, выбрасывает FiberError.
Без аргументов, выбрасывает RuntimeError. С одним аргументом типа String, выбрасывает RuntimeError с сообщением в виде строки. В противном случае, первый параметр должен быть именем класса Exception (или объектом, который возвращает объект типа Exception при отправке сообщения exception). Необязательный второй параметр устанавливает сообщение, связанное с исключением, а третий параметр — массив информации о вызове обратного вызова. Исключения перехватываются блоком rescue блоков begin...end.
static VALUE
rb_fiber_m_resume(int argc, VALUE *argv, VALUE fiber)
{
return rb_fiber_resume_kw(fiber, argc, argv, rb_keyword_given_p());
} Возобновляет волокно с точки, в которой был вызван последний Fiber.yield, или начинает его выполнение, если это первый вызов resume. Аргументы, переданные в resume, будут значением выражения Fiber.yield, или будут переданы как параметры блока в блок волокна, если это первый вызов resume.
В качестве альтернативы, при вызове resume оно вычисляется как аргументы, переданные следующему оператору Fiber.yield внутри блока волокна или как значение блока, если он выполняется до конца без Fiber.yield
static VALUE
fiber_to_s(VALUE fiber_value)
{
const rb_fiber_t *fiber = fiber_ptr(fiber_value);
const rb_proc_t *proc;
char status_info[0x20];
if (fiber->resuming_fiber) {
snprintf(status_info, 0x20, " (%s by resuming)", fiber_status_name(fiber->status));
}
else {
snprintf(status_info, 0x20, " (%s)", fiber_status_name(fiber->status));
}
if (!rb_obj_is_proc(fiber->first_proc)) {
VALUE str = rb_any_to_s(fiber_value);
strlcat(status_info, ">", sizeof(status_info));
rb_str_set_len(str, RSTRING_LEN(str)-1);
rb_str_cat_cstr(str, status_info);
return str;
}
GetProcPtr(fiber->first_proc, proc);
return rb_block_to_s(fiber_value, &proc->block, status_info);
} static VALUE
rb_fiber_m_transfer(int argc, VALUE *argv, VALUE self)
{
return rb_fiber_transfer_kw(self, argc, argv, rb_keyword_given_p());
} Передаёт управление другому волокну, возобновляя его с последней остановки или запуская его, если оно не было возобновлено ранее. Вызывающее волокно приостановится, как и при вызове Fiber.yield.
Волокно, которое получает вызов передачи, обрабатывает его как вызов возобновления. Аргументы, переданные в transfer, обрабатываются как те, что переданы в resume.
Два стиля передачи управления в волокно (один — resume и Fiber::yield, другой — transfer в волокно и из него) не могут быть свободно смешаны.
-
Если жизненный цикл волокна начался с передачи, оно никогда не сможет уступить или получить управление путём возобновления, только завершить или передать управление обратно. (Оно всё ещё может возобновить другие волокна, которые разрешено возобновить.)
-
Если жизненный цикл волокна начался с возобновления, оно может уступить управление или передать управление другому
Fiber, но может получить управление обратно только способом, совместимым со способом, которым оно было отдано: если оно передавало, оно только может быть передано обратно, а если оно уступало, оно только может быть возобновлено обратно. После этого оно снова может передать или уступить.
Если эти правила нарушены, поднимается FiberError.
Для отдельного проектирования Fiber, yield/resume легче в использовании (волокно просто отдает управление, ему не нужно думать, кому оно отдаётся), а передача более гибкая для сложных случаев, позволяя создавать произвольные графы волокон, зависящих друг от друга.
Пример:
manager = nil # For local var to be visible inside worker block
# This fiber would be started with transfer
# It can't yield, and can't be resumed
worker = Fiber.new { |work|
puts "Worker: starts"
puts "Worker: Performed #{work.inspect}, transferring back"
# Fiber.yield # this would raise FiberError: attempt to yield on a not resumed fiber
# manager.resume # this would raise FiberError: attempt to resume a resumed fiber (double resume)
manager.transfer(work.capitalize)
}
# This fiber would be started with resume
# It can yield or transfer, and can be transferred
# back or resumed
manager = Fiber.new {
puts "Manager: starts"
puts "Manager: transferring 'something' to worker"
result = worker.transfer('something')
puts "Manager: worker returned #{result.inspect}"
# worker.resume # this would raise FiberError: attempt to resume a transferring fiber
Fiber.yield # this is OK, the fiber transferred from and to, now it can yield
puts "Manager: finished"
}
puts "Starting the manager"
manager.resume
puts "Resuming the manager"
# manager.transfer # this would raise FiberError: attempt to transfer to a yielding fiber
manager.resume
результат
Starting the manager Manager: starts Manager: transferring 'something' to worker Worker: starts Worker: Performed "something", transferring back Manager: worker returned "Something" Resuming the manager Manager: finished
Ruby Core © 1993–2022 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.