Why fiber scheduler work different when switch fibers use Channel#send OR Fiber.yield?

Following is a example.

ch = Channel(UInt128).new
terminate = Channel(Nil).new
done = Channel(Nil).new

[1, 2, 3].each do |i|
  spawn do
    x = 0_u128
    y = rand(5000000..10000000)
    puts "6"*50
    (1..y).each { |e| x += e }
    puts "7"*50
    ch.send x
    puts "8"*50
  end
end

spawn do
  loop do
    select
    when value = ch.receive
      puts "4"*50
      p value
    when terminate.receive?
      puts "3"*50
      break
    end
  end
  done.close
  puts "exiting loop fiber"
end

puts "1"*50
terminate.close
puts "2"*50
done.receive?
puts "end"

When i run above code, i get result like following:

11111111111111111111111111111111111111111111111111
22222222222222222222222222222222222222222222222222

66666666666666666666666666666666666666666666666666
77777777777777777777777777777777777777777777777777
66666666666666666666666666666666666666666666666666
77777777777777777777777777777777777777777777777777
66666666666666666666666666666666666666666666666666
77777777777777777777777777777777777777777777777777

44444444444444444444444444444444444444444444444444
31855631199211
44444444444444444444444444444444444444444444444444
45854814984355
44444444444444444444444444444444444444444444444444
15626260923210

33333333333333333333333333333333333333333333333333
exiting loop fiber

88888888888888888888888888888888888888888888888888
88888888888888888888888888888888888888888888888888
88888888888888888888888888888888888888888888888888

end

If I change the 20th line of code from ch.send x into Fiber.yield, like following:

ch = Channel(UInt128).new
terminate = Channel(Nil).new
done = Channel(Nil).new

[1, 2, 3].each do |i|
  spawn do
    x = 0_u128
    y = rand(5000000..10000000)
    puts "6"*50
    (1..y).each { |e| x += e }
    puts "7"*50
    Fiber.yield
    puts "8"*50
  end
end

spawn do
  loop do
    select
    when value = ch.receive
      puts "4"*50
      p value
    when terminate.receive?
      puts "3"*50
      break
    end
  end
  done.close
  puts "exiting loop fiber"
end

puts "1"*50
terminate.close
puts "2"*50
done.receive?
puts "end"

I got new output like following:

11111111111111111111111111111111111111111111111111
22222222222222222222222222222222222222222222222222

66666666666666666666666666666666666666666666666666
77777777777777777777777777777777777777777777777777
66666666666666666666666666666666666666666666666666
77777777777777777777777777777777777777777777777777
66666666666666666666666666666666666666666666666666
77777777777777777777777777777777777777777777777777

33333333333333333333333333333333333333333333333333
exiting loop fiber

end

My question is, why the first code output following 888888

88888888888888888888888888888888888888888888888888
88888888888888888888888888888888888888888888888888
88888888888888888888888888888888888888888888888888

but it does not for the second code?

the first code scheduler switch to the blocked fibers in the each before loop's fiber exit so puts "8"*50, but, it’s does not happen for second code.

What are the differences when fiber scheduler works for above two code?

Before switch back to the done.receive in the main fiber, main fiber will waiting for all spawned fiber to be fulled blocked/exit or not? above two codes get opposite result, causing me very confused.

Test on 1.14.0, thanks


EDIT:

If replace ch.send x with sleep 1 or sleep 0, the result same as Fiber.yield, only ch.send x behavior different.

The only explanation I can think of is that Fiber scheduler will tries switch to fibers which blocked by channel operation (e.g. ch.send) and execute them before main fiber exit, but this is not the case when used with Fiber.yield/sleep 0/sleep 1?

The problem is not the scheduler. The sub fibers gets called once only. The yield and sleep version miss a way to reenter/continue the paused fibers.

Thanks for the answer, so, only operation on the channel can reenter/continue the paused fiber, right? another way to say it, an unbuffered channel will be executed before the main fiber?

Is this true for sleep 1 too? i means, sleep 1 can explain as: i want to rest a second, come back later, it expected to back later.

It is not the only way: inserting yields in your code

done.receive?
Fiber.yield
Fiber.yield
Fiber.yield
puts "end"

reenters the paused fibers

It is not the only way: inserting yields in your code

Thanks, i consider this is another story, I want to confirm if there is a more reasonable explanation (or doc) to explain the difference behavior between Fiber.yield/sleep and channel operation, that is, why ch.send x version works even even without need Fiber.yield?

BTW: i tested with two Fiber.yield added, it works same as three.

But i thought it should work same way if only one Fiber.yield added, but the result is only output 88888 twice instead expected 3 times, could you explain why?

done.receive?
Fiber.yield
puts "end"

Thanks

with Fiber.yield you order scheduler to switch.
with sleep you pause the fiber and the scheduler will look wether there are other fibers waiting = ready to go on.
with send you mark data to be worked on elsewhere but continue in that fiber.

1 Like

After check scheduler tracing log, it become more clearly for now, as original answer supplement:

with Fiber.yield you force switch, manually.

with sleep or Channel operation, just activate rescheduler event, whether switch or not, or switch to which fiber, need re-calculate, because both of them affect orders. e.g. ch.receive probably add the switched out fiber back, sleep time different will affect fiber order.

There are many things that can be said, but, limited by English is not my native language, it’s difficult to express.