text: from threading import Event, Thread
  
  
  class Stoppable:
      def __init__(self, name=None):
          self.start_requested = Event()
          self.stop_requested = Event()
          self.name = name or self.__class__.__name__
  
      def start(self):
          self.start_requested.set()
  
      def stop(self):
          self.stop_requested.set()
  
      @property
      def is_started(self):
          return self.start_requested.is_set()
  
      @property
      def is_stopped(self):
          return self.stop_requested.is_set()
  
      def __enter__(self):
          self.start()
          return self
  
      def __exit__(self, type, value, traceback):
          self.stop()
          self.join()
  
  
  class StoppableThread(Thread, Stoppable):
      def __init__(self, target=None, name=None, daemon=True, **kwargs):
          Thread.__init__(self, target=target, daemon=daemon, **kwargs)
          Stoppable.__init__(self, name)
  
      def start(self):
          if not self.is_started:
              Stoppable.start(self)
              Thread.start(self)
  
      def run(self):
          try:
              while not self.is_stopped and self._target:
                  self._target(*self._args, **self._kwargs)
          finally:
              # Avoid a refcycle if the thread is running a function with
              # an argument that has a member that points to the thread.
              del self._target, self._args, self._kwargs
  
  
  class StoppableThreadList(Stoppable):
      def __init__(self, name=None):
          super().__init__(name)
          self.threads = []
  
      def add_thread(self, thread):
          self.threads.append(thread)
          return thread
  
      def new_thread(self, *args, **kwds):
          return self.add_thread(StoppableThread(*args, **kwds))
  
      def start(self):
          super().start()
          for i in self.threads:
              if not i.is_started:
                  i.start()
  
      def stop(self):
          super().stop()
          for i in self.threads:
              i.stop()
  
      def join(self, timeout=None):
          for i in self.threads:
              i.join(timeout=timeout)
  
      def is_alive(self):
          return any(i.is_alive() for i in self.threads)
  