class_docstr: The dispatcher is responsible for assigning tasks to free workers, report results back to the master and
  communicate to the nameserver.
code: def run(self):
      with self.discover_cond:
          t1 = threading.Thread(
              target=self.discover_workers, name="discover_workers"
          )
          t1.start()
          self.logger.info(
              "DISPATCHER: started the 'discover_worker' thread"
          )
          t2 = threading.Thread(
              target=self.job_runner, name="job_runner"
          )
          t2.start()
          self.logger.info(
              "DISPATCHER: started the 'job_runner' thread"
          )
  
          self.pyro_daemon = Pyro4.core.Daemon(host=self.host)
  
          with Pyro4.locateNS(
              host=self.nameserver, port=self.nameserver_port
          ) as ns:
              uri = self.pyro_daemon.register(self, self.pyro_id)
              ns.register(self.pyro_id, uri)
  
          self.logger.info(
              "DISPATCHER: Pyro daemon running on %s"
              % (self.pyro_daemon.locationStr)
          )
  
      self.pyro_daemon.requestLoop()
  
      with self.discover_cond:
          self.shutdown_all_threads = True
          self.logger.info("DISPATCHER: Dispatcher shutting down")
  
          self.runner_cond.notify_all()
          self.discover_cond.notify_all()
  
          with Pyro4.locateNS(
              self.nameserver, port=self.nameserver_port
          ) as ns:
              ns.remove(self.pyro_id)
  
      t1.join()
      self.logger.debug("DISPATCHER: 'discover_worker' thread exited")
      t2.join()
      self.logger.debug("DISPATCHER: 'job_runner' thread exited")
      self.logger.info("DISPATCHER: shut down complete")
  
imports: import threading
  import logging
  import queue
  import time
  import Pyro4
label_desc: Use image node_id 2 for calling the Dispatcher obj's underlying member method code with example usage: obj.run() without return types