From: "Eregon (Benoit Daloze) via ruby-core" Date: 2023-12-28T13:05:19+00:00 Subject: [ruby-core:115957] [Ruby master Feature#13821] Allow fibers to be resumed across threads Issue #13821 has been updated by Eregon (Benoit Daloze). bascule (Tony Arcieri) wrote in #note-10: > There's a simple solution to this: track if a given fiber is holding mutexes (e.g. keep a count of them) and if it is, make Fiber#resume raise an exception if it is resumed in a different thread from the one where it was originally yielded. > > That way you eliminate the nasty edge case, but still allow fibers which aren't holding mutexes (or whatever other synchronization primitives you're worried about) to be resumed in a different thread. This may work for ::Mutex, but it won't work for any other synchronization. For example any of the locks/barriers/countdown latch in concurrent-ruby. Or even locks in C extensions. > The same solution could work for thread-local storage: disallow fiber cross-thread fiber resumption if thread local storage is in use. I think this will be so limiting that it makes creating a Fiber which can migrate between threads almost always useless in practice as such a Fiber can't e.g. call a gem as that might use thread-local storage or synchronization internally. @shan Regarding Enumerator, only next/peek create a Fiber, all other methods are fine and do not need a Fiber. Using an Enumerator in a next/peek way from multiple threads is something that is fundamentally very tricky, and that Ruby currently does not really have a concept to support it. Basically you'd want a thread-safe Java Iterator(#hasNext/#next), but there is no such protocol in Ruby. That's really tricky, and few data structures can actually support that. I think it has very limited value, because next/peek is rare, and doing next/peek in threads concurrently seems asking for concurrency bugs and data races. A Queue seems a much better thing to use to get some elements on one thread and some on another. @ioquatix > I'll put together a PR so we can experiment. This proposal breaks Fiber semantics so fundamentally I think if such a feature is added it should have another name than Fiber, to make it clear it can migrate threads and is subject to all thread races. In fact since it would have the exact same concurrency semantics as Thread, so I think it should include thread in the name. BTW, now CRuby has M-N threads (#19842), is there any reason to try this over using `RUBY_MN_THREADS=1`? One could also simulate this by creating every Fiber in a Thread with `RUBY_MN_THREADS=1` as threads should be cheaper that way. That also avoids all the problems of a Fiber changing threads dynamically, while not restricting to run a Fiber on a given thread. Migrating a Fiber to another threads seems to also have little value on CRuby due to the GVL, as it won't improve parallelism. ---------------------------------------- Feature #13821: Allow fibers to be resumed across threads https://bugs.ruby-lang.org/issues/13821#change-105914 * Author: cremes (Chuck Remes) * Status: Assigned * Priority: Normal * Assignee: ko1 (Koichi Sasada) ---------------------------------------- Given a Fiber created in ThreadA, Ruby 2.4.1 (and earlier releases) raise a FiberError if the fiber is resumed in ThreadB or any other thread other than the one that created the original Fiber. Sample code attached to demonstrate problem. If Fibers are truly encapsulating all of the data for the continuation, we should be allowed to move them between Threads and resume their operation. Why? One use-case is to support the async-await asynchronous programming model. In that model, a method marked async runs *synchronously* until the #await method is encountered. At that point the method is suspended and control is returned to the caller. When the #await method completes (asynchronously) then it may resume the suspended method and continue. The only way to capture this program state, suspend and resume, is via a Fiber. example: ``` class Wait include AsyncAwait def dofirst async do puts 'Synchronously print dofirst.' result = await { dosecond } puts 'dosecond is complete' result end end def dosecond async do puts 'Synchronously print dosecond from async task.' slept = await { sleep 3 } puts 'Sleep complete' slept end end def run task = dofirst puts 'Received task' p AsyncAwait::Task.await(task) end end Wait.new.run ``` ``` # Expected output: # Synchronous print dofirst. # Received task # Synchronously print dosecond from async task. # Sleep complete # dosecond is complete # 3 ``` Right now the best way to accomplish suspension of the #dofirst and #dosecond commands and allow them to run asynchronously is by passing those blocks to *another thread* (other than the callers thread) so they can be encapsulated in a new Fiber and then yielded. When it's time to resume after #await completes, that other thread must lookup the fiber and resume it. This is lots of extra code and logic to make sure that fibers are only resumed on the threads that created them. Allowing Fibers to migrate between threads would eliminate this problem. ---Files-------------------------------- fiber_across_threads.rb (377 Bytes) wait.rb (728 Bytes) -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/postorius/lists/ruby-core.ml.ruby-lang.org/