code: def spawn_containers(addrs, env_cls=Environment,
                       env_params=None,
                       mgr_cls=EnvManager, *args, **kwargs):
      
      pool = multiprocessing.Pool(len(addrs))
      kwargs['env_cls'] = env_cls
      kwargs['mgr_cls'] = mgr_cls
      r = []
      for i, addr in enumerate(addrs):
          if env_params is not None:
              k = env_params[i]
              k['env_cls'] = env_cls
              k['mgr_cls'] = mgr_cls
          # Copy kwargs so that we can apply different address to different
          # containers.
          else:
              k = kwargs.copy()
          k['addr'] = addr
          ret = pool.apply_async(spawn_container,
                                 args=args,
                                 kwds=k,
                                 error_callback=logger.warning)
          r.append(ret)
      return pool, r
docs: Spawn environments in a multiprocessing :class:`multiprocessing.Pool`.
  
      Arguments and keyword arguments are passed down to the created environments
      at initialization time if *env_params* is None. If *env_params* is not
      None, then it is assumed to contain individual initialization parameters
      for each environment in *addrs*.
  
      :param addrs:
          List of (HOST, PORT) addresses for the environments.
  
      :param env_cls:
          Callable for the environments. Must be a subclass of
          :py:class:`~creamas.core.environment.Environment`.
  
      :param env_params: Initialization parameters for the environments.
      :type env_params: Iterable of same length as *addrs* or None.
  
      :param mgr_cls:
          Callable for the managers. Must be a subclass of
          :py:class:`~creamas.mp.EnvManager`.s
  
      :returns:
          The created process pool and the *ApplyAsync* results for the spawned
          environments.