''' Classes for controlling groups of threads that execute
      tasks periodically.

      The problem with simpler ways of launching and killing
      sets of threads that do repetitive tasks is that one
      usually uses time.sleep to control the repetition (or
      polling), leading to a delay in killing each thread.

      The approach used here is to start a single control
      thread that frequently checks a flag variable.  When
      the variable is false, the control thread exits. All
      the other threads in the group are waiting until
      either a time increment is over or the control thread
      dies; they loop as soon as the latter occurs, so they
      immediately see the changed flag variable and exit.
'''

# 2004/10/06 EF Added the poll_dt and function arguments,
#               plus more documentation; changed the
#               method of giving the control thread
#               access to the ThreadGroup flag.

# 2002/08/22 EF

import time
from threading import Thread

class controlThread(Thread):
   '''This thread exists solely to provide a means of
   signalling other threads in the group that the group
   is being stopped.
   The start() method is inherited from Thread.
   '''
   def __init__(self, is_running, poll_dt = 0.2):
      Thread.__init__(self)
      self.setDaemon(1)
      self.poll_dt = poll_dt
      self.is_running = is_running

   def run(self):
      while self.is_running():
         time.sleep(self.poll_dt)



class ThreadGroup:
   '''A group of worker threads plus a control thread to stop them all.

   Two methods of stopping the threads are provided.  First,
   the stop() method of the ThreadGroup can be called.  It
   sets a flag.  At intervals of poll_dt, the control thread
   checks this flag and dies if it is zero.  Then any thread
   waiting on the ThreadGroup.timer, or checking the same
   flag via the check_running() method, will exit.  If the
   optional function argument is supplied to the ThreadGroup initializer,
   then it will be called at the same time as the flag, and
   a False return will have the same effect as setting the flag.
   An example of such a function would be os.path.exists(filename).
   Deleting the file would then signal the threads to stop.
   '''
   def __init__(self, poll_dt = 0.2, function = None):
      self.running = 1
      if function:
         self.function = function
      else:
         self.function = lambda : True
      self.cThread = controlThread(self.check_running, poll_dt)
      self.threadList = []
      self.add(self.cThread)

   def timer(self, t):
      ''' Timer function to be used by a thread. '''
      self.cThread.join(t)  # Blocks for time t, or until cThread dies.

   def check_running(self):
      ''' Loop control function for a thread.'''
      return self.running and self.function

   def add(self, T):
      ''' Register a new thread T in the group. '''
      self.threadList.append(T)

   def start(self):
      ''' Start all threads currently registered. '''
      self.running = 1
      for T in self.threadList:
         T.start()

   def stop(self):
      ''' Signal all threads to stop, and return when all
      have stopped.'''
      self.running = 0
      ii = 0
      while len([1 for T in self.threadList if T.isAlive()]) > 0:
         time.sleep(0.05)
         ii += 1
         if ii > 40:
            print "Warning: one or more threads survived 2 seconds."
            break
      # Remove all the references to subthreads; otherwise there
      # will be persistent cyclic references. I don't know if this
      # could ever be a significant problem--probably not.
      self.threadList = []


class subThread(Thread):
   ''' This is like a regular thread, except that the
   target is a function that will be executed repeatedly
   by a loop inside the run method. It is simply an example
   of one useful type of thread that can be in a threadgroup.
   The start() method is inherited from Thread.
   '''
   def __init__(self, Tgroup, timeout = 1, target = None,
                name = None, args = (), kwargs = {}):
      Thread.__init__(self, name = name)
      self.setDaemon(1)  # So it will always exit if main thread ends.
      self.timeout = timeout
      self.Tgroup = Tgroup
      self.timer = Tgroup.timer
      self.__target = target
      self.__args = args
      self.__kwargs = kwargs

      #self.__args = ()
      #if kw.has_key('args'): self.__args = kw['args']
      #self.__kwargs = {}
      #if kw.has_key('kwargs'): self.__kwargs = kw['kwargs']

   def run(self):
      self.name = self.getName()
      print "%s starting" % self.name
      timeout = self.timeout
      fn = self.__target
      args = (self,) + self.__args
      while self.Tgroup.check_running():
         apply(fn, args, self.__kwargs)
         self.timer(timeout)
      print "%s ending" % self.name


# Test; illustration of how to use the ThreadGroup and subThread

if __name__ == "__main__":
   from Tkinter import Tk, Button

   def fn1(self, otherstuff, dict_arg1, dict_arg2):
      print self.name
      print otherstuff
      print dict_arg1 + dict_arg2

   def fn2(self):
      print self.name
      print "This function only prints its name"

   tg = ThreadGroup()
   tg.add(subThread(name = "test_1", target = fn2, Tgroup = tg, timeout = 3.1))
   tg.add(subThread(name = "test_2", target = fn1, Tgroup = tg, timeout = 2,
                    args = ('otherstuff--args',),
                    kwargs = {'dict_arg1': 'DictArg1',
                              'dict_arg2': 'DictArg2'}))
   tg.add(subThread(name = "test_3", target = fn2, Tgroup = tg, timeout = 1.05))
   tg.start()


   def quit():
      tg.stop()
      raise SystemExit

   root = Tk()
   root.option_add('*Button.activeBackground', 'yellow')
   b = Button(root, text = "test", command = quit)
   b.pack()
   root.mainloop()

