async: Simple example with `puts` hangs depending on whether newlines included
Ruby 3.1.0, async 2.0.0
There’s a high probably that this isn’t actually a bug, but in case it is:
This example works:
Async do |t|
puts "Root start\n"
t.async do
puts "Sleep 1 Start\n"
sleep 1
puts "Sleep 1 End\n"
end
t.async do
puts "Sleep 2 Start\n"
sleep 1
puts "Sleep 2 End\n"
end
puts "Root end\n"
end
and produces the following
Root start
Sleep 1 Start
Sleep 2 Start
Root end
Sleep 1 End
Sleep 2 End
But the following example, which simply removes the newlines, hangs:
Async do |t|
puts "Root start"
t.async do
puts "Sleep 1 Start"
sleep 1
puts "Sleep 1 End"
end
t.async do
puts "Sleep 2 Start"
sleep 1
puts "Sleep 2 End"
end
puts "Root end"
end
Here’s the output it produces before it hangs:
Root start
Sleep 1 StartSleep 2 Start
Root end
Sleep 2 End
I’m guessing this has something to do with sharing the stdout IO object between Fibers, and that newlines flush the IO object to put it in a more usable state to pass to another fiber, but why would that cause a hang? Is that expected?
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Reactions: 1
- Comments: 36 (18 by maintainers)
Commits related to this issue
- Fix kqueue event clobbering Fixes root issue desribed here: https://github.com/socketry/async/issues/137 The solution is to set the EV_UDATA_SPECIFIC flag on all kqueue events, which tells kqueue t... — committed to machty/io-event by machty 2 years ago
- Fix kqueue event clobbering Fixes root issue desribed here: https://github.com/socketry/async/issues/137 The solution is to set the EV_UDATA_SPECIFIC flag on all kqueue events, which tells kqueue t... — committed to socketry/io-event by machty 2 years ago
This should be completely fixed on Ruby head and in Ruby 3.2.0 when it is released.
It’s not good practice to write to the same IO from different fibers/threads generally.
IO#puts "x"internally corresponds to the following operations:This internally maps to
rb_writev_internalwithio.c:https://github.com/ruby/ruby/blob/0ca00e2cb74f9d07d27844d97c29c208caab95a7/io.c#L1180-L1200
However the fiber scheduler doesn’t support io vectors at this time… we might add support, or we might not. However, it’s totally valid to only write the first iov as a partial write and expect Ruby to retry.
However, in this case, Ruby actually assumes that the IO is not ready for writing, so instead of trying again it calls
io_wait. This schedules the IO into the event loop and ultimately kqueue. This is basically unnecessary in this case.Anyway, to cut a long story short, it maps to the following sequence of operations:
However in the fiber scheduler,
io_waitis a switch point so you end up with two fibers doing this:Ruby has a
write_lockfor multi-thread access to synchronous IO, i.e.$stdin,$stdout. But in this case, the locking isn’t around the whole operation, only thewriteoperation, so we still break whenio_waitoccurs the 2nd time. I think we should fix this, personally.I implemented the change I described above here and it fixes the issue (it produces the clean non-mangled output [and doesn’t hang]) https://github.com/ruby/ruby/commit/756b25466fda0c829498853cbd13c8ac97288bab
Output
I’m on
io-eventv1.0.2 and do not see this issue.output
Here’s me running the whole thing include the ruby --version at the top:
https://github.com/ruby/ruby/pull/5419