class_docstr: This object serves as the interface between a ZMQ socket passing
  Requests and Replies, and a thread or threads serving those requests.
  Received requests are received on a ZMQ socket and placed on upward_queue,
  and notify_all() is called on updward_cv.  Replies (via the Request.reply()
  method) are dispatched to their requesters via a downward queue.
  
  The Boundary wakes up the socket thread via the notify socket. This lets
  the socket thread poll for changes on the notify and request sockets, but
  allows it to receive Python objects via the downward queue.
class_docstr_tok: ["This","object","serves","as","the","interface","between","a","ZMQ","socket","passing","Requests","and","Replies",",","and","a","thread","or","threads","serving","those","requests",".","Received","requests","are","received","on","a","ZMQ","socket","and","placed","on","upward_queue",",","and","notify_all","(",")","is","called","on","updward_cv",".","Replies","(","via","the","Request.reply","(",")","method",")","are","dispatched","to","their","requesters","via","a","downward","queue",".","The","Boundary","wakes","up","the","socket","thread","via","the","notify","socket",".","This","lets","the","socket","thread","poll","for","changes","on","the","notify","and","request","sockets",",","but","allows","it","to","receive","Python","objects","via","the","downward","queue","."]
docstr: Construction
  
  zmq_address - the address for announcements and requests
  port - the port for announcements, defaults to random
docstr_tok: ["Construction","zmq_address","-","the","address","for","announcements","and","requests","port","-","the","port","for","announcements",",","defaults","to","random"]
code: def __init__(self, zmq_address, port=None):
      """Construction
  
      zmq_address - the address for announcements and requests
      port - the port for announcements, defaults to random
      """
      self.analysis_context = None
      self.analysis_context_lock = threading.RLock()
      #
      # Dictionary of request dictionary to queue for handler
      # (not including AnalysisRequest)
      #
      self.request_dictionary = {}
      self.zmq_context = zmq.Context()
      # Set linger to 0 so that all sockets close without
      # waiting for transmission during shutdown.
      self.zmq_context.setsockopt(zmq.LINGER, 0)
      self.zmq_address = zmq_address
      # The downward queue is used to feed replies to the socket thread
      self.downward_queue = queue.Queue()
  
      # Not entirely convinced that threadlocal is still needed, but meh.
      self.threadlocal = (
          threading.local()
      )  # for connecting to notification socket, and receiving replies
  
      #
      # Socket for telling workers whether to keep running
      # We should ONLY send over this socket from the spin thread
      # We need to create it here so that we can transmit the address to
      # workers on creation.
      #
      self.keepalive_socket = self.zmq_context.socket(zmq.PUB)
  
      self.keepalive_socket_port = (
          self.keepalive_socket.bind_to_random_port("tcp://127.0.0.1")
      )
      self.keepalive_address = (
          f"tcp://127.0.0.1:{self.keepalive_socket_port}"
      )
  
      self.thread = threading.Thread(
          target=self.spin,
          name="Boundary spin()",
      )
      self.thread.start()
  
imports: import logging
  import queue
  import threading
  import zmq
  import cellprofiler_core.utilities.zmq
  from cellprofiler_core.utilities.zmq._analysis_context import AnalysisContext
  from .communicable import Communicable
  from .communicable.reply.upstream_exit import BoundaryExited
  from .communicable.request import AnalysisRequest, Request
  from ...constants.worker import NOTIFY_STOP, NOTIFY_RUN
label_desc: Use image node_id 1 to create a new Boundary object  with example: obj = Boundary(zmq_address, port)