From 4495dea153a097c59d56819bc827bebfbef5be3f Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Tue, 20 Dec 2022 22:10:37 +0100 Subject: [PATCH] Improve documentation for fiber-scoped variables * Especially around Enumerator. --- NEWS.md | 4 ++-- cont.c | 4 ++-- enumerator.c | 45 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/NEWS.md b/NEWS.md index 6d4b1221ff..744cf6a580 100644 --- a/NEWS.md +++ b/NEWS.md @@ -103,8 +103,8 @@ Note: We're only listing outstanding class updates. fiber. [[Feature #19078]] Existing Thread and Fiber local variables can be tricky to use. - Thread local variables are shared between all fibers, making it - hard to isolate, while Fiber local variables can be hard to + Thread-local variables are shared between all fibers, making it + hard to isolate, while Fiber-local variables can be hard to share. It is often desirable to define unit of execution ("execution context") such that some state is shared between all fibers and threads created in that context. This is what Fiber diff --git a/cont.c b/cont.c index 89dcd49545..71d93cebd0 100644 --- a/cont.c +++ b/cont.c @@ -2153,7 +2153,7 @@ rb_fiber_storage_set(VALUE self, VALUE value) /** * call-seq: Fiber[key] -> value * - * Returns the value of the fiber-local variable identified by +key+. + * Returns the value of the fiber-scoped variable identified by +key+. * * The +key+ must be a symbol, and the value is set by Fiber#[]= or * Fiber#store. @@ -2176,7 +2176,7 @@ rb_fiber_storage_aref(VALUE class, VALUE key) /** * call-seq: Fiber[key] = value * - * Assign +value+ to the fiber-local variable identified by +key+. + * Assign +value+ to the fiber-scoped variable identified by +key+. * The variable is created if it doesn't exist. * * +key+ must be a Symbol, otherwise a TypeError is raised. diff --git a/enumerator.c b/enumerator.c index a4e9d9a5c8..cba4369606 100644 --- a/enumerator.c +++ b/enumerator.c @@ -73,6 +73,8 @@ * puts %w[foo bar baz].map.with_index { |w, i| "#{i}:#{w}" } * # => ["0:foo", "1:bar", "2:baz"] * + * == External Iteration + * * An Enumerator can also be used as an external iterator. * For example, Enumerator#next returns the next value of the iterator * or raises StopIteration if the Enumerator is at the end. @@ -83,15 +85,44 @@ * puts e.next # => 3 * puts e.next # raises StopIteration * - * Note that enumeration sequence by +next+, +next_values+, +peek+ and - * +peek_values+ do not affect other non-external - * enumeration methods, unless the underlying iteration method itself has - * side-effect, e.g. IO#each_line. + * +next+, +next_values+, +peek+ and +peek_values+ are the only methods + * which use external iteration (and Array#zip(Enumerable-not-Array) which uses +next+). * - * Moreover, implementation typically uses fibers so performance could be - * slower and exception stacktraces different than expected. + * These methods do not affect other internal enumeration methods, + * unless the underlying iteration method itself has side-effect, e.g. IO#each_line. * - * You can use this to implement an internal iterator as follows: + * External iteration differs *significantly* from internal iteration + * due to using a Fiber: + * - The Fiber adds some overhead compared to internal enumeration. + * - The stacktrace will only include the stack from the Enumerator, not above. + * - Fiber-local variables are *not* inherited inside the Enumerator Fiber, + * which instead starts with no Fiber-local variables. + * - Fiber-scoped variables *are* inherited and are designed + * to handle Enumerator Fibers. Assigning to a Fiber-scope variable + * only affects the current Fiber, so if you want to change state + * in the caller Fiber of the Enumerator Fiber, you need to use an + * extra indirection (e.g., use some object in the Fiber-scoped + * variable and mutate some ivar of it). + * + * Concretely: + * Thread.current[:fiber_local] = 1 + * Fiber[:scoped_var] = 1 + * e = Enumerator.new do |y| + * p Thread.current[:fiber_local] # for external iteration: nil, for internal iteration: 1 + * p Fiber[:scoped_var] # => 1, inherited + * Fiber[:scoped_var] += 1 + * y << 42 + * end + * + * p e.next # => 42 + * p Fiber[:scoped_var] # => 1 (it ran in a different Fiber) + * + * e.each { p _1 } + * p Fiber[:scoped_var] # => 2 (it ran in the same Fiber/"stack" as the current Fiber) + * + * == Convert External Iteration to Internal Iteration + * + * You can use an external iterator to implement an internal iterator as follows: * * def ext_each(e) * while true