docs: def genmatrix(self, num_processes=1):
          """
          Actually generate the matrix
  
          :param num_processes: If you want to use multiprocessing to split up the
              work and run ``combinfunc()`` in parallel, specify
              ``num_processes > 1`` and this number of workers will be spun up,
              the work is split up amongst them evenly.
          """
          use_multiprocessing = num_processes > 1
          if use_multiprocessing:
              self.task_queue = Queue()
              self.done_queue = Queue()
  
          self.matrix = []
          logger.info("Generating matrix for %s items - O(n^2)", len(self.data))
          if use_multiprocessing:
              logger.info("Using multiprocessing on %s processes!", num_processes)
  
          if use_multiprocessing:
              logger.info("Spinning up %s workers", num_processes)
              processes = [Process(target=self.worker) for i in range(num_processes)]
              [process.start() for process in processes]
  
          for row_index, item in enumerate(self.data):
              logger.debug("Generating row %s/%s (%0.2f%%)",
                           row_index,
                           len(self.data),
                           100.0 * row_index / len(self.data))
              row = {}
              if use_multiprocessing:
                  num_tasks_queued = num_tasks_completed = 0
              for col_index, item2 in enumerate(self.data):
                  if self.diagonal is not None and col_index == row_index:
                      # This is a cell on the diagonal
                      row[col_index] = self.diagonal
                  elif self.symmetric and col_index < row_index:
                      # The matrix is symmetric and we are "in the lower left
                      # triangle" - fill this in after (in case of multiprocessing)
                      pass
                  # Otherwise, this cell is not on the diagonal and we do indeed
                  # need to call combinfunc()
                  elif use_multiprocessing:
                      # Add that thing to the task queue!
                      self.task_queue.put((col_index, item, item2))
                      num_tasks_queued += 1
                      # Start grabbing the results as we go, so as not to stuff all of
                      # the worker args into memory at once (as Queue.get() is a
                      # blocking operation)
                      if num_tasks_queued > num_processes:
                          col_index, result = self.done_queue.get()
                          row[col_index] = result
                          num_tasks_completed += 1
                  else:
                      # Otherwise do it here, in line
                      """
                      if not hasattr(item, '__iter__') or isinstance(item, tuple):
                          item = [item]
                      if not hasattr(item2, '__iter__') or isinstance(item2, tuple):
                          item2 = [item2]
                      """
                      # See the comment in function _encapsulate_item_for_combinfunc
                      # for details of why the lines above have been replaced
                      # by function invocations
                      item = _encapsulate_item_for_combinfunc(item)
                      item2 = _encapsulate_item_for_combinfunc(item2)
                      row[col_index] = self.combinfunc(item, item2)
  
              if self.symmetric:
                  # One more iteration to get symmetric lower left triangle
                  for col_index, item2 in enumerate(self.data):
                      if col_index >= row_index:
                          break
                      # post-process symmetric "lower left triangle"
                      row[col_index] = self.matrix[col_index][row_index]
  
              if use_multiprocessing:
                  # Grab the remaining worker task results
                  while num_tasks_completed < num_tasks_queued:
                      col_index, result = self.done_queue.get()
                      row[col_index] = result
                      num_tasks_completed += 1
  
              row_indexed = [row[index] for index in range(len(self.data))]
              self.matrix.append(row_indexed)
  
          if use_multiprocessing:
              logger.info("Stopping/joining %s workers", num_processes)
              [self.task_queue.put('STOP') for i in range(num_processes)]
              [process.join() for process in processes]
  
          logger.info("Matrix generated")