# it's really messed up on a number of levels that doing this is necessary from multiprocessing import Process, Pipe from multiprocessing.connection import Connection from threading import Thread, Lock, Condition from queue import Queue from typing import Any, Iterable, List, Tuple, Optional import os class _ExceptionContainer: exn: Exception def __init__(self, exn: Exception): self.exn = exn class WorkItem: _callable: Any _arg: Any def __init__(self, callable: Any, arg: Any): self._callable = callable self._arg = arg def __call__(self) -> Any: return self._callable(self._arg) class WorkList: _inputs: List _outputs: List _position: int _lock: Lock def __init__(self, inputs: Iterable): self._inputs = list(inputs) self._outputs = [None] * len(self._inputs) self._position = 0 self._number_done = 0 self._lock = Lock() self._cv = Condition(self._lock) def __iter__(self): return self def __next__(self) -> Tuple[int, Any]: with self._lock: if self._position < len(self._inputs): i = self._position self._position += 1 return i, self._inputs[i] raise StopIteration def set_output(self, i: int, output: Any) -> None: with self._lock: self._outputs[i] = output self._number_done += 1 if self._number_done == len(self._inputs): self._cv.notify_all() def cancel(self) -> None: with self._lock: self._number_done = len(self._inputs) self._cv.notify_all() def wait_until_done(self) -> None: with self._lock: while self._number_done < len(self._inputs): self._cv.wait() def get_outputs(self) -> List: for o in self._outputs: if isinstance(o, _ExceptionContainer): raise o.exn return self._outputs def _child_entry(parent_conn: Connection, child_conn: Connection) -> None: parent_conn.close() while True: try: item = child_conn.recv() except EOFError: break if item is None: break i, work = item try: output, exn = work(), None except Exception as e: output, exn = None, e child_conn.send((i, output, exn)) child_conn.close() class Child: _proc: Process _parent_conn: Connection _child_conn: Connection _in_queue: Queue _to_courier: Thread _from_courier: Thread _current_worklist: Optional[WorkList] def __init__(self): self._in_queue = Queue() self._parent_conn, self._child_conn = Pipe(duplex=True) self._proc = Process(target=_child_entry, args=(self._parent_conn, self._child_conn,), daemon=True) self._proc.start() self._child_conn.close() self._to_courier = Thread(target=self._to_courier_entry, daemon=True) self._to_courier.start() self._from_courier = Thread(target=self._from_courier_entry, daemon=True) self._from_courier.start() self._current_worklist = None def _to_courier_entry(self) -> None: try: while True: worklist = self._in_queue.get() if worklist is None: self._parent_conn.send(None) break self._current_worklist = worklist for i, work in worklist: self._parent_conn.send((i, work)) worklist.wait_until_done() self._current_worklist = None except IOError: pass def _from_courier_entry(self) -> None: try: while True: try: i, output, exn = self._parent_conn.recv() except EOFError: break if exn is not None: output = _ExceptionContainer(exn) self._current_worklist.set_output(i, output) except IOError: pass def consume_worklist(self, worklist: WorkList) -> None: self._in_queue.put(worklist) def close(self) -> None: self._in_queue.put(None) def join(self) -> None: self._to_courier.join() self._from_courier.join() self._proc.join() def terminate(self) -> None: self._in_queue.put(None) self._parent_conn.close() self._proc.terminate() if self._current_worklist is not None: self._current_worklist.cancel() class Pool: def __init__(self, processes=None, initializer=None, initargs=None, maxtasksperchild=None, context=None): if processes is None: self.num_processes = os.cpu_count() else: if processes < 0: raise ValueError("processes must be a positive integer") self.num_processes = processes if initializer: raise NotImplementedError("initializer not implemented") if initargs: raise NotImplementedError("initargs not implemented") if maxtasksperchild: raise NotImplementedError("maxtasksperchild not implemented") if context: raise NotImplementedError("context not implemented") self._closed = False assert self.num_processes > 0, "Must have at least one process" self.children = [Child() for _ in range(self.num_processes)] def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() self.join() self.terminate() def __del__(self): self.terminate() def close(self): if not self._closed: for c in self.children: c.close() self._closed |= True def join(self): assert self._closed, "Must close before joining" for c in self.children: c.join() def terminate(self): for c in self.children: c.terminate() self._closed |= True def map(self, func, iterable, chunksize=None, callback=None, error_callback=None) -> List: assert not self._closed, "Cannot submit tasks after closing" worklist = WorkList(WorkItem(func, x) for x in iterable) for c in self.children: c.consume_worklist(worklist) worklist.wait_until_done() return worklist.get_outputs()