@@ -141,10 +141,11 @@ def __init__(self, future, fn, args, kwargs):
141
141
self .kwargs = kwargs
142
142
143
143
class _ResultItem (object ):
144
- def __init__ (self , work_id , exception = None , result = None ):
144
+ def __init__ (self , work_id , exception = None , result = None , exit_pid = None ):
145
145
self .work_id = work_id
146
146
self .exception = exception
147
147
self .result = result
148
+ self .exit_pid = exit_pid
148
149
149
150
class _CallItem (object ):
150
151
def __init__ (self , work_id , fn , args , kwargs ):
@@ -201,17 +202,19 @@ def _process_chunk(fn, chunk):
201
202
return [fn (* args ) for args in chunk ]
202
203
203
204
204
- def _sendback_result (result_queue , work_id , result = None , exception = None ):
205
+ def _sendback_result (result_queue , work_id , result = None , exception = None ,
206
+ exit_pid = None ):
205
207
"""Safely send back the given result or exception"""
206
208
try :
207
209
result_queue .put (_ResultItem (work_id , result = result ,
208
- exception = exception ))
210
+ exception = exception , exit_pid = exit_pid ))
209
211
except BaseException as e :
210
212
exc = _ExceptionWithTraceback (e , e .__traceback__ )
211
- result_queue .put (_ResultItem (work_id , exception = exc ))
213
+ result_queue .put (_ResultItem (work_id , exception = exc ,
214
+ exit_pid = exit_pid ))
212
215
213
216
214
- def _process_worker (call_queue , result_queue , initializer , initargs ):
217
+ def _process_worker (call_queue , result_queue , initializer , initargs , max_tasks = None ):
215
218
"""Evaluates calls from call_queue and places the results in result_queue.
216
219
217
220
This worker is run in a separate process.
@@ -232,25 +235,38 @@ def _process_worker(call_queue, result_queue, initializer, initargs):
232
235
# The parent will notice that the process stopped and
233
236
# mark the pool broken
234
237
return
238
+ num_tasks = 0
239
+ exit_pid = None
235
240
while True :
236
241
call_item = call_queue .get (block = True )
237
242
if call_item is None :
238
243
# Wake up queue management thread
239
244
result_queue .put (os .getpid ())
240
245
return
246
+
247
+ if max_tasks is not None :
248
+ num_tasks += 1
249
+ if num_tasks >= max_tasks :
250
+ exit_pid = os .getpid ()
251
+
241
252
try :
242
253
r = call_item .fn (* call_item .args , ** call_item .kwargs )
243
254
except BaseException as e :
244
255
exc = _ExceptionWithTraceback (e , e .__traceback__ )
245
- _sendback_result (result_queue , call_item .work_id , exception = exc )
256
+ _sendback_result (result_queue , call_item .work_id , exception = exc ,
257
+ exit_pid = exit_pid )
246
258
else :
247
- _sendback_result (result_queue , call_item .work_id , result = r )
259
+ _sendback_result (result_queue , call_item .work_id , result = r ,
260
+ exit_pid = exit_pid )
248
261
del r
249
262
250
263
# Liberate the resource as soon as possible, to avoid holding onto
251
264
# open files or shared memory that is not needed anymore
252
265
del call_item
253
266
267
+ if exit_pid is not None :
268
+ return
269
+
254
270
255
271
class _ExecutorManagerThread (threading .Thread ):
256
272
"""Manages the communication between this process and the worker processes.
@@ -301,6 +317,10 @@ def weakref_cb(_,
301
317
# A queue.Queue of work ids e.g. Queue([5, 6, ...]).
302
318
self .work_ids_queue = executor ._work_ids
303
319
320
+ # Maximum number of tasks a worker process can execute before
321
+ # exiting safely
322
+ self .max_tasks_per_child = executor ._max_tasks_per_child
323
+
304
324
# A dict mapping work ids to _WorkItems e.g.
305
325
# {5: <_WorkItem...>, 6: <_WorkItem...>, ...}
306
326
self .pending_work_items = executor ._pending_work_items
@@ -320,15 +340,23 @@ def run(self):
320
340
return
321
341
if result_item is not None :
322
342
self .process_result_item (result_item )
343
+
344
+ process_exited = result_item .exit_pid is not None
345
+ if process_exited :
346
+ p = self .processes .pop (result_item .exit_pid )
347
+ p .join ()
348
+
323
349
# Delete reference to result_item to avoid keeping references
324
350
# while waiting on new results.
325
351
del result_item
326
352
327
- # attempt to increment idle process count
328
- executor = self .executor_reference ()
329
- if executor is not None :
330
- executor ._idle_worker_semaphore .release ()
331
- del executor
353
+ if executor := self .executor_reference ():
354
+ if process_exited :
355
+ with self .shutdown_lock :
356
+ executor ._adjust_process_count ()
357
+ else :
358
+ executor ._idle_worker_semaphore .release ()
359
+ del executor
332
360
333
361
if self .is_shutting_down ():
334
362
self .flag_executor_shutting_down ()
@@ -578,7 +606,7 @@ class BrokenProcessPool(_base.BrokenExecutor):
578
606
579
607
class ProcessPoolExecutor (_base .Executor ):
580
608
def __init__ (self , max_workers = None , mp_context = None ,
581
- initializer = None , initargs = ()):
609
+ initializer = None , initargs = (), * , max_tasks_per_child = None ):
582
610
"""Initializes a new ProcessPoolExecutor instance.
583
611
584
612
Args:
@@ -589,6 +617,11 @@ def __init__(self, max_workers=None, mp_context=None,
589
617
object should provide SimpleQueue, Queue and Process.
590
618
initializer: A callable used to initialize worker processes.
591
619
initargs: A tuple of arguments to pass to the initializer.
620
+ max_tasks_per_child: The maximum number of tasks a worker process can
621
+ complete before it will exit and be replaced with a fresh
622
+ worker process, to enable unused resources to be freed. The
623
+ default value is None, which means worker process will live
624
+ as long as the executor will live.
592
625
"""
593
626
_check_system_limits ()
594
627
@@ -616,6 +649,13 @@ def __init__(self, max_workers=None, mp_context=None,
616
649
self ._initializer = initializer
617
650
self ._initargs = initargs
618
651
652
+ if max_tasks_per_child is not None :
653
+ if not isinstance (max_tasks_per_child , int ):
654
+ raise TypeError ("max_tasks_per_child must be an integer" )
655
+ elif max_tasks_per_child <= 0 :
656
+ raise ValueError ("max_tasks_per_child must be >= 1" )
657
+ self ._max_tasks_per_child = max_tasks_per_child
658
+
619
659
# Management thread
620
660
self ._executor_manager_thread = None
621
661
@@ -678,7 +718,8 @@ def _adjust_process_count(self):
678
718
args = (self ._call_queue ,
679
719
self ._result_queue ,
680
720
self ._initializer ,
681
- self ._initargs ))
721
+ self ._initargs ,
722
+ self ._max_tasks_per_child ))
682
723
p .start ()
683
724
self ._processes [p .pid ] = p
684
725
0 commit comments