class_docstr: Execute workflow with multiprocessing, not sending more jobs at once
  than the system can support.
  
  The plugin_args input to run can be used to control the multiprocessing
  execution and defining the maximum amount of memory and threads that
  should be used. When those parameters are not specified,
  the number of threads and memory of the system is used.
  
  System consuming nodes should be tagged::
  
    memory_consuming_node.mem_gb = 8
    thread_consuming_node.n_procs = 16
  
  The default number of threads and memory are set at node
  creation, and are 1 and 0.25GB respectively.
  
  Currently supported options are:
  
  - non_daemon : boolean flag to execute as non-daemon processes
  - n_procs: maximum number of threads to be executed in parallel
  - memory_gb: maximum memory (in GB) that can be used at once.
  - raise_insufficient: raise error if the requested resources for
      a node over the maximum `n_procs` and/or `memory_gb`
      (default is ``True``).
  - scheduler: sort jobs topologically (``'tsort'``, default value)
      or prioritize jobs by, first, memory consumption and, second,
      number of threads (``'mem_thread'`` option).
  - maxtasksperchild: number of nodes to run on each process before
      refreshing the worker (default: 10).
docstr: Sends jobs to workers when system resources are available.
docstr_tok: ["Sends","jobs","to","workers","when","system","resources","are","available","."]
code: def _send_procs_to_workers(self, updatehash=False, graph=None):
      """
      Sends jobs to workers when system resources are available.
      """
  
      # Check to see if a job is available (jobs with all dependencies run)
      # See https://github.com/nipy/nipype/pull/2200#discussion_r141605722
      # See also https://github.com/nipy/nipype/issues/2372
      jobids = np.flatnonzero(
          ~self.proc_done & (self.depidx.sum(axis=0) == 0).__array__()
      )
  
      # Check available resources by summing all threads and memory used
      free_memory_gb, free_processors = self._check_resources(
          self.pending_tasks
      )
  
      stats = (
          len(self.pending_tasks),
          len(jobids),
          free_memory_gb,
          self.memory_gb,
          free_processors,
          self.processors,
      )
      if self._stats != stats:
          tasks_list_msg = ""
  
          if logger.level <= INFO:
              running_tasks = [
                  "  * %s" % self.procs[jobid].fullname
                  for _, jobid in self.pending_tasks
              ]
              if running_tasks:
                  tasks_list_msg = "\nCurrently running:\n"
                  tasks_list_msg += "\n".join(running_tasks)
                  tasks_list_msg = indent(tasks_list_msg, " " * 21)
          logger.info(
              "[LegacyMultiProc] Running %d tasks, and %d jobs ready. Free "
              "memory (GB): %0.2f/%0.2f, Free processors: %d/%d.%s",
              len(self.pending_tasks),
              len(jobids),
              free_memory_gb,
              self.memory_gb,
              free_processors,
              self.processors,
              tasks_list_msg,
          )
          self._stats = stats
  
      if free_memory_gb < 0.01 or free_processors == 0:
          logger.debug("No resources available")
          return
  
      if len(jobids) + len(self.pending_tasks) == 0:
          logger.debug(
              "No tasks are being run, and no jobs can "
              "be submitted to the queue. Potential deadlock"
          )
          return
  
      jobids = self._sort_jobs(
          jobids, scheduler=self.plugin_args.get("scheduler")
      )
  
      # Run garbage collector before potentially submitting jobs
      gc.collect()
  
      # Submit jobs
      for jobid in jobids:
          # First expand mapnodes
          if isinstance(self.procs[jobid], MapNode):
              try:
                  num_subnodes = self.procs[jobid].num_subnodes()
              except Exception:
                  traceback = format_exception(*sys.exc_info())
                  self._clean_queue(
                      jobid,
                      graph,
                      result={"result": None, "traceback": traceback},
                  )
                  self.proc_pending[jobid] = False
                  continue
              if num_subnodes > 1:
                  submit = self._submit_mapnode(jobid)
                  if not submit:
                      continue
  
          # Check requirements of this job
          next_job_gb = min(self.procs[jobid].mem_gb, self.memory_gb)
          next_job_th = min(self.procs[jobid].n_procs, self.processors)
  
          # If node does not fit, skip at this moment
          if (
              next_job_th > free_processors
              or next_job_gb > free_memory_gb
          ):
              logger.debug(
                  "Cannot allocate job %d (%0.2fGB, %d threads).",
                  jobid,
                  next_job_gb,
                  next_job_th,
              )
              continue
  
          free_memory_gb -= next_job_gb
          free_processors -= next_job_th
          logger.debug(
              "Allocating %s ID=%d (%0.2fGB, %d threads). Free: "
              "%0.2fGB, %d threads.",
              self.procs[jobid].fullname,
              jobid,
              next_job_gb,
              next_job_th,
              free_memory_gb,
              free_processors,
          )
  
          # change job status in appropriate queues
          self.proc_done[jobid] = True
          self.proc_pending[jobid] = True
  
          # If cached and up-to-date just retrieve it, don't run
          if self._local_hash_check(jobid, graph):
              continue
  
          # updatehash and run_without_submitting are also run locally
          if updatehash or self.procs[jobid].run_without_submitting:
              logger.debug(
                  "Running node %s on master thread", self.procs[jobid]
              )
              try:
                  self.procs[jobid].run(updatehash=updatehash)
              except Exception:
                  traceback = format_exception(*sys.exc_info())
                  self._clean_queue(
                      jobid,
                      graph,
                      result={"result": None, "traceback": traceback},
                  )
  
              # Release resources
              self._task_finished_cb(jobid)
              self._remove_node_dirs()
              free_memory_gb += next_job_gb
              free_processors += next_job_th
              # Display stats next loop
              self._stats = None
  
              # Clean up any debris from running node in main process
              gc.collect()
              continue
  
          # Task should be submitted to workers
          # Send job to task manager and add to pending tasks
          if self._status_callback:
              self._status_callback(self.procs[jobid], "start")
          tid = self._submit_job(
              deepcopy(self.procs[jobid]), updatehash=updatehash
          )
          if tid is None:
              self.proc_done[jobid] = False
              self.proc_pending[jobid] = False
          else:
              self.pending_tasks.insert(0, (tid, jobid))
          # Display stats next loop
          self._stats = None
  
imports: import os
  import multiprocessing
  from multiprocessing import Pool, cpu_count, pool
  from traceback import format_exception
  import sys
  from logging import INFO
  import gc
  from copy import deepcopy
  import numpy
  from ...None import logging
  from ...utils.profiler import get_system_total_memory_gb
  from ..engine import MapNode
  from .base import DistributedPluginBase
label_desc: Use image node_id 9 for calling the LegacyMultiProcPlugin obj's underlying member method code with example usage: obj._send_procs_to_workers(updatehash, graph) without return types