Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stopping a thread, ensuring that certain final code is run

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?

Stop at end of loop via Mutex

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".

Stop at end of loop via Thread-local variable

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

Kill and Ensure the Thread

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

Raise and Rescue the Thread

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
like image 744
Phrogz Avatar asked Apr 21 '12 22:04

Phrogz


2 Answers

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.

like image 77
tadman Avatar answered Nov 25 '22 14:11

tadman


I prefer your synchronized stop condition approach but you have a couple bugs:

  1. after you set the stop variable, you almost immediately end the program, so the thing that stops the thread is program exit, not the conditional test in the loop; use Thread#join to wait for the thread to exit normally and you'll get the consistent output you want.
  2. 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.

like image 42
dbenhur Avatar answered Nov 25 '22 14:11

dbenhur