您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

230 行
8.8 KiB

  1. import os
  2. import pickle
  3. import subprocess
  4. import sys
  5. from collections import deque
  6. from importlib.util import module_from_spec, spec_from_file_location
  7. from typing import Callable, Deque, List, Optional, Set, Tuple, TypeVar, cast
  8. from ._core._eventloop import current_time, get_asynclib, get_cancelled_exc_class
  9. from ._core._exceptions import BrokenWorkerProcess
  10. from ._core._subprocesses import open_process
  11. from ._core._synchronization import CapacityLimiter
  12. from ._core._tasks import CancelScope, fail_after
  13. from .abc import ByteReceiveStream, ByteSendStream, Process
  14. from .lowlevel import RunVar, checkpoint_if_cancelled
  15. from .streams.buffered import BufferedByteReceiveStream
  16. WORKER_MAX_IDLE_TIME = 300 # 5 minutes
  17. T_Retval = TypeVar('T_Retval')
  18. _process_pool_workers: RunVar[Set[Process]] = RunVar('_process_pool_workers')
  19. _process_pool_idle_workers: RunVar[Deque[Tuple[Process, float]]] = RunVar(
  20. '_process_pool_idle_workers')
  21. _default_process_limiter: RunVar[CapacityLimiter] = RunVar('_default_process_limiter')
  22. async def run_sync(
  23. func: Callable[..., T_Retval], *args: object, cancellable: bool = False,
  24. limiter: Optional[CapacityLimiter] = None) -> T_Retval:
  25. """
  26. Call the given function with the given arguments in a worker process.
  27. If the ``cancellable`` option is enabled and the task waiting for its completion is cancelled,
  28. the worker process running it will be abruptly terminated using SIGKILL (or
  29. ``terminateProcess()`` on Windows).
  30. :param func: a callable
  31. :param args: positional arguments for the callable
  32. :param cancellable: ``True`` to allow cancellation of the operation while it's running
  33. :param limiter: capacity limiter to use to limit the total amount of processes running
  34. (if omitted, the default limiter is used)
  35. :return: an awaitable that yields the return value of the function.
  36. """
  37. async def send_raw_command(pickled_cmd: bytes) -> object:
  38. try:
  39. await stdin.send(pickled_cmd)
  40. response = await buffered.receive_until(b'\n', 50)
  41. status, length = response.split(b' ')
  42. if status not in (b'RETURN', b'EXCEPTION'):
  43. raise RuntimeError(f'Worker process returned unexpected response: {response!r}')
  44. pickled_response = await buffered.receive_exactly(int(length))
  45. except BaseException as exc:
  46. workers.discard(process)
  47. try:
  48. process.kill()
  49. with CancelScope(shield=True):
  50. await process.aclose()
  51. except ProcessLookupError:
  52. pass
  53. if isinstance(exc, get_cancelled_exc_class()):
  54. raise
  55. else:
  56. raise BrokenWorkerProcess from exc
  57. retval = pickle.loads(pickled_response)
  58. if status == b'EXCEPTION':
  59. assert isinstance(retval, BaseException)
  60. raise retval
  61. else:
  62. return retval
  63. # First pickle the request before trying to reserve a worker process
  64. await checkpoint_if_cancelled()
  65. request = pickle.dumps(('run', func, args), protocol=pickle.HIGHEST_PROTOCOL)
  66. # If this is the first run in this event loop thread, set up the necessary variables
  67. try:
  68. workers = _process_pool_workers.get()
  69. idle_workers = _process_pool_idle_workers.get()
  70. except LookupError:
  71. workers = set()
  72. idle_workers = deque()
  73. _process_pool_workers.set(workers)
  74. _process_pool_idle_workers.set(idle_workers)
  75. get_asynclib().setup_process_pool_exit_at_shutdown(workers)
  76. async with (limiter or current_default_process_limiter()):
  77. # Pop processes from the pool (starting from the most recently used) until we find one that
  78. # hasn't exited yet
  79. process: Process
  80. while idle_workers:
  81. process, idle_since = idle_workers.pop()
  82. if process.returncode is None:
  83. stdin = cast(ByteSendStream, process.stdin)
  84. buffered = BufferedByteReceiveStream(cast(ByteReceiveStream, process.stdout))
  85. # Prune any other workers that have been idle for WORKER_MAX_IDLE_TIME seconds or
  86. # longer
  87. now = current_time()
  88. killed_processes: List[Process] = []
  89. while idle_workers:
  90. if now - idle_workers[0][1] < WORKER_MAX_IDLE_TIME:
  91. break
  92. process, idle_since = idle_workers.popleft()
  93. process.kill()
  94. workers.remove(process)
  95. killed_processes.append(process)
  96. with CancelScope(shield=True):
  97. for process in killed_processes:
  98. await process.aclose()
  99. break
  100. workers.remove(process)
  101. else:
  102. command = [sys.executable, '-u', '-m', __name__]
  103. process = await open_process(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
  104. try:
  105. stdin = cast(ByteSendStream, process.stdin)
  106. buffered = BufferedByteReceiveStream(cast(ByteReceiveStream, process.stdout))
  107. with fail_after(20):
  108. message = await buffered.receive(6)
  109. if message != b'READY\n':
  110. raise BrokenWorkerProcess(
  111. f'Worker process returned unexpected response: {message!r}')
  112. main_module_path = getattr(sys.modules['__main__'], '__file__', None)
  113. pickled = pickle.dumps(('init', sys.path, main_module_path),
  114. protocol=pickle.HIGHEST_PROTOCOL)
  115. await send_raw_command(pickled)
  116. except (BrokenWorkerProcess, get_cancelled_exc_class()):
  117. raise
  118. except BaseException as exc:
  119. process.kill()
  120. raise BrokenWorkerProcess('Error during worker process initialization') from exc
  121. workers.add(process)
  122. with CancelScope(shield=not cancellable):
  123. try:
  124. return cast(T_Retval, await send_raw_command(request))
  125. finally:
  126. if process in workers:
  127. idle_workers.append((process, current_time()))
  128. def current_default_process_limiter() -> CapacityLimiter:
  129. """
  130. Return the capacity limiter that is used by default to limit the number of worker processes.
  131. :return: a capacity limiter object
  132. """
  133. try:
  134. return _default_process_limiter.get()
  135. except LookupError:
  136. limiter = CapacityLimiter(os.cpu_count() or 2)
  137. _default_process_limiter.set(limiter)
  138. return limiter
  139. def process_worker() -> None:
  140. # Redirect standard streams to os.devnull so that user code won't interfere with the
  141. # parent-worker communication
  142. stdin = sys.stdin
  143. stdout = sys.stdout
  144. sys.stdin = open(os.devnull)
  145. sys.stdout = open(os.devnull, 'w')
  146. stdout.buffer.write(b'READY\n')
  147. while True:
  148. retval = exception = None
  149. try:
  150. command, *args = pickle.load(stdin.buffer)
  151. except EOFError:
  152. return
  153. except BaseException as exc:
  154. exception = exc
  155. else:
  156. if command == 'run':
  157. func, args = args
  158. try:
  159. retval = func(*args)
  160. except BaseException as exc:
  161. exception = exc
  162. elif command == 'init':
  163. main_module_path: Optional[str]
  164. sys.path, main_module_path = args
  165. del sys.modules['__main__']
  166. if main_module_path:
  167. # Load the parent's main module but as __mp_main__ instead of __main__
  168. # (like multiprocessing does) to avoid infinite recursion
  169. try:
  170. spec = spec_from_file_location('__mp_main__', main_module_path)
  171. if spec and spec.loader:
  172. main = module_from_spec(spec)
  173. spec.loader.exec_module(main)
  174. sys.modules['__main__'] = main
  175. except BaseException as exc:
  176. exception = exc
  177. try:
  178. if exception is not None:
  179. status = b'EXCEPTION'
  180. pickled = pickle.dumps(exception, pickle.HIGHEST_PROTOCOL)
  181. else:
  182. status = b'RETURN'
  183. pickled = pickle.dumps(retval, pickle.HIGHEST_PROTOCOL)
  184. except BaseException as exc:
  185. exception = exc
  186. status = b'EXCEPTION'
  187. pickled = pickle.dumps(exc, pickle.HIGHEST_PROTOCOL)
  188. stdout.buffer.write(b'%s %d\n' % (status, len(pickled)))
  189. stdout.buffer.write(pickled)
  190. # Respect SIGTERM
  191. if isinstance(exception, SystemExit):
  192. raise exception
  193. if __name__ == '__main__':
  194. process_worker()