class Fiber
Fibers sind Primitiven zur Implementierung leichter kooperativer Nebenläufigkeit in Ruby. Grundsätzlich sind sie ein Mittel zur Erstellung von Codeblöcken, die wie Threads unterbrochen und fortgesetzt werden können. Der Hauptunterschied besteht darin, dass sie nie präemptiv unterbrochen werden und die Planung vom Programmierer und nicht von der VM durchgeführt werden muss.
Im Gegensatz zu anderen Stackless-Leichtgewichts-Nebenläufigkeitsmodellen verfügt jede Fiber über einen Stack. Dies ermöglicht es der Fiber, von tief verschachtelten Funktionsaufrufen innerhalb des Fiber-Blocks aus angehalten zu werden. Sehen Sie sich das Handbuch ruby(1) an, um die Größe des Fiber-Stacks zu konfigurieren.
Wenn eine Fiber erstellt wird, läuft sie nicht automatisch. Vielmehr muss sie explizit mit der Methode Fiber#resume zum Laufen aufgefordert werden. Der Code, der innerhalb der Fiber läuft, kann die Kontrolle aufgeben, indem er Fiber.yield aufruft, in welchem Fall die Kontrolle an den Aufrufer zurückgegeben wird (den Aufrufer von Fiber#resume).
Bei der Übergabe oder Beendigung gibt die Fiber den Wert des zuletzt ausgeführten Ausdrucks zurück.
Zum Beispiel
fiber = Fiber.new do Fiber.yield 1 2 end puts fiber.resume puts fiber.resume puts fiber.resume
ergibt
1 2 FiberError: dead fiber called
Die Methode Fiber#resume akzeptiert eine beliebige Anzahl von Parametern. Wenn es sich um den ersten Aufruf von resume handelt, werden diese als Blockargumente übergeben. Andernfalls sind sie der Rückgabewert des Aufrufs von Fiber.yield.
Beispiel
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"
ergibt
12 1000000 FiberError: dead fiber called
Nicht-blockierende Fibers
Das Konzept der nicht-blockierenden Fiber wurde in Ruby 3.0 eingeführt. Eine nicht-blockierende Fiber gibt die Kontrolle an andere Fibers ab, wenn sie auf eine Operation stößt, die die Fiber normalerweise blockieren würde (wie sleep oder das Warten auf einen anderen Prozess oder E/A). Dies ermöglicht es dem Scheduler, das Blockieren und Aufwecken (Fortsetzen) dieser Fiber zu handhaben, wenn sie fortfahren kann.
Damit eine Fiber sich nicht-blockierend verhält, muss sie in Fiber.new mit blocking: false (was der Standard ist) erstellt werden und Fiber.scheduler muss mit Fiber.set_scheduler gesetzt werden. Wenn Fiber.scheduler im aktuellen Thread nicht gesetzt ist, ist das Verhalten von blockierenden und nicht-blockierenden Fibers identisch.
Ruby stellt keine Scheduler-Klasse zur Verfügung: sie muss vom Benutzer implementiert werden und entspricht Fiber::Scheduler.
Es gibt auch die Methode Fiber.schedule, die erwartet, dass sie den angegebenen Block sofort nicht-blockierend ausführt. Ihre tatsächliche Implementierung liegt beim Scheduler.
Öffentliche Klassenmethoden
Source
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);
}
Gibt den Wert der Fiber-Speichervariablen zurück, die durch Schlüssel identifiziert wird.
Der Schlüssel muss ein Symbol sein, und der Wert wird von Fiber#[]= oder Fiber#speicher gesetzt.
Siehe auch Fiber::[]=.
Source
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);
}
}
Source
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);
}
}
Zwingt die Fiber, während der Dauer des Blocks blockierend zu sein. Gibt das Ergebnis des Blocks zurück.
Siehe den Abschnitt "Nicht-blockierende Fibers" in der Klassendokumentation für Details.
Source
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);
}
Gibt falsch zurück, wenn die aktuelle Fiber nicht blockiert. Eine Fiber ist nicht blockierend, wenn sie durch Übergabe von blocking: false an Fiber.new oder durch Fiber.schedule erstellt wurde.
Wenn die aktuelle Fiber blockiert, gibt die Methode 1 zurück. Zukünftige Entwicklungen könnten Situationen zulassen, in denen größere ganze Zahlen zurückgegeben werden könnten.
Beachten Sie, dass, selbst wenn die Methode falsch zurückgibt, das Verhalten der Fiber nur dann anders ist, wenn Fiber.scheduler im aktuellen Thread gesetzt ist.
Siehe den Abschnitt "Nicht-blockierende Fibers" in der Klassendokumentation für Details.
Source
static VALUE
rb_fiber_s_current(VALUE klass)
{
return rb_fiber_current();
}
Gibt die aktuelle Fiber zurück. Wenn Sie sich nicht im Kontext einer Fiber befinden, gibt diese Methode die Root-Fiber zurück.
Source
static VALUE
rb_fiber_current_scheduler(VALUE klass)
{
return rb_fiber_scheduler_current();
}
Gibt den Fiber Scheduler zurück, der zuletzt für den aktuellen Thread mit Fiber.set_scheduler gesetzt wurde, und zwar nur dann, wenn die aktuelle Fiber nicht blockiert.
Source
static VALUE
rb_fiber_initialize(int argc, VALUE* argv, VALUE self)
{
return rb_fiber_initialize_kw(argc, argv, self, rb_keyword_given_p());
}
Erstellt eine neue Fiber. Anfangs läuft die Fiber nicht und kann mit resume fortgesetzt werden. Argumente für den ersten resume Aufruf werden an den Block übergeben.
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 ...
Wenn blocking: false an Fiber.new übergeben wird und der aktuelle Thread einen definierten Fiber.scheduler hat, wird die Fiber nicht-blockierend (siehe Abschnitt "Nicht-blockierende Fibers" in der Klassendokumentation).
Wenn storage nicht angegeben ist, wird standardmäßig eine Kopie des Speichers von der aktuellen Fiber übernommen. Dies ist dasselbe wie storage: true.
Fiber[:x] = 1 Fiber.new do Fiber[:x] # => 1 Fiber[:x] = 2 end.resume Fiber[:x] # => 1
Wenn der angegebene storage nil ist, initialisiert diese Funktion den internen Speicher verzögert, der als leeres Hash beginnt.
Fiber[:x] = "Hello World" Fiber.new(storage: nil) do Fiber[:x] # nil end
Andernfalls wird der angegebene storage als Speicher der neuen Fiber verwendet, und er muss eine Instanz von Hash sein.
Die explizite Verwendung von storage: true ist derzeit experimentell und kann sich in Zukunft ändern.
Source
static VALUE
rb_fiber_s_schedule(int argc, VALUE *argv, VALUE obj)
{
return rb_fiber_s_schedule_kw(argc, argv, rb_keyword_given_p());
}
Die Methode soll den bereitgestellten Codeblock sofort in einer separaten, nicht-blockierenden Fiber ausführen.
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"
Unter der Annahme, dass MyScheduler ordnungsgemäß implementiert ist, wird dieses Programm Folgendes erzeugen:
Go to sleep! Going to sleep Wakey-wakey, sleepyhead ...1 sec pause here... I slept well
... z. B. bei der ersten blockierenden Operation innerhalb der Fiber (sleep(1)) wird die Kontrolle an den externen Code (Haupt-Fiber) übergeben, und *am Ende dieser Ausführung* kümmert sich der Scheduler ordnungsgemäß um das Fortsetzen aller blockierten Fibers.
Beachten Sie, dass das beschriebene Verhalten das ist, was die Methode *erwartet* zu tun; das tatsächliche Verhalten hängt von der Implementierung der Methode Fiber::Scheduler#fiber durch den aktuellen Scheduler ab. Ruby erzwingt nicht, dass diese Methode sich in irgendeiner bestimmten Weise verhält.
Wenn der Scheduler nicht gesetzt ist, löst die Methode RuntimeError (Kein Scheduler verfügbar!) aus.
Source
static VALUE
rb_fiber_s_scheduler(VALUE klass)
{
return rb_fiber_scheduler_get();
}
Gibt den Fiber Scheduler zurück, der zuletzt für den aktuellen Thread mit Fiber.set_scheduler gesetzt wurde. Gibt nil zurück, wenn kein Scheduler gesetzt ist (was der Standard ist), und das Verhalten nicht-blockierender Fibers ist dasselbe wie bei blockierenden. (Siehe Abschnitt "Nicht-blockierende Fibers" in der Klassendokumentation für Details zum Scheduler-Konzept).
Source
static VALUE
rb_fiber_set_scheduler(VALUE klass, VALUE scheduler)
{
return rb_fiber_scheduler_set(scheduler);
}
Setzt den Fiber Scheduler für den aktuellen Thread. Wenn der Scheduler gesetzt ist, rufen nicht-blockierende Fibers (erstellt von Fiber.new mit blocking: false oder von Fiber.schedule) die Hook-Methoden des Schedulers bei potenziell blockierenden Operationen auf, und der aktuelle Thread ruft die close-Methode des Schedulers bei der Finalisierung auf (was es dem Scheduler ermöglicht, alle nicht abgeschlossenen Fibers ordnungsgemäß zu verwalten).
scheduler kann ein Objekt einer beliebigen Klasse sein, die Fiber::Scheduler entspricht. Seine Implementierung liegt beim Benutzer.
Siehe auch den Abschnitt "Nicht-blockierende Fibers" in der Klassendokumentation.
Source
static VALUE
rb_fiber_s_yield(int argc, VALUE *argv, VALUE klass)
{
return rb_fiber_yield_kw(argc, argv, rb_keyword_given_p());
}
Gibt die Kontrolle an den Kontext zurück, der die Fiber fortgesetzt hat, und übergibt alle Argumente, die an sie übergeben wurden. Die Fiber wird an diesem Punkt die Verarbeitung fortsetzen, wenn resume das nächste Mal aufgerufen wird. Alle Argumente, die an das nächste resume übergeben werden, sind der Wert, zu dem dieser Fiber.yield Ausdruck ausgewertet wird.
Öffentliche Instanzmethoden
Source
VALUE
rb_fiber_alive_p(VALUE fiber_value)
{
return RBOOL(!FIBER_TERMINATED_P(fiber_ptr(fiber_value)));
}
Gibt true zurück, wenn die Fiber noch fortgesetzt (oder übertragen) werden kann. Nach Abschluss der Ausführung des Fiber-Blocks gibt diese Methode immer falsch zurück.
Source
static VALUE
rb_fiber_backtrace(int argc, VALUE *argv, VALUE fiber)
{
return rb_vm_backtrace(argc, argv, &fiber_ptr(fiber)->cont.saved_ec);
}
Gibt den aktuellen Ausführungsstack der Fiber zurück. start, count und end erlauben die Auswahl nur von Teilen des Backtrace.
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
Source
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);
}
Wie backtrace, gibt aber jede Zeile des Ausführungsstacks als Thread::Backtrace::Location zurück. Akzeptiert die gleichen Argumente wie backtrace.
f = Fiber.new { Fiber.yield } f.resume loc = f.backtrace_locations.first loc.label #=> "yield" loc.path #=> "test.rb" loc.lineno #=> 1
Source
VALUE
rb_fiber_blocking_p(VALUE fiber)
{
return RBOOL(fiber_ptr(fiber)->blocking);
}
Gibt true zurück, wenn fiber blockiert, und falsch andernfalls. Eine Fiber ist nicht blockierend, wenn sie durch Übergabe von blocking: false an Fiber.new oder durch Fiber.schedule erstellt wurde.
Beachten Sie, dass, selbst wenn die Methode falsch zurückgibt, das Verhalten der Fiber nur dann anders ist, wenn Fiber.scheduler im aktuellen Thread gesetzt ist.
Siehe den Abschnitt "Nicht-blockierende Fibers" in der Klassendokumentation für Details.
Source
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;
}
Beendet die Fiber, indem eine nicht abfangbare Ausnahme ausgelöst wird. Sie beendet nur die gegebene Fiber und keine andere Fiber, und gibt nil an eine andere Fiber zurück, wenn diese Fiber resume oder transfer aufgerufen hat.
Fiber#kill unterbricht eine andere Fiber nur, wenn sie sich in Fiber.yield befindet. Wenn sie auf die aktuelle Fiber angewendet wird, löst sie diese Ausnahme an der Aufrufstelle von Fiber#kill aus.
Wenn die Fiber noch nicht gestartet wurde, geht sie direkt in den beendeten Zustand über.
Wenn die Fiber bereits beendet ist, tut sie nichts.
Löst FiberError aus, wenn sie auf eine Fiber eines anderen Threads angewendet wird.
Source
static VALUE
rb_fiber_m_raise(int argc, VALUE *argv, VALUE self)
{
return rb_fiber_raise(self, argc, argv);
}
Löst eine Ausnahme in der Fiber an der Stelle aus, an der der letzte Fiber.yield aufgerufen wurde.
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"
Ausgabe
Before the first yield 1 t.rb:8:in 'Fiber.yield': Gotcha (RuntimeError) from t.rb:8:in 'block in <main>'
Wenn die Fiber nicht gestartet wurde oder bereits abgeschlossen ist, wird FiberError ausgelöst. Wenn die Fiber yieldet, wird sie fortgesetzt. Wenn sie transferiert, wird sie hinein transferiert. Wenn sie aber resume wird, wird FiberError ausgelöst.
Löst FiberError aus, wenn sie auf eine Fiber eines anderen Thread angewendet wird.
Siehe Kernel#raise für weitere Informationen zu Argumenten.
Source
static VALUE
rb_fiber_m_resume(int argc, VALUE *argv, VALUE fiber)
{
return rb_fiber_resume_kw(fiber, argc, argv, rb_keyword_given_p());
}
Setzt die Fiber ab dem Punkt fort, an dem der letzte Fiber.yield aufgerufen wurde, oder startet sie, wenn dies der erste Aufruf von resume ist. An resume übergebene Argumente sind der Wert des Fiber.yield-Ausdrucks oder werden als Blockparameter an den Block der Fiber übergeben, wenn dies das erste resume ist.
Alternativ wird resume, wenn es aufgerufen wird, zu den Argumenten ausgewertet, die an die nächste Fiber.yield-Anweisung innerhalb des Fiber-Blocks übergeben werden, oder zum Blockwert, wenn er ohne Fiber.yield abgeschlossen wird.
Source
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);
}
}
Gibt eine Kopie des Speichers-Hash für die Fiber zurück. Die Methode kann nur auf die Fiber.current angewendet werden.
Source
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;
}
Setzt den Speicher-Hash für die Fiber. Dieses Feature ist experimentell und kann sich in Zukunft ändern. Die Methode kann nur auf die Fiber.current angewendet werden.
Sie sollten vorsichtig mit der Verwendung dieser Methode sein, da Sie möglicherweise versehentlich wichtige Fiber-Speicherzustände löschen. Sie sollten vorzugsweise bestimmte Schlüssel im Speicher mit Fiber::[]= zuweisen.
Sie können auch Fiber.new(storage: nil) verwenden, um eine Fiber mit einem leeren Speicher zu erstellen.
Beispiel
while request = request_queue.pop # Reset the per-request state: Fiber.current.storage = nil handle_request(request) end
Source
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);
}
Source
static VALUE
rb_fiber_m_transfer(int argc, VALUE *argv, VALUE self)
{
return rb_fiber_transfer_kw(self, argc, argv, rb_keyword_given_p());
}
Überträgt die Kontrolle an eine andere Fiber, setzt sie dort fort, wo sie zuletzt angehalten hat, oder startet sie, wenn sie noch nicht fortgesetzt wurde. Die aufrufende Fiber wird wie bei einem Aufruf von Fiber.yield suspendiert.
Die Fiber, die den Transferaufruf empfängt, behandelt ihn ähnlich wie einen Resume-Aufruf. An transfer übergebene Argumente werden wie an resume übergebene Argumente behandelt.
Die beiden Arten der Steuerung zwischen Fiber (eine ist resume und Fiber::yield, die andere ist transfer zu und von Fiber) können nicht frei gemischt werden.
-
Wenn der Lebenszyklus der Fiber mit transfer begonnen hat, kann sie niemals yielden oder die Kontrolle zurückerhalten, sondern nur beenden oder zurücktransferieren. (Sie kann immer noch andere Fibers fortsetzen, die fortgesetzt werden dürfen.)
-
Wenn der Lebenszyklus der Fiber mit resume begonnen hat, kann sie an eine andere
Fibertransferieren oder yielden, kann aber die Kontrolle nur auf die Weise zurückerhalten, die mit der Art und Weise kompatibel ist, wie sie weggegeben wurde: Wenn sie transferiert hat, kann sie nur zurücktransferiert werden, und wenn sie yieldet hat, kann sie nur zurück resumed werden. Danach kann sie wieder transferieren oder yielden.
Wenn diese Regeln gebrochen werden, wird FiberError ausgelöst.
Für das Design einer einzelnen Fiber ist yield/resume einfacher zu verwenden (die Fiber gibt einfach die Kontrolle ab, sie muss nicht darüber nachdenken, an wen die Kontrolle gegeben wird), während transfer flexibler für komplexe Fälle ist und den Aufbau beliebiger Graphen voneinander abhängiger Fibers ermöglicht.
Beispiel
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
ergibt
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