класс 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.new с blocking: false (что является значением по умолчанию), и Fiber.scheduler должен быть установлен с помощью Fiber.set_scheduler. Если Fiber.scheduler не установлен в текущем потоке, поведение блокирующих и неблокирующих волокон идентично.
Ruby не предоставляет класс планировщика: ожидается, что он будет реализован пользователем и соответствует Fiber::Scheduler.
Также существует метод Fiber.schedule, который, как ожидается, немедленно выполнит заданный блок в неблокирующем режиме. Его фактическая реализация зависит от планировщика.
Методы публичного класса
static VALUE
rb_fiber_storage_aref(VALUE class, VALUE key)
{
ID id = rb_check_id(&key);
if (!id) return Qnil;
VALUE storage = fiber_storage_get(fiber_current());
if (storage == Qnil) return Qnil;
return rb_hash_aref(storage, key);
} Возвращает значение переменной хранилища волокна, идентифицированной по key.
key должно быть символом, а значение устанавливается с помощью Fiber#[]= или Fiber#store.
См. также Fiber::[]=.
static VALUE
rb_fiber_storage_aset(VALUE class, VALUE key, VALUE value)
{
ID id = rb_check_id(&key);
if (!id) return Qnil;
VALUE storage = fiber_storage_get(fiber_current());
return rb_hash_aset(storage, key, value);
} Присваивает value переменной хранилища волокна, идентифицированной по key. Переменная создается, если она не существует.
key должно быть Symbol, в противном случае генерируется TypeError.
См. также Fiber::[].
VALUE
rb_fiber_blocking(VALUE class)
{
VALUE fiber_value = rb_fiber_current();
rb_fiber_t *fiber = fiber_ptr(fiber_value);
// If we are already blocking, this is essentially a no-op:
if (fiber->blocking) {
return rb_yield(fiber_value);
} else {
return rb_ensure(fiber_blocking_yield, fiber_value, fiber_blocking_ensure, fiber_value);
}
} Принудительно делает волокно блокирующим на время выполнения блока. Возвращает результат выполнения блока.
См. раздел «Неблокируемые волокна» в документации к классу для получения подробностей.
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 становится неблокирующим (см. раздел «Неблокируемые волокна» в документации к классу).
Если storage не указано, по умолчанию используется копия хранилища текущего волокна. Это эквивалентно указанию storage: true.
Fiber[:x] = 1 Fiber.new do Fiber[:x] # => 1 Fiber[:x] = 2 end.resume Fiber[:x] # => 1
Если переданное storage — nil, эта функция отложит инициализацию внутреннего хранилища, которое изначально пустое.
Fiber[:x] = "Hello World" Fiber.new(storage: nil) do Fiber[:x] # nil end
В противном случае переданное storage используется в качестве хранилища нового волокна и должно быть экземпляром Hash.
Явное использование storage: true в настоящее время экспериментальное и может измениться в будущем.
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::Scheduler#fiber. Ruby не навязывает этому методу никакого определённого поведения.
Если планировщик не задан, метод генерирует RuntimeError (No scheduler is available!).
static VALUE
rb_fiber_s_scheduler(VALUE klass)
{
return rb_fiber_scheduler_get();
} Возвращает планировщик Fiber, который был последним задан для текущего потока с помощью Fiber.set_scheduler. Возвращает nil, если планировщик не задан (что является по умолчанию), и поведение неблокирующих волокон такое же, как у блокирующих (см. раздел «Неблокируемые волокна» в документации к классу для получения подробностей о концепции планировщика).
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::Scheduler. Его реализация зависит от пользователя.
См. также раздел «Неблокируемые волокна» в документации к классу.
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 RBOOL(!FIBER_TERMINATED_P(fiber_ptr(fiber_value)));
} Возвращает 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);
} Возвращает 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
rb_fiber_storage_get(VALUE self)
{
storage_access_must_be_from_same_fiber(self);
return rb_obj_dup(fiber_storage_get(fiber_ptr(self)));
} Возвращает копию хэша хранилища для волокна. Метод может быть вызван только для Fiber.current.
static VALUE
rb_fiber_storage_set(VALUE self, VALUE value)
{
if (rb_warning_category_enabled_p(RB_WARN_CATEGORY_EXPERIMENTAL)) {
rb_category_warn(RB_WARN_CATEGORY_EXPERIMENTAL,
"Fiber#storage= is experimental and may be removed in the future!");
}
storage_access_must_be_from_same_fiber(self);
fiber_storage_validate(value);
fiber_ptr(self)->cont.saved_ec.storage = rb_obj_dup(value);
return value;
} Устанавливает хэш хранилища для волокна. Эта функция экспериментальная и может быть изменена в будущем. Метод может быть вызван только для Fiber.current.
Следует проявлять осторожность при использовании этого метода, так как вы можете непреднамеренно очистить важные данные состояния хранения волокна. Лучше использовать присвоение конкретных ключей в хранилище с помощью Fiber::[]=.
Также можно использовать Fiber.new(storage: nil) для создания волокна с пустым хранилищем.
Пример:
while request = request_queue.pop # Reset the per-request state: Fiber.current.storage = nil handle_request(request) end
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. Аргументы, переданные в transfer, обрабатываются так же, как переданные в resume.
Два стиля передачи управления между волокнами (один – resume и Fiber::yield, другой – transfer к и от волокна) нельзя свободно смешивать.
-
Если жизненный цикл волокна начался с transfer, оно никогда не сможет использовать yield или resume для передачи управления, только завершиться или передать управление обратно. (Оно всё ещё может возобновить другие волокна, которым разрешено быть возобновлёнными.)
-
Если жизненный цикл волокна начался с resume, оно может использовать yield или transfer для другого
Fiber, но может получить управление обратно только способом, совместимым со способом, которым оно было отдано: если оно передавало управление, оно только может получить его обратно через transfer, а если уступало, только через resume. После этого оно снова может использовать transfer или yield.
Если эти правила нарушены, поднимается FiberError.
Для отдельного Fiber дизайна yield/resume проще в использовании (волокно просто отдает управление, оно не нужно думать о том, кому управление передаётся), а transfer более гибкий для сложных случаев, позволяющий строить произвольные графы взаимосвязанных волокон.
Пример:
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.