Use ensure clause in spawn block, can't guarantee code in ensure always be execute, is expected behavior?

Following is a example:

spawn do
  puts "start"
  sleep 1 # yield to main fiber

ensure
  puts "done!"
end

Fiber.yield

The output is:

 ╰─ $ cr test.cr 
start

I guess the above code will convert to:

spawn do
  begin
    puts "start"
    sleep 1 # yield to main fiber
  ensure
    puts "done!"
  end
end

Fiber.yield

Which cause the code in the ensure clause never executed, right?

But, i consider this is not a expected behavior when used within spawn block, If there is no guarantee, the user should not use it in this case.


BTW: format code not work well here, the blank line under the sleep 1 will added by compiler unexpectedly.

spawn do
  puts "start"
  sleep 1
                         # <= this line added by formatter.
ensure
  puts "done!"
end

Fiber.yield

Thanks.

The main fiber exits before the child fiber is done.

done = Channel(Nil).new
spawn do
  puts "start"
  sleep 1
ensure
  puts "done!"
  done.send nil
end
 
done.receive

Thanks, It’s seem like we have to use channel this way to ensure codes in ensure always executed.

What i mean is, if the spawn not exit normally, code in ensure never executed, this behavior probably not so ensure.

Wow, Ruby is delightful:

Thread.new do
  puts 1
  sleep 30
ensure
  puts 2
end

sleep 1
exit

This prints 1 and 2, as one would expect.

That said, making this work in Crystal, which is not an interpreted language, is extremely hard… unless someone has ideas about how we could do it.

1 Like

For the single threaded case, invent uncatchable exceptions without backtrace and use those to rewind the stacks of other fibers?

But it falls down pretty hard with preview_mt.

That would be Gracefully stopping fibers · Issue #3561 · crystal-lang/crystal · GitHub

However, I don’t think it would be a good idea to incorporate an implicit cleanup mechanic into fibers. That would a great deal of complexity for the runtime has to keep track of fibers and figure out in which order to unroll them.
And then the ensure handlers could be doing really anything like starting running something new, while we’re actually supposed to be on the exit path.
No, exit should immediately terminate the process, not run any ensure hooks or whatever. If you want to exit, you should get an exit, not a “rewind the stack(s)”.

Note that this is actually not special with fibers, you get the same behaviour with only a single main fiber:

begin
  puts "foo"
  exit
ensure
  puts "ensure" # never gets printed
end

A fiber is just a very basic concurrency primitive. The path forward is to provide higher-level concurrency mechanisms which can keep track of a fiber’s lifetime, ensure all pieces of work are complete and if necessary, can trigger a graceful stack rewind if a fiber needs to be cancelled.
See [RFC] Structured Concurrency · Issue #6468 · crystal-lang/crystal · GitHub for this.

3 Likes