For this answer I wrote code like:
def show_wait_spinner
dirty = false
spinner = Thread.new{
loop{
print "*"
dirty = true
sleep 0.1
print "\b"
dirty = false
}
}
yield
spinner.kill
print "\b" if dirty
end
print "A"
show_wait_spinner{ sleep rand }
puts "B"
The goal is to ensure that the final output was "AB"
—to print a final "\b"
if it was not already printed by the thread. That code seems messy to me in Ruby where begin/rescue/ensure
exists. So I tried some other implementations of show_wait_spinner
; all of them fail to ensure that "AB"
is always the output, and never "A*B"
or "AB*"
.
Is there a cleaner, more Ruby-esque way to implement this logic?
def show_wait_spinner
stop = false
stopm = Mutex.new
spinner = Thread.new{
loop{
print "*"
sleep 0.1
print "\b"
stopm.synchronize{ break if stop }
}
}
yield
stopm.synchronize{ stop = true }
STDOUT.flush
end
…but my logic must be off, since this always results in "A*B".
This second attempt results in sometimes "A*B" being printed, sometimes "AB":
def show_wait_spinner
stop = false
spinner = Thread.new{
Thread.current[:stop] = false
loop{
print "*"
sleep 0.1
print "\b"
stopm.synchronize{ break if Thread.current[:stop] }
}
}
yield
spinner[:stop] = true
STDOUT.flush
end
def show_wait_spinner
spinner = Thread.new{
dirty = false
begin
loop{
print "*"
dirty = true
sleep 0.1
print "\b"
dirty = false
}
ensure
print "\b" if dirty
end
}
yield
spinner.kill
STDOUT.flush
end
def show_wait_spinner
spinner = Thread.new{
dirty = false
begin
loop{
print "*"
dirty = true
sleep 0.1
print "\b"
dirty = false
}
rescue
puts "YAY"
print "\b" if dirty
end
}
yield
spinner.raise
STDOUT.flush
end
Instead of killing your thread, why don't you flip a variable that causes it to stop at a pre-defined point? If you let it cycle through and exit at the end of the loop you won't have nearly as much trouble.
For instance:
def show_wait_spinner
running = true
spinner = Thread.new do
while (running) do
print "*"
sleep 0.1
print "\b"
end
end
yield
running = false
spinner.join
end
print "A"
show_wait_spinner{ sleep rand }
puts "B"
When you call Thread#kill
you have no idea where the thread is, and the thread isn't given an opportunity to clean up what it's doing. You can always kill the thread if your polite "stop running" request isn't respected.
I prefer your synchronized stop condition approach but you have a couple bugs:
break
in the synchronize block breaks out of the block, not the
loop.
def show_wait_spinner
stop = false
stopm = Mutex.new
spinner = Thread.new{
loop{
print "*"
sleep 0.1
print "\b"
break if stopm.synchronize{ stop }
}
}
yield
stopm.synchronize{ stop = true }
spinner.join
STDOUT.flush
end
print "A"
show_wait_spinner{ sleep rand }
puts "B"
I would avoid any solution involving Thread#raise and Thread#kill as their behavior can never be predictable and correct, see Charles Nutter's rant about the brokenness of these methods.
The Mutex#synchronize is only necessary for this simple boolean flipping if you really care a lot about the precise timing around the race condition when the parent thread sets the var, which in this example isn't likely, so you could avoid the overhead and just set and read stop
normally.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With