Spec-Zone.ru › Ruby 4.0

класс Fiber

Родительский класс:
Object

Волокна — это примитивы для реализации лёгкой кооперативной многозадачности в Ruby. По сути, они позволяют создавать блоки кода, которые можно приостанавливать и возобновлять, подобно потокам. Главное отличие заключается в том, что выполнение волокон никогда не прерывается принудительно, а планирование должен выполнять программист, а не виртуальная машина.

В отличие от других моделей лёгкой многозадачности без стека, у каждого волокна есть стек. Благодаря этому волокно можно приостановить даже при глубоко вложенных вызовах функций внутри его блока. Сведения о настройке размера стека волокон см. на странице руководства ruby(1).

После создания волокно не запускается автоматически. Чтобы запустить его, нужно явно вызвать метод Fiber#resume. Код, выполняющийся внутри волокна, может уступить управление, вызвав Fiber.yield; в этом случае управление возвращается вызывающему коду (коду, вызвавшему Fiber#resume).

При уступке управления или завершении Fiber возвращает значение последнего выполненного выражения.

Например:

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::Scheduler.

Также есть метод Fiber.schedule, который должен немедленно выполнить заданный блок в неблокирующем режиме. Его фактическая реализация зависит от планировщика.

Публичные методы класса

Fiber[key] → value Показать исходный код
static VALUE
rb_fiber_storage_aref(VALUE class, VALUE key)
{
    key = rb_to_symbol(key);

    VALUE storage = fiber_storage_get(fiber_current(), FALSE);
    if (storage == Qnil) return Qnil;

    return rb_hash_aref(storage, key);
}

Возвращает значение переменной хранилища волокна, идентифицируемой с помощью key.

key должен быть символом, а значение задаётся с помощью Fiber#[]= или Fiber#storage.

См. также Fiber::[]=.

Fiber[key] = value Показать исходный код
static VALUE
rb_fiber_storage_aset(VALUE class, VALUE key, VALUE value)
{
    key = rb_to_symbol(key);

    VALUE storage = fiber_storage_get(fiber_current(), value != Qnil);
    if (storage == Qnil) return Qnil;

    if (value == Qnil) {
        return rb_hash_delete(storage, key);
    }
    else {
        return rb_hash_aset(storage, key, value);
    }
}

Присваивает value переменной хранилища волокна, идентифицируемой с помощью key. Если переменная не существует, она создаётся.

key должен быть Symbol, иначе будет вызвано исключение TypeError.

См. также Fiber::[].

blocking{|fiber| ...} → result Показать исходный код
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);
    }
}

Принудительно переводит волокно в блокирующий режим на время выполнения блока. Возвращает результат выполнения блока.

Подробности см. в разделе «Неблокирующие волокна» документации класса.

blocking? → false or 1 Показать исходный код
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.

Подробности см. в разделе «Неблокирующие волокна» документации класса.

current → fiber Показать исходный код
static VALUE
rb_fiber_s_current(VALUE klass)
{
    return rb_fiber_current();
}

Возвращает текущее волокно. Если код выполняется не в контексте волокна, этот метод возвращает корневое волокно.

current_scheduler → obj or nil Показать исходный код
static VALUE
rb_fiber_current_scheduler(VALUE klass)
{
    return rb_fiber_scheduler_current();
}

Возвращает планировщик Fiber, последний раз заданный для текущего потока с помощью Fiber.set_scheduler, только если текущее волокно является неблокирующим.

new(blocking: false, storage: true) { |*args| ... } → fiber Показать исходный код
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 ...

Если при вызове Fiber.new передан blocking: false и для текущего потока задан 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 в настоящее время является экспериментальным и может измениться в будущем.

schedule { |*args| ... } → 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::Scheduler#fiber текущим планировщиком. Ruby не требует от этого метода какого-либо определённого поведения.

Если планировщик не задан, метод вызывает исключение RuntimeError (No scheduler is available!).

scheduler → obj or nil Показать исходный код
static VALUE
rb_fiber_s_scheduler(VALUE klass)
{
    return rb_fiber_scheduler_get();
}

Возвращает планировщик Fiber, последний раз заданный для текущего потока с помощью Fiber.set_scheduler. Возвращает nil, если планировщик не задан (это значение используется по умолчанию); в таком случае неблокирующие волокна ведут себя так же, как блокирующие. (Подробности о понятии планировщика см. в разделе «Неблокирующие волокна» документации класса.)

set_scheduler(scheduler) → scheduler Показать исходный код
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. Реализация предоставляется пользователем.

См. также раздел «Неблокирующие волокна» документации класса.

yield(args, ...) → obj Показать исходный код
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.

Публичные методы экземпляра

alive? → true or false Показать исходный код
VALUE
rb_fiber_alive_p(VALUE fiber_value)
{
    return RBOOL(!FIBER_TERMINATED_P(fiber_ptr(fiber_value)));
}

Возвращает true, если волокно всё ещё можно возобновить (или передать ему управление). После завершения выполнения блока волокна этот метод всегда возвращает false.

backtrace → array Показать исходный код
backtrace(start) → array
backtrace(start, count) → array
backtrace(start..end) → array
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
backtrace_locations → array Показать исходный код
backtrace_locations(start) → array
backtrace_locations(start, count) → array
backtrace_locations(start..end) → array
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
blocking? → true or false Показать исходный код
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.

Подробности см. в разделе «Неблокирующие волокна» документации класса.

inspect ()
Псевдоним для: to_s
kill → nil Показать исходный код
static VALUE
rb_fiber_m_kill(VALUE self)
{
    rb_fiber_t *fiber = fiber_ptr(self);

    if (fiber->killed) return Qfalse;
    fiber->killed = 1;

    if (fiber->status == FIBER_CREATED) {
        fiber->status = FIBER_TERMINATED;
    }
    else if (fiber->status != FIBER_TERMINATED) {
        if (fiber_current() == fiber) {
            fiber_check_killed(fiber);
        }
        else {
            fiber_raise(fiber_ptr(self), Qnil);
        }
    }

    return self;
}

Завершает волокно, вызывая перехватываемое исключение. Завершается только указанное волокно, а не другие; если другое волокно вызвало resume или transfer, ему возвращается nil.

Fiber#kill прерывает другое волокно только тогда, когда оно находится внутри Fiber.yield. Если вызвать метод для текущего волокна, исключение будет вызвано в месте вызова Fiber#kill.

Если волокно ещё не запускалось, оно сразу переходит в состояние завершения.

Если волокно уже завершено, ничего не происходит.

Если метод вызван для волокна, принадлежащего другому потоку, возникает исключение FiberError.

raise(exception, message = exception.to_s, backtrace = nil, cause: $!) Показать исходный код
raise(message = nil, cause: $!)
static VALUE
rb_fiber_m_raise(int argc, VALUE *argv, VALUE self)
{
    return rb_fiber_raise(self, argc, argv);
}

Вызывает исключение в волокне в точке, где последний раз был вызван Fiber.yield.

f = Fiber.new {
  puts "Before the yield"
  Fiber.yield 1 # -- exception will be raised here
  puts "After the yield"
}

p f.resume
f.raise "Gotcha"

Вывод

Before the first yield
1
t.rb:8:in 'Fiber.yield': Gotcha (RuntimeError)
  from t.rb:8:in 'block in <main>'

Если волокно ещё не запускалось или уже завершило выполнение, возникает исключение FiberError. Если волокно уступает управление, оно возобновляется. Если оно передаёт управление, выполнение переключается на него. Если же оно возобновляется, возникает исключение FiberError.

Если метод вызван для Fiber, принадлежащего другому Thread, возникает исключение FiberError.

Дополнительные сведения об аргументах см. в описании Kernel#raise.

resume(args, ...) → obj Показать исходный код
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.

storage → hash (dup) Показать исходный код
static VALUE
rb_fiber_storage_get(VALUE self)
{
    storage_access_must_be_from_same_fiber(self);

    VALUE storage = fiber_storage_get(fiber_ptr(self), FALSE);

    if (storage == Qnil) {
        return Qnil;
    }
    else {
        return rb_obj_dup(storage);
    }
}

Возвращает копию хеша хранилища волокна. Метод можно вызывать только для Fiber.current.

storage = hash Показать исходный код
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
to_s () Показать исходный код
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);
}
Также имеет псевдоним: inspect
transfer(args, ...) → obj Показать исходный код
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, оно больше не сможет уступать управление или получать его через resume; оно может только завершиться или передать управление обратно. (При этом оно по-прежнему может возобновлять другие волокна, которым разрешено возобновление.)

  • Если жизненный цикл волокна начался с resume, оно может уступить управление или передать его другому Fiber, но получить управление обратно может только тем же способом, которым оно было передано: если управление было передано через transfer, его можно вернуть только через transfer; если оно было уступлено, вернуть его можно только через resume. После этого волокно снова может передавать или уступать управление.

При нарушении этих правил возникает исключение FiberError.

Для отдельного сценария с Fiber использовать yield/resume проще (волокно Fiber просто уступает управление и ему не нужно знать, кому оно передаётся), тогда как 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–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API