Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/joblib/externals/loky/process_executor.py: 19%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

573 statements  

1############################################################################### 

2# Re-implementation of the ProcessPoolExecutor more robust to faults 

3# 

4# author: Thomas Moreau and Olivier Grisel 

5# 

6# adapted from concurrent/futures/process_pool_executor.py (17/02/2017) 

7# * Add an extra management thread to detect executor_manager_thread failures, 

8# * Improve the shutdown process to avoid deadlocks, 

9# * Add timeout for workers, 

10# * More robust pickling process. 

11# 

12# Copyright 2009 Brian Quinlan. All Rights Reserved. 

13# Licensed to PSF under a Contributor Agreement. 

14 

15"""Implements ProcessPoolExecutor. 

16 

17The follow diagram and text describe the data-flow through the system: 

18 

19|======================= In-process =====================|== Out-of-process ==| 

20 

21+----------+ +----------+ +--------+ +-----------+ +---------+ 

22| | => | Work Ids | | | | Call Q | | Process | 

23| | +----------+ | | +-----------+ | Pool | 

24| | | ... | | | | ... | +---------+ 

25| | | 6 | => | | => | 5, call() | => | | 

26| | | 7 | | | | ... | | | 

27| Process | | ... | | Local | +-----------+ | Process | 

28| Pool | +----------+ | Worker | | #1..n | 

29| Executor | | Thread | | | 

30| | +----------- + | | +-----------+ | | 

31| | <=> | Work Items | <=> | | <= | Result Q | <= | | 

32| | +------------+ | | +-----------+ | | 

33| | | 6: call() | | | | ... | | | 

34| | | future | +--------+ | 4, result | | | 

35| | | ... | | 3, except | | | 

36+----------+ +------------+ +-----------+ +---------+ 

37 

38Executor.submit() called: 

39- creates a uniquely numbered _WorkItem and adds it to the "Work Items" dict 

40- adds the id of the _WorkItem to the "Work Ids" queue 

41 

42Local worker thread: 

43- reads work ids from the "Work Ids" queue and looks up the corresponding 

44 WorkItem from the "Work Items" dict: if the work item has been cancelled then 

45 it is simply removed from the dict, otherwise it is repackaged as a 

46 _CallItem and put in the "Call Q". New _CallItems are put in the "Call Q" 

47 until "Call Q" is full. NOTE: the size of the "Call Q" is kept small because 

48 calls placed in the "Call Q" can no longer be cancelled with Future.cancel(). 

49- reads _ResultItems from "Result Q", updates the future stored in the 

50 "Work Items" dict and deletes the dict entry 

51 

52Process #1..n: 

53- reads _CallItems from "Call Q", executes the calls, and puts the resulting 

54 _ResultItems in "Result Q" 

55""" 

56 

57 

58__author__ = "Thomas Moreau (thomas.moreau.2010@gmail.com)" 

59 

60 

61import faulthandler 

62import os 

63import gc 

64import sys 

65import queue 

66import struct 

67import weakref 

68import warnings 

69import itertools 

70import traceback 

71import threading 

72from time import time, sleep 

73import multiprocessing as mp 

74from functools import partial 

75from pickle import PicklingError 

76from concurrent.futures import Executor, InvalidStateError 

77from concurrent.futures._base import LOGGER 

78from concurrent.futures.process import BrokenProcessPool as _BPPException 

79from multiprocessing.connection import wait 

80 

81from ._base import Future 

82from .backend import get_context 

83from .backend.context import cpu_count, _MAX_WINDOWS_WORKERS 

84from .backend.queues import Queue, SimpleQueue 

85from .backend.reduction import set_loky_pickler, get_loky_pickler_name 

86from .backend.utils import kill_process_tree, get_exitcodes_terminated_worker 

87from .initializers import _prepare_initializer 

88 

89 

90# Mechanism to prevent infinite process spawning. When a worker of a 

91# ProcessPoolExecutor nested in MAX_DEPTH Executor tries to create a new 

92# Executor, a LokyRecursionError is raised 

93MAX_DEPTH = int(os.environ.get("LOKY_MAX_DEPTH", 10)) 

94_CURRENT_DEPTH = 0 

95 

96# Minimum time interval between two consecutive memory leak protection checks. 

97_MEMORY_LEAK_CHECK_DELAY = 1.0 

98 

99# Number of bytes of memory usage allowed over the reference process size. 

100_MAX_MEMORY_LEAK_SIZE = int(3e8) 

101 

102 

103try: 

104 from psutil import Process 

105 

106 _USE_PSUTIL = True 

107 

108 def _get_memory_usage(pid, force_gc=False): 

109 if force_gc: 

110 gc.collect() 

111 

112 mem_size = Process(pid).memory_info().rss 

113 mp.util.debug(f"psutil return memory size: {mem_size}") 

114 return mem_size 

115 

116except ImportError: 

117 _USE_PSUTIL = False 

118 

119 

120class _ThreadWakeup: 

121 def __init__(self): 

122 self._closed = False 

123 self._reader, self._writer = mp.Pipe(duplex=False) 

124 

125 def close(self): 

126 if not self._closed: 

127 self._closed = True 

128 self._writer.close() 

129 self._reader.close() 

130 

131 def wakeup(self): 

132 if not self._closed: 

133 self._writer.send_bytes(b"") 

134 

135 def clear(self): 

136 if not self._closed: 

137 while self._reader.poll(): 

138 self._reader.recv_bytes() 

139 

140 

141class _ExecutorFlags: 

142 """necessary references to maintain executor states without preventing gc 

143 

144 It permits to keep the information needed by executor_manager_thread 

145 and crash_detection_thread to maintain the pool without preventing the 

146 garbage collection of unreferenced executors. 

147 """ 

148 

149 def __init__(self, shutdown_lock): 

150 

151 self.shutdown = False 

152 self.broken = None 

153 self.kill_workers = False 

154 self.shutdown_lock = shutdown_lock 

155 

156 def flag_as_shutting_down(self, kill_workers=None): 

157 with self.shutdown_lock: 

158 self.shutdown = True 

159 if kill_workers is not None: 

160 self.kill_workers = kill_workers 

161 

162 def flag_as_broken(self, broken): 

163 with self.shutdown_lock: 

164 self.shutdown = True 

165 self.broken = broken 

166 

167 

168# Prior to 3.9, executor_manager_thread is created as daemon thread. This means 

169# that it is not joined automatically when the interpreter is shutting down. 

170# To work around this problem, an exit handler is installed to tell the 

171# thread to exit when the interpreter is shutting down and then waits until 

172# it finishes. The thread needs to be daemonized because the atexit hooks are 

173# called after all non daemonized threads are joined. 

174# 

175# Starting 3.9, there exists a specific atexit hook to be called before joining 

176# the threads so the executor_manager_thread does not need to be daemonized 

177# anymore. 

178# 

179# The atexit hooks are registered when starting the first ProcessPoolExecutor 

180# to avoid import having an effect on the interpreter. 

181 

182_global_shutdown = False 

183_global_shutdown_lock = threading.Lock() 

184_threads_wakeups = weakref.WeakKeyDictionary() 

185 

186 

187def _python_exit(): 

188 global _global_shutdown 

189 _global_shutdown = True 

190 

191 # Materialize the list of items to avoid error due to iterating over 

192 # changing size dictionary. 

193 items = list(_threads_wakeups.items()) 

194 if len(items) > 0: 

195 mp.util.debug( 

196 f"Interpreter shutting down. Waking up {len(items)}" 

197 f"executor_manager_thread:\n{items}" 

198 ) 

199 

200 # Wake up the executor_manager_thread's so they can detect the interpreter 

201 # is shutting down and exit. 

202 for _, (shutdown_lock, thread_wakeup) in items: 

203 with shutdown_lock: 

204 thread_wakeup.wakeup() 

205 

206 # Collect the executor_manager_thread's to make sure we exit cleanly. 

207 for thread, _ in items: 

208 # This locks is to prevent situations where an executor is gc'ed in one 

209 # thread while the atexit finalizer is running in another thread. 

210 with _global_shutdown_lock: 

211 thread.join() 

212 

213 

214# With the fork context, _thread_wakeups is propagated to children. 

215# Clear it after fork to avoid some situation that can cause some 

216# freeze when joining the workers. 

217mp.util.register_after_fork(_threads_wakeups, lambda obj: obj.clear()) 

218 

219 

220# Module variable to register the at_exit call 

221process_pool_executor_at_exit = None 

222 

223# Controls how many more calls than processes will be queued in the call queue. 

224# A smaller number will mean that processes spend more time idle waiting for 

225# work while a larger number will make Future.cancel() succeed less frequently 

226# (Futures in the call queue cannot be cancelled). 

227EXTRA_QUEUED_CALLS = 1 

228 

229 

230class _RemoteTraceback(Exception): 

231 """Embed stringification of remote traceback in local traceback""" 

232 

233 def __init__(self, tb=None): 

234 self.tb = f'\n"""\n{tb}"""' 

235 

236 def __str__(self): 

237 return self.tb 

238 

239 

240# Do not inherit from BaseException to mirror 

241# concurrent.futures.process._ExceptionWithTraceback 

242class _ExceptionWithTraceback: 

243 def __init__(self, exc): 

244 tb = getattr(exc, "__traceback__", None) 

245 if tb is None: 

246 _, _, tb = sys.exc_info() 

247 tb = traceback.format_exception(type(exc), exc, tb) 

248 tb = "".join(tb) 

249 self.exc = exc 

250 self.tb = tb 

251 

252 def __reduce__(self): 

253 return _rebuild_exc, (self.exc, self.tb) 

254 

255 

256def _rebuild_exc(exc, tb): 

257 exc.__cause__ = _RemoteTraceback(tb) 

258 return exc 

259 

260 

261class _WorkItem: 

262 

263 __slots__ = ["future", "fn", "args", "kwargs"] 

264 

265 def __init__(self, future, fn, args, kwargs): 

266 self.future = future 

267 self.fn = fn 

268 self.args = args 

269 self.kwargs = kwargs 

270 

271 

272class _ResultItem: 

273 def __init__(self, work_id, exception=None, result=None): 

274 self.work_id = work_id 

275 self.exception = exception 

276 self.result = result 

277 

278 

279class _CallItem: 

280 def __init__(self, work_id, fn, args, kwargs): 

281 self.work_id = work_id 

282 self.fn = fn 

283 self.args = args 

284 self.kwargs = kwargs 

285 

286 # Store the current loky_pickler so it is correctly set in the worker 

287 self.loky_pickler = get_loky_pickler_name() 

288 

289 def __call__(self): 

290 set_loky_pickler(self.loky_pickler) 

291 return self.fn(*self.args, **self.kwargs) 

292 

293 def __repr__(self): 

294 return ( 

295 f"CallItem({self.work_id}, {self.fn}, {self.args}, {self.kwargs})" 

296 ) 

297 

298 

299class _SafeQueue(Queue): 

300 """Safe Queue set exception to the future object linked to a job""" 

301 

302 def __init__( 

303 self, 

304 max_size=0, 

305 ctx=None, 

306 pending_work_items=None, 

307 running_work_items=None, 

308 thread_wakeup=None, 

309 shutdown_lock=None, 

310 reducers=None, 

311 ): 

312 self.thread_wakeup = thread_wakeup 

313 self.shutdown_lock = shutdown_lock 

314 self.pending_work_items = pending_work_items 

315 self.running_work_items = running_work_items 

316 super().__init__(max_size, reducers=reducers, ctx=ctx) 

317 

318 def _on_queue_feeder_error(self, e, obj): 

319 if isinstance(obj, _CallItem): 

320 # format traceback only works on python3 

321 if isinstance(e, struct.error): 

322 raised_error = RuntimeError( 

323 "The task could not be sent to the workers as it is too " 

324 "large for `send_bytes`." 

325 ) 

326 else: 

327 raised_error = PicklingError( 

328 "Could not pickle the task to send it to the workers." 

329 ) 

330 tb = traceback.format_exception( 

331 type(e), e, getattr(e, "__traceback__", None) 

332 ) 

333 raised_error.__cause__ = _RemoteTraceback("".join(tb)) 

334 work_item = self.pending_work_items.pop(obj.work_id, None) 

335 self.running_work_items.remove(obj.work_id) 

336 # work_item can be None if another process terminated. In this 

337 # case, the executor_manager_thread fails all work_items with 

338 # BrokenProcessPool 

339 if work_item is not None: 

340 work_item.future.set_exception(raised_error) 

341 del work_item 

342 with self.shutdown_lock: 

343 self.thread_wakeup.wakeup() 

344 else: 

345 super()._on_queue_feeder_error(e, obj) 

346 

347 

348def _get_chunks(chunksize, *iterables): 

349 """Iterates over zip()ed iterables in chunks.""" 

350 it = zip(*iterables) 

351 while True: 

352 chunk = tuple(itertools.islice(it, chunksize)) 

353 if not chunk: 

354 return 

355 yield chunk 

356 

357 

358def _process_chunk(fn, chunk): 

359 """Processes a chunk of an iterable passed to map. 

360 

361 Runs the function passed to map() on a chunk of the 

362 iterable passed to map. 

363 

364 This function is run in a separate process. 

365 

366 """ 

367 return [fn(*args) for args in chunk] 

368 

369 

370def _sendback_result(result_queue, work_id, result=None, exception=None): 

371 """Safely send back the given result or exception""" 

372 try: 

373 result_queue.put( 

374 _ResultItem(work_id, result=result, exception=exception) 

375 ) 

376 except BaseException as e: 

377 exc = _ExceptionWithTraceback(e) 

378 result_queue.put(_ResultItem(work_id, exception=exc)) 

379 

380 

381def _enable_faulthandler_if_needed(): 

382 if "PYTHONFAULTHANDLER" in os.environ: 

383 # Respect the environment variable to configure faulthandler. This 

384 # makes it possible to never enable faulthandler in the loky workers by 

385 # setting PYTHONFAULTHANDLER=0 explicitly in the environment. 

386 mp.util.debug( 

387 f"faulthandler explicitly configured by environment variable: " 

388 f"PYTHONFAULTHANDLER={os.environ['PYTHONFAULTHANDLER']}." 

389 ) 

390 else: 

391 if faulthandler.is_enabled(): 

392 # Fault handler is already enabled, possibly via a custom 

393 # initializer to customize the behavior. 

394 mp.util.debug("faulthandler already enabled.") 

395 else: 

396 # Enable faulthandler by default with default paramaters otherwise. 

397 mp.util.debug( 

398 "Enabling faulthandler to report tracebacks on worker crashes." 

399 ) 

400 faulthandler.enable() 

401 

402 

403def _process_worker( 

404 call_queue, 

405 result_queue, 

406 initializer, 

407 initargs, 

408 processes_management_lock, 

409 timeout, 

410 worker_exit_lock, 

411 current_depth, 

412): 

413 """Evaluates calls from call_queue and places the results in result_queue. 

414 

415 This worker is run in a separate process. 

416 

417 Args: 

418 call_queue: A ctx.Queue of _CallItems that will be read and 

419 evaluated by the worker. 

420 result_queue: A ctx.Queue of _ResultItems that will written 

421 to by the worker. 

422 initializer: A callable initializer, or None 

423 initargs: A tuple of args for the initializer 

424 processes_management_lock: A ctx.Lock avoiding worker timeout while 

425 some workers are being spawned. 

426 timeout: maximum time to wait for a new item in the call_queue. If that 

427 time is expired, the worker will shutdown. 

428 worker_exit_lock: Lock to avoid flagging the executor as broken on 

429 workers timeout. 

430 current_depth: Nested parallelism level, to avoid infinite spawning. 

431 """ 

432 if initializer is not None: 

433 try: 

434 initializer(*initargs) 

435 except BaseException: 

436 LOGGER.critical("Exception in initializer:", exc_info=True) 

437 # The parent will notice that the process stopped and 

438 # mark the pool broken 

439 return 

440 

441 # set the global _CURRENT_DEPTH mechanism to limit recursive call 

442 global _CURRENT_DEPTH 

443 _CURRENT_DEPTH = current_depth 

444 _process_reference_size = None 

445 _last_memory_leak_check = None 

446 pid = os.getpid() 

447 

448 mp.util.debug(f"Worker started with timeout={timeout}") 

449 _enable_faulthandler_if_needed() 

450 

451 while True: 

452 try: 

453 call_item = call_queue.get(block=True, timeout=timeout) 

454 if call_item is None: 

455 mp.util.info("Shutting down worker on sentinel") 

456 except queue.Empty: 

457 mp.util.info(f"Shutting down worker after timeout {timeout:0.3f}s") 

458 if processes_management_lock.acquire(block=False): 

459 processes_management_lock.release() 

460 call_item = None 

461 else: 

462 mp.util.info("Could not acquire processes_management_lock") 

463 continue 

464 except BaseException: 

465 previous_tb = traceback.format_exc() 

466 try: 

467 result_queue.put(_RemoteTraceback(previous_tb)) 

468 except BaseException: 

469 # If we cannot format correctly the exception, at least print 

470 # the traceback. 

471 print(previous_tb) 

472 mp.util.debug("Exiting with code 1") 

473 sys.exit(1) 

474 if call_item is None: 

475 # Notify queue management thread about worker shutdown 

476 result_queue.put(pid) 

477 is_clean = worker_exit_lock.acquire(True, timeout=30) 

478 

479 # Early notify any loky executor running in this worker process 

480 # (nested parallelism) that this process is about to shutdown to 

481 # avoid a deadlock waiting undifinitely for the worker to finish. 

482 _python_exit() 

483 

484 if is_clean: 

485 mp.util.debug("Exited cleanly") 

486 else: 

487 mp.util.info("Main process did not release worker_exit") 

488 return 

489 try: 

490 r = call_item() 

491 except BaseException as e: 

492 exc = _ExceptionWithTraceback(e) 

493 result_queue.put(_ResultItem(call_item.work_id, exception=exc)) 

494 else: 

495 _sendback_result(result_queue, call_item.work_id, result=r) 

496 del r 

497 

498 # Free the resource as soon as possible, to avoid holding onto 

499 # open files or shared memory that is not needed anymore 

500 del call_item 

501 

502 if _USE_PSUTIL: 

503 if _process_reference_size is None: 

504 # Make reference measurement after the first call 

505 _process_reference_size = _get_memory_usage(pid, force_gc=True) 

506 _last_memory_leak_check = time() 

507 continue 

508 if time() - _last_memory_leak_check > _MEMORY_LEAK_CHECK_DELAY: 

509 mem_usage = _get_memory_usage(pid) 

510 _last_memory_leak_check = time() 

511 if mem_usage - _process_reference_size < _MAX_MEMORY_LEAK_SIZE: 

512 # Memory usage stays within bounds: everything is fine. 

513 continue 

514 

515 # Check again memory usage; this time take the measurement 

516 # after a forced garbage collection to break any reference 

517 # cycles. 

518 mem_usage = _get_memory_usage(pid, force_gc=True) 

519 _last_memory_leak_check = time() 

520 if mem_usage - _process_reference_size < _MAX_MEMORY_LEAK_SIZE: 

521 # The GC managed to free the memory: everything is fine. 

522 continue 

523 

524 # The process is leaking memory: let the master process 

525 # know that we need to start a new worker. 

526 mp.util.info("Memory leak detected: shutting down worker") 

527 result_queue.put(pid) 

528 with worker_exit_lock: 

529 mp.util.debug("Exit due to memory leak") 

530 return 

531 else: 

532 # if psutil is not installed, trigger gc.collect events 

533 # regularly to limit potential memory leaks due to reference cycles 

534 if _last_memory_leak_check is None or ( 

535 time() - _last_memory_leak_check > _MEMORY_LEAK_CHECK_DELAY 

536 ): 

537 gc.collect() 

538 _last_memory_leak_check = time() 

539 

540 

541class _ExecutorManagerThread(threading.Thread): 

542 """Manages the communication between this process and the worker processes. 

543 

544 The manager is run in a local thread. 

545 

546 Args: 

547 executor: A reference to the ProcessPoolExecutor that owns 

548 this thread. A weakref will be own by the manager as well as 

549 references to internal objects used to introspect the state of 

550 the executor. 

551 """ 

552 

553 def __init__(self, executor): 

554 # Store references to necessary internals of the executor. 

555 

556 # A _ThreadWakeup to allow waking up the executor_manager_thread from 

557 # the main Thread and avoid deadlocks caused by permanently 

558 # locked queues. 

559 self.thread_wakeup = executor._executor_manager_thread_wakeup 

560 self.shutdown_lock = executor._shutdown_lock 

561 

562 # A weakref.ref to the ProcessPoolExecutor that owns this thread. Used 

563 # to determine if the ProcessPoolExecutor has been garbage collected 

564 # and that the manager can exit. 

565 # When the executor gets garbage collected, the weakref callback 

566 # will wake up the queue management thread so that it can terminate 

567 # if there is no pending work item. 

568 def weakref_cb( 

569 _, 

570 thread_wakeup=self.thread_wakeup, 

571 shutdown_lock=self.shutdown_lock, 

572 ): 

573 if mp is not None: 

574 # At this point, the multiprocessing module can already be 

575 # garbage collected. We only log debug info when still 

576 # possible. 

577 mp.util.debug( 

578 "Executor collected: triggering callback for" 

579 " QueueManager wakeup" 

580 ) 

581 with shutdown_lock: 

582 thread_wakeup.wakeup() 

583 

584 self.executor_reference = weakref.ref(executor, weakref_cb) 

585 

586 # The flags of the executor 

587 self.executor_flags = executor._flags 

588 

589 # A list of the ctx.Process instances used as workers. 

590 self.processes = executor._processes 

591 

592 # A ctx.Queue that will be filled with _CallItems derived from 

593 # _WorkItems for processing by the process workers. 

594 self.call_queue = executor._call_queue 

595 

596 # A ctx.SimpleQueue of _ResultItems generated by the process workers. 

597 self.result_queue = executor._result_queue 

598 

599 # A queue.Queue of work ids e.g. Queue([5, 6, ...]). 

600 self.work_ids_queue = executor._work_ids 

601 

602 # A dict mapping work ids to _WorkItems e.g. 

603 # {5: <_WorkItem...>, 6: <_WorkItem...>, ...} 

604 self.pending_work_items = executor._pending_work_items 

605 

606 # A list of the work_ids that are currently running 

607 self.running_work_items = executor._running_work_items 

608 

609 # A lock to avoid concurrent shutdown of workers on timeout and spawn 

610 # of new processes or shut down 

611 self.processes_management_lock = executor._processes_management_lock 

612 

613 super().__init__(name="ExecutorManagerThread") 

614 if sys.version_info < (3, 9): 

615 self.daemon = True 

616 

617 def run(self): 

618 # Main loop for the executor manager thread. 

619 

620 while True: 

621 self.add_call_item_to_queue() 

622 

623 result_item, is_broken, bpe = self.wait_result_broken_or_wakeup() 

624 

625 if is_broken: 

626 self.terminate_broken(bpe) 

627 return 

628 if result_item is not None: 

629 self.process_result_item(result_item) 

630 # Delete reference to result_item to avoid keeping references 

631 # while waiting on new results. 

632 del result_item 

633 

634 if self.is_shutting_down(): 

635 self.flag_executor_shutting_down() 

636 

637 # When only canceled futures remain in pending_work_items, our 

638 # next call to wait_result_broken_or_wakeup would hang forever. 

639 # This makes sure we have some running futures or none at all. 

640 self.add_call_item_to_queue() 

641 

642 # Since no new work items can be added, it is safe to shutdown 

643 # this thread if there are no pending work items. 

644 if not self.pending_work_items: 

645 self.join_executor_internals() 

646 return 

647 

648 def add_call_item_to_queue(self): 

649 # Fills call_queue with _WorkItems from pending_work_items. 

650 # This function never blocks. 

651 while True: 

652 if self.call_queue.full(): 

653 return 

654 try: 

655 work_id = self.work_ids_queue.get(block=False) 

656 except queue.Empty: 

657 return 

658 else: 

659 work_item = self.pending_work_items[work_id] 

660 

661 if work_item.future.set_running_or_notify_cancel(): 

662 self.running_work_items += [work_id] 

663 self.call_queue.put( 

664 _CallItem( 

665 work_id, 

666 work_item.fn, 

667 work_item.args, 

668 work_item.kwargs, 

669 ), 

670 block=True, 

671 ) 

672 else: 

673 del self.pending_work_items[work_id] 

674 continue 

675 

676 def wait_result_broken_or_wakeup(self): 

677 # Wait for a result to be ready in the result_queue while checking 

678 # that all worker processes are still running, or for a wake up 

679 # signal send. The wake up signals come either from new tasks being 

680 # submitted, from the executor being shutdown/gc-ed, or from the 

681 # shutdown of the python interpreter. 

682 result_reader = self.result_queue._reader 

683 wakeup_reader = self.thread_wakeup._reader 

684 readers = [result_reader, wakeup_reader] 

685 worker_sentinels = [p.sentinel for p in list(self.processes.values())] 

686 ready = wait(readers + worker_sentinels) 

687 

688 bpe = None 

689 is_broken = True 

690 result_item = None 

691 if result_reader in ready: 

692 try: 

693 result_item = result_reader.recv() 

694 if isinstance(result_item, _RemoteTraceback): 

695 bpe = BrokenProcessPool( 

696 "A task has failed to un-serialize. Please ensure that" 

697 " the arguments of the function are all picklable." 

698 ) 

699 bpe.__cause__ = result_item 

700 else: 

701 is_broken = False 

702 except BaseException as e: 

703 bpe = BrokenProcessPool( 

704 "A result has failed to un-serialize. Please ensure that " 

705 "the objects returned by the function are always " 

706 "picklable." 

707 ) 

708 tb = traceback.format_exception( 

709 type(e), e, getattr(e, "__traceback__", None) 

710 ) 

711 bpe.__cause__ = _RemoteTraceback("".join(tb)) 

712 

713 elif wakeup_reader in ready: 

714 # This is simply a wake-up event that might either trigger putting 

715 # more tasks in the queue or trigger the clean up of resources. 

716 is_broken = False 

717 else: 

718 # A worker has terminated and we don't know why, set the state of 

719 # the executor as broken 

720 exit_codes = "" 

721 if sys.platform != "win32": 

722 # In Windows, introspecting terminated workers exitcodes seems 

723 # unstable, therefore they are not appended in the exception 

724 # message. 

725 exit_codes = ( 

726 "\nThe exit codes of the workers are " 

727 f"{get_exitcodes_terminated_worker(self.processes)}" 

728 ) 

729 mp.util.debug( 

730 "A worker unexpectedly terminated. Workers that " 

731 "might have caused the breakage: " 

732 + str( 

733 { 

734 p.name: p.exitcode 

735 for p in list(self.processes.values()) 

736 if p is not None and p.sentinel in ready 

737 } 

738 ) 

739 ) 

740 bpe = TerminatedWorkerError( 

741 "A worker process managed by the executor was unexpectedly " 

742 "terminated. This could be caused by a segmentation fault " 

743 "while calling the function or by an excessive memory usage " 

744 "causing the Operating System to kill the worker.\n" 

745 f"{exit_codes}\n" 

746 "Detailed tracebacks of the workers should have been printed " 

747 "to stderr in the executor process if faulthandler was not " 

748 "disabled." 

749 ) 

750 

751 self.thread_wakeup.clear() 

752 

753 return result_item, is_broken, bpe 

754 

755 def process_result_item(self, result_item): 

756 # Process the received a result_item. This can be either the PID of a 

757 # worker that exited gracefully or a _ResultItem 

758 

759 if isinstance(result_item, int): 

760 # Clean shutdown of a worker using its PID, either on request 

761 # by the executor.shutdown method or by the timeout of the worker 

762 # itself: we should not mark the executor as broken. 

763 with self.processes_management_lock: 

764 p = self.processes.pop(result_item, None) 

765 

766 # p can be None if the executor is concurrently shutting down. 

767 if p is not None: 

768 p._worker_exit_lock.release() 

769 mp.util.debug( 

770 f"joining {p.name} when processing {p.pid} as result_item" 

771 ) 

772 p.join() 

773 del p 

774 

775 # Make sure the executor have the right number of worker, even if a 

776 # worker timeout while some jobs were submitted. If some work is 

777 # pending or there is less processes than running items, we need to 

778 # start a new Process and raise a warning. 

779 n_pending = len(self.pending_work_items) 

780 n_running = len(self.running_work_items) 

781 if n_pending - n_running > 0 or n_running > len(self.processes): 

782 executor = self.executor_reference() 

783 if ( 

784 executor is not None 

785 and len(self.processes) < executor._max_workers 

786 ): 

787 warnings.warn( 

788 "A worker stopped while some jobs were given to the " 

789 "executor. This can be caused by a too short worker " 

790 "timeout or by a memory leak.", 

791 UserWarning, 

792 ) 

793 with executor._processes_management_lock: 

794 executor._adjust_process_count() 

795 executor = None 

796 else: 

797 # Received a _ResultItem so mark the future as completed. 

798 work_item = self.pending_work_items.pop(result_item.work_id, None) 

799 # work_item can be None if another process terminated (see above) 

800 if work_item is not None: 

801 if result_item.exception is not None: 

802 work_item.future.set_exception(result_item.exception) 

803 else: 

804 work_item.future.set_result(result_item.result) 

805 self.running_work_items.remove(result_item.work_id) 

806 

807 def is_shutting_down(self): 

808 # Check whether we should start shutting down the executor. 

809 executor = self.executor_reference() 

810 # No more work items can be added if: 

811 # - The interpreter is shutting down OR 

812 # - The executor that owns this thread is not broken AND 

813 # * The executor that owns this worker has been collected OR 

814 # * The executor that owns this worker has been shutdown. 

815 # If the executor is broken, it should be detected in the next loop. 

816 return _global_shutdown or ( 

817 (executor is None or self.executor_flags.shutdown) 

818 and not self.executor_flags.broken 

819 ) 

820 

821 def terminate_broken(self, bpe): 

822 # Terminate the executor because it is in a broken state. The bpe 

823 # argument can be used to display more information on the error that 

824 # lead the executor into becoming broken. 

825 

826 # Mark the process pool broken so that submits fail right now. 

827 self.executor_flags.flag_as_broken(bpe) 

828 

829 # Mark pending tasks as failed. 

830 for work_item in self.pending_work_items.values(): 

831 try: 

832 work_item.future.set_exception(bpe) 

833 except InvalidStateError: 

834 # set_exception() fails if the future is cancelled: ignore it. 

835 # Trying to check if the future is cancelled before calling 

836 # set_exception() would leave a race condition if the future is 

837 # cancelled between the check and set_exception(). 

838 pass 

839 # Delete references to object. See issue16284 

840 del work_item 

841 self.pending_work_items.clear() 

842 

843 # Terminate remaining workers forcibly: the queues or their 

844 # locks may be in a dirty state and block forever. 

845 self.kill_workers(reason="broken executor") 

846 

847 # clean up resources 

848 self.join_executor_internals() 

849 

850 def flag_executor_shutting_down(self): 

851 # Flag the executor as shutting down and cancel remaining tasks if 

852 # requested as early as possible if it is not gc-ed yet. 

853 self.executor_flags.flag_as_shutting_down() 

854 

855 # Cancel pending work items if requested. 

856 if self.executor_flags.kill_workers: 

857 while self.pending_work_items: 

858 _, work_item = self.pending_work_items.popitem() 

859 work_item.future.set_exception( 

860 ShutdownExecutorError( 

861 "The Executor was shutdown with `kill_workers=True` " 

862 "before this job could complete." 

863 ) 

864 ) 

865 del work_item 

866 

867 # Kill the remaining worker forcibly to no waste time joining them 

868 self.kill_workers(reason="executor shutting down") 

869 

870 def kill_workers(self, reason=""): 

871 # Terminate the remaining workers using SIGKILL. This function also 

872 # terminates descendant workers of the children in case there is some 

873 # nested parallelism. 

874 while self.processes: 

875 _, p = self.processes.popitem() 

876 mp.util.debug(f"terminate process {p.name}, reason: {reason}") 

877 try: 

878 kill_process_tree(p) 

879 except ProcessLookupError: # pragma: no cover 

880 pass 

881 

882 def shutdown_workers(self): 

883 # shutdown all workers in self.processes 

884 

885 # Create a list to avoid RuntimeError due to concurrent modification of 

886 # processes. nb_children_alive is thus an upper bound. Also release the 

887 # processes' _worker_exit_lock to accelerate the shutdown procedure, as 

888 # there is no need for hand-shake here. 

889 with self.processes_management_lock: 

890 n_children_to_stop = 0 

891 for p in list(self.processes.values()): 

892 mp.util.debug(f"releasing worker exit lock on {p.name}") 

893 p._worker_exit_lock.release() 

894 n_children_to_stop += 1 

895 

896 mp.util.debug(f"found {n_children_to_stop} processes to stop") 

897 

898 # Send the right number of sentinels, to make sure all children are 

899 # properly terminated. Do it with a mechanism that avoid hanging on 

900 # Full queue when all workers have already been shutdown. 

901 n_sentinels_sent = 0 

902 cooldown_time = 0.001 

903 while ( 

904 n_sentinels_sent < n_children_to_stop 

905 and self.get_n_children_alive() > 0 

906 ): 

907 for _ in range(n_children_to_stop - n_sentinels_sent): 

908 try: 

909 self.call_queue.put_nowait(None) 

910 n_sentinels_sent += 1 

911 except queue.Full as e: 

912 if cooldown_time > 5.0: 

913 mp.util.info( 

914 "failed to send all sentinels and exit with error." 

915 f"\ncall_queue size={self.call_queue._maxsize}; " 

916 f" full is {self.call_queue.full()}; " 

917 ) 

918 raise e 

919 mp.util.info( 

920 "full call_queue prevented to send all sentinels at " 

921 "once, waiting..." 

922 ) 

923 sleep(cooldown_time) 

924 cooldown_time *= 1.2 

925 break 

926 

927 mp.util.debug(f"sent {n_sentinels_sent} sentinels to the call queue") 

928 

929 def join_executor_internals(self): 

930 self.shutdown_workers() 

931 

932 # Release the queue's resources as soon as possible. Flag the feeder 

933 # thread for clean exit to avoid having the crash detection thread flag 

934 # the Executor as broken during the shutdown. This is safe as either: 

935 # * We don't need to communicate with the workers anymore 

936 # * There is nothing left in the Queue buffer except None sentinels 

937 mp.util.debug("closing call_queue") 

938 self.call_queue.close() 

939 self.call_queue.join_thread() 

940 

941 # Closing result_queue 

942 mp.util.debug("closing result_queue") 

943 self.result_queue.close() 

944 

945 mp.util.debug("closing thread_wakeup") 

946 with self.shutdown_lock: 

947 self.thread_wakeup.close() 

948 

949 # If .join() is not called on the created processes then 

950 # some ctx.Queue methods may deadlock on macOS. 

951 with self.processes_management_lock: 

952 mp.util.debug(f"joining {len(self.processes)} processes") 

953 n_joined_processes = 0 

954 while True: 

955 try: 

956 pid, p = self.processes.popitem() 

957 mp.util.debug(f"joining process {p.name} with pid {pid}") 

958 p.join() 

959 n_joined_processes += 1 

960 except KeyError: 

961 break 

962 

963 mp.util.debug( 

964 "executor management thread clean shutdown of " 

965 f"{n_joined_processes} workers" 

966 ) 

967 

968 def get_n_children_alive(self): 

969 # This is an upper bound on the number of children alive. 

970 with self.processes_management_lock: 

971 return sum(p.is_alive() for p in list(self.processes.values())) 

972 

973 

974_system_limits_checked = False 

975_system_limited = None 

976 

977 

978def _check_system_limits(): 

979 global _system_limits_checked, _system_limited 

980 if _system_limits_checked and _system_limited: 

981 raise NotImplementedError(_system_limited) 

982 _system_limits_checked = True 

983 try: 

984 nsems_max = os.sysconf("SC_SEM_NSEMS_MAX") 

985 except (AttributeError, ValueError): 

986 # sysconf not available or setting not available 

987 return 

988 if nsems_max == -1: 

989 # undetermined limit, assume that limit is determined 

990 # by available memory only 

991 return 

992 if nsems_max >= 256: 

993 # minimum number of semaphores available 

994 # according to POSIX 

995 return 

996 _system_limited = ( 

997 f"system provides too few semaphores ({nsems_max} available, " 

998 "256 necessary)" 

999 ) 

1000 raise NotImplementedError(_system_limited) 

1001 

1002 

1003def _chain_from_iterable_of_lists(iterable): 

1004 """ 

1005 Specialized implementation of itertools.chain.from_iterable. 

1006 Each item in *iterable* should be a list. This function is 

1007 careful not to keep references to yielded objects. 

1008 """ 

1009 for element in iterable: 

1010 element.reverse() 

1011 while element: 

1012 yield element.pop() 

1013 

1014 

1015def _check_max_depth(context): 

1016 # Limit the maxmal recursion level 

1017 global _CURRENT_DEPTH 

1018 if context.get_start_method() == "fork" and _CURRENT_DEPTH > 0: 

1019 raise LokyRecursionError( 

1020 "Could not spawn extra nested processes at depth superior to " 

1021 "MAX_DEPTH=1. It is not possible to increase this limit when " 

1022 "using the 'fork' start method." 

1023 ) 

1024 

1025 if 0 < MAX_DEPTH and _CURRENT_DEPTH + 1 > MAX_DEPTH: 

1026 raise LokyRecursionError( 

1027 "Could not spawn extra nested processes at depth superior to " 

1028 f"MAX_DEPTH={MAX_DEPTH}. If this is intendend, you can change " 

1029 "this limit with the LOKY_MAX_DEPTH environment variable." 

1030 ) 

1031 

1032 

1033class LokyRecursionError(RuntimeError): 

1034 """A process tries to spawn too many levels of nested processes.""" 

1035 

1036 

1037class BrokenProcessPool(_BPPException): 

1038 """ 

1039 Raised when the executor is broken while a future was in the running state. 

1040 The cause can an error raised when unpickling the task in the worker 

1041 process or when unpickling the result value in the parent process. It can 

1042 also be caused by a worker process being terminated unexpectedly. 

1043 """ 

1044 

1045 

1046class TerminatedWorkerError(BrokenProcessPool): 

1047 """ 

1048 Raised when a process in a ProcessPoolExecutor terminated abruptly 

1049 while a future was in the running state. 

1050 """ 

1051 

1052 

1053# Alias for backward compat (for code written for loky 1.1.4 and earlier). Do 

1054# not use in new code. 

1055BrokenExecutor = BrokenProcessPool 

1056 

1057 

1058class ShutdownExecutorError(RuntimeError): 

1059 """ 

1060 Raised when a ProcessPoolExecutor is shutdown while a future was in the 

1061 running or pending state. 

1062 """ 

1063 

1064 

1065class ProcessPoolExecutor(Executor): 

1066 

1067 _at_exit = None 

1068 

1069 def __init__( 

1070 self, 

1071 max_workers=None, 

1072 job_reducers=None, 

1073 result_reducers=None, 

1074 timeout=None, 

1075 context=None, 

1076 initializer=None, 

1077 initargs=(), 

1078 env=None, 

1079 ): 

1080 """Initializes a new ProcessPoolExecutor instance. 

1081 

1082 Args: 

1083 max_workers: int, optional (default: cpu_count()) 

1084 The maximum number of processes that can be used to execute the 

1085 given calls. If None or not given then as many worker processes 

1086 will be created as the number of CPUs the current process 

1087 can use. 

1088 job_reducers, result_reducers: dict(type: reducer_func) 

1089 Custom reducer for pickling the jobs and the results from the 

1090 Executor. If only `job_reducers` is provided, `result_reducer` 

1091 will use the same reducers 

1092 timeout: int, optional (default: None) 

1093 Idle workers exit after timeout seconds. If a new job is 

1094 submitted after the timeout, the executor will start enough 

1095 new Python processes to make sure the pool of workers is full. 

1096 context: A multiprocessing context to launch the workers. This 

1097 object should provide SimpleQueue, Queue and Process. 

1098 initializer: An callable used to initialize worker processes. 

1099 initargs: A tuple of arguments to pass to the initializer. 

1100 env: A dict of environment variable to overwrite in the child 

1101 process. The environment variables are set before any module is 

1102 loaded. Note that this only works with the loky context. 

1103 """ 

1104 _check_system_limits() 

1105 

1106 if max_workers is None: 

1107 self._max_workers = cpu_count() 

1108 else: 

1109 if max_workers <= 0: 

1110 raise ValueError("max_workers must be greater than 0") 

1111 self._max_workers = max_workers 

1112 

1113 if ( 

1114 sys.platform == "win32" 

1115 and self._max_workers > _MAX_WINDOWS_WORKERS 

1116 ): 

1117 warnings.warn( 

1118 f"On Windows, max_workers cannot exceed {_MAX_WINDOWS_WORKERS} " 

1119 "due to limitations of the operating system." 

1120 ) 

1121 self._max_workers = _MAX_WINDOWS_WORKERS 

1122 

1123 if context is None: 

1124 context = get_context() 

1125 self._context = context 

1126 self._env = env 

1127 

1128 self._initializer, self._initargs = _prepare_initializer( 

1129 initializer, initargs 

1130 ) 

1131 _check_max_depth(self._context) 

1132 

1133 if result_reducers is None: 

1134 result_reducers = job_reducers 

1135 

1136 # Timeout 

1137 self._timeout = timeout 

1138 

1139 # Management thread 

1140 self._executor_manager_thread = None 

1141 

1142 # Map of pids to processes 

1143 self._processes = {} 

1144 

1145 # Internal variables of the ProcessPoolExecutor 

1146 self._processes = {} 

1147 self._queue_count = 0 

1148 self._pending_work_items = {} 

1149 self._running_work_items = [] 

1150 self._work_ids = queue.Queue() 

1151 self._processes_management_lock = self._context.Lock() 

1152 self._executor_manager_thread = None 

1153 self._shutdown_lock = threading.Lock() 

1154 

1155 # _ThreadWakeup is a communication channel used to interrupt the wait 

1156 # of the main loop of executor_manager_thread from another thread (e.g. 

1157 # when calling executor.submit or executor.shutdown). We do not use the 

1158 # _result_queue to send wakeup signals to the executor_manager_thread 

1159 # as it could result in a deadlock if a worker process dies with the 

1160 # _result_queue write lock still acquired. 

1161 # 

1162 # _shutdown_lock must be locked to access _ThreadWakeup.wakeup. 

1163 self._executor_manager_thread_wakeup = _ThreadWakeup() 

1164 

1165 # Flag to hold the state of the Executor. This permits to introspect 

1166 # the Executor state even once it has been garbage collected. 

1167 self._flags = _ExecutorFlags(self._shutdown_lock) 

1168 

1169 # Finally setup the queues for interprocess communication 

1170 self._setup_queues(job_reducers, result_reducers) 

1171 

1172 mp.util.debug("ProcessPoolExecutor is setup") 

1173 

1174 def _setup_queues(self, job_reducers, result_reducers, queue_size=None): 

1175 # Make the call queue slightly larger than the number of processes to 

1176 # prevent the worker processes from idling. But don't make it too big 

1177 # because futures in the call queue cannot be cancelled. 

1178 if queue_size is None: 

1179 queue_size = 2 * self._max_workers + EXTRA_QUEUED_CALLS 

1180 self._call_queue = _SafeQueue( 

1181 max_size=queue_size, 

1182 pending_work_items=self._pending_work_items, 

1183 running_work_items=self._running_work_items, 

1184 thread_wakeup=self._executor_manager_thread_wakeup, 

1185 shutdown_lock=self._shutdown_lock, 

1186 reducers=job_reducers, 

1187 ctx=self._context, 

1188 ) 

1189 # Killed worker processes can produce spurious "broken pipe" 

1190 # tracebacks in the queue's own worker thread. But we detect killed 

1191 # processes anyway, so silence the tracebacks. 

1192 self._call_queue._ignore_epipe = True 

1193 

1194 self._result_queue = SimpleQueue( 

1195 reducers=result_reducers, ctx=self._context 

1196 ) 

1197 

1198 def _start_executor_manager_thread(self): 

1199 if self._executor_manager_thread is None: 

1200 mp.util.debug("_start_executor_manager_thread called") 

1201 

1202 # Start the processes so that their sentinels are known. 

1203 self._executor_manager_thread = _ExecutorManagerThread(self) 

1204 self._executor_manager_thread.start() 

1205 

1206 # register this executor in a mechanism that ensures it will wakeup 

1207 # when the interpreter is exiting. 

1208 _threads_wakeups[self._executor_manager_thread] = ( 

1209 self._shutdown_lock, 

1210 self._executor_manager_thread_wakeup, 

1211 ) 

1212 

1213 global process_pool_executor_at_exit 

1214 if process_pool_executor_at_exit is None: 

1215 # Ensure that the _python_exit function will be called before 

1216 # the multiprocessing.Queue._close finalizers which have an 

1217 # exitpriority of 10. 

1218 

1219 if sys.version_info < (3, 9): 

1220 process_pool_executor_at_exit = mp.util.Finalize( 

1221 None, _python_exit, exitpriority=20 

1222 ) 

1223 else: 

1224 process_pool_executor_at_exit = threading._register_atexit( 

1225 _python_exit 

1226 ) 

1227 

1228 def _adjust_process_count(self): 

1229 while len(self._processes) < self._max_workers: 

1230 worker_exit_lock = self._context.BoundedSemaphore(1) 

1231 args = ( 

1232 self._call_queue, 

1233 self._result_queue, 

1234 self._initializer, 

1235 self._initargs, 

1236 self._processes_management_lock, 

1237 self._timeout, 

1238 worker_exit_lock, 

1239 _CURRENT_DEPTH + 1, 

1240 ) 

1241 worker_exit_lock.acquire() 

1242 try: 

1243 # Try to spawn the process with some environment variable to 

1244 # overwrite but it only works with the loky context for now. 

1245 p = self._context.Process( 

1246 target=_process_worker, args=args, env=self._env 

1247 ) 

1248 except TypeError: 

1249 p = self._context.Process(target=_process_worker, args=args) 

1250 p._worker_exit_lock = worker_exit_lock 

1251 p.start() 

1252 self._processes[p.pid] = p 

1253 mp.util.debug( 

1254 f"Adjusted process count to {self._max_workers}: " 

1255 f"{[(p.name, pid) for pid, p in self._processes.items()]}" 

1256 ) 

1257 

1258 def _ensure_executor_running(self): 

1259 """ensures all workers and management thread are running""" 

1260 with self._processes_management_lock: 

1261 if len(self._processes) != self._max_workers: 

1262 self._adjust_process_count() 

1263 self._start_executor_manager_thread() 

1264 

1265 def submit(self, fn, *args, **kwargs): 

1266 with self._flags.shutdown_lock: 

1267 if self._flags.broken is not None: 

1268 raise self._flags.broken 

1269 if self._flags.shutdown: 

1270 raise ShutdownExecutorError( 

1271 "cannot schedule new futures after shutdown" 

1272 ) 

1273 

1274 # Cannot submit a new calls once the interpreter is shutting down. 

1275 # This check avoids spawning new processes at exit. 

1276 if _global_shutdown: 

1277 raise RuntimeError( 

1278 "cannot schedule new futures after interpreter shutdown" 

1279 ) 

1280 

1281 f = Future() 

1282 w = _WorkItem(f, fn, args, kwargs) 

1283 

1284 self._pending_work_items[self._queue_count] = w 

1285 self._work_ids.put(self._queue_count) 

1286 self._queue_count += 1 

1287 # Wake up queue management thread 

1288 self._executor_manager_thread_wakeup.wakeup() 

1289 

1290 self._ensure_executor_running() 

1291 return f 

1292 

1293 submit.__doc__ = Executor.submit.__doc__ 

1294 

1295 def map(self, fn, *iterables, **kwargs): 

1296 """Returns an iterator equivalent to map(fn, iter). 

1297 

1298 Args: 

1299 fn: A callable that will take as many arguments as there are 

1300 passed iterables. 

1301 timeout: The maximum number of seconds to wait. If None, then there 

1302 is no limit on the wait time. 

1303 chunksize: If greater than one, the iterables will be chopped into 

1304 chunks of size chunksize and submitted to the process pool. 

1305 If set to one, the items in the list will be sent one at a 

1306 time. 

1307 

1308 Returns: 

1309 An iterator equivalent to: map(func, *iterables) but the calls may 

1310 be evaluated out-of-order. 

1311 

1312 Raises: 

1313 TimeoutError: If the entire result iterator could not be generated 

1314 before the given timeout. 

1315 Exception: If fn(*args) raises for any values. 

1316 """ 

1317 timeout = kwargs.get("timeout", None) 

1318 chunksize = kwargs.get("chunksize", 1) 

1319 if chunksize < 1: 

1320 raise ValueError("chunksize must be >= 1.") 

1321 

1322 results = super().map( 

1323 partial(_process_chunk, fn), 

1324 _get_chunks(chunksize, *iterables), 

1325 timeout=timeout, 

1326 ) 

1327 return _chain_from_iterable_of_lists(results) 

1328 

1329 def shutdown(self, wait=True, kill_workers=False): 

1330 mp.util.debug(f"shutting down executor {self}") 

1331 

1332 self._flags.flag_as_shutting_down(kill_workers) 

1333 executor_manager_thread = self._executor_manager_thread 

1334 executor_manager_thread_wakeup = self._executor_manager_thread_wakeup 

1335 

1336 if executor_manager_thread_wakeup is not None: 

1337 # Wake up queue management thread 

1338 with self._shutdown_lock: 

1339 self._executor_manager_thread_wakeup.wakeup() 

1340 

1341 if executor_manager_thread is not None and wait: 

1342 # This locks avoids concurrent join if the interpreter 

1343 # is shutting down. 

1344 with _global_shutdown_lock: 

1345 executor_manager_thread.join() 

1346 _threads_wakeups.pop(executor_manager_thread, None) 

1347 

1348 # To reduce the risk of opening too many files, remove references to 

1349 # objects that use file descriptors. 

1350 self._executor_manager_thread = None 

1351 self._executor_manager_thread_wakeup = None 

1352 self._call_queue = None 

1353 self._result_queue = None 

1354 self._processes_management_lock = None 

1355 

1356 shutdown.__doc__ = Executor.shutdown.__doc__