#!/usr/bin/env rubyrequire'tempfile'Tempfile.create(['foo','.rb'])do|file|file.write(<<~RUBY)
#
$stderr.puts 1; q = Queue.new
$stderr.puts 2; t = Thread.new{q.pop}
$stderr.puts 3; q << :sig
$stderr.puts 4; t.join
sleep 1
class C
end
RUBYfile.closeautoload:C,file.pathThread.newdothreads=3.times.mapdo|i|Thread.newdo$stderr.puts"LOADING C"$stderr.putsCendendthreads.each(&:join)end.joinend
This one doesn't:
#!/usr/bin/env rubyrequire'tempfile'require_relative'lib/async'Tempfile.create(['foo','.rb'])do|file|file.write(<<~RUBY)
#
$stderr.puts 1; q = Queue.new
$stderr.puts 2; t = Thread.new{q.pop}
$stderr.puts 3; q << :sig
$stderr.puts 4; t.join
class C
end
RUBYfile.closeautoload:C,file.pathAsyncdo|task|3.timesdo|i|task.asyncdo$stderr.puts"LOADING C"$stderr.putsCendendend.waitend
Semantically, they should be very similar. It feels like someone is checking the current thread rather than the current fiber or there is a poor implementation of locking somewhere, however I don't actually know for sure yet, investigation is required.
#!/usr/bin/env rubyrequire'tempfile'Tempfile.create(['foo','.rb'])do|file|file.write(<<~RUBY)
Fiber.yield
class C
end
RUBYfile.closeautoload:C,file.path3.timesdo|i|fiber=Fiber.newdo$stderr.puts"#{i} LOADING C"$stderr.putsCrescueNameError$stderr.puts"NameError: #{i}"raiseendfiber.resumeendend
staticVALUEautoload_sleep(VALUEarg){structautoload_state*state=(structautoload_state*)arg;/*
* autoload_reset in other thread will resume us and remove us
* from the waitq list
*/do{rb_thread_sleep_deadly();}while(state->thread!=Qfalse);returnQfalse;}
It basically spins on state->thread.
I hesitate to call it poorly implemented, but it doesn’t look great. I don’t fully understand the implementation yet.
@fxn If I understand correctly, autoload is mostly a feature of a development environment, so if we made this a little bit slower in order to improve accuracy (i.e. using a proper mutex), it wouldn't be huge loss right? Ruby's mutex isn't that slow but I realise if we took this route, it would introduce some overhead.
The feature in development for a web application is reloading. Ordinary gems using Zeitwerk may autoload in any project they are used, and when you eager load Rails in production you may need to autoload top-level constants like superclasses or mixins while eager loading, for example.
However, as the say goes, I can be fast if I can be wrong :). I'd go with that mutex, probably not a big deal, and fibers are key for the future of Ruby, so better be compatible in my view.
I've tracked down the root of this bug, being that it's not yielding to the fiber scheduler and implements it's own condition variable like semantics. I'll propose a fix.
I've confirmed that my PR fixes the given examples here.
There is a tiny bit of extra overhead; using a mutex has an object allocation, mutex lock and unlock, etc.
A light weight concurrency construct rb_condition or rb_fiber_condition that behaves like a "pessimistic mutex", i.e. avoids the allocation until it's actually needed might be a better solution but let's get it correct first, and we can optimise it later.
Just a few notes:
structrb_condition{...waitq;}VALUErb_condition_wait(structrb_condition*condition,VALUEtimeout);rb_condition_signal(structrb_condition*condition,VALUEresult);// wake up all waiters