@@ -190,6 +190,11 @@ def __init__(self, processes=None, initializer=None, initargs=(),
190190 self ._ctx = context or get_context ()
191191 self ._setup_queues ()
192192 self ._taskqueue = queue .SimpleQueue ()
193+ # The _taskqueue_buffersize_semaphores exist to allow calling .release()
194+ # on every active semaphore when the pool is terminating to let task_handler
195+ # wake up to stop. It's a set so that each iterator object can efficiently
196+ # deregister its semaphore when iterator finishes.
197+ self ._taskqueue_buffersize_semaphores = set ()
193198 # The _change_notifier queue exist to wake up self._handle_workers()
194199 # when the cache (self._cache) is empty or when there is a change in
195200 # the _state variable of the thread that runs _handle_workers.
@@ -256,7 +261,8 @@ def __init__(self, processes=None, initializer=None, initargs=(),
256261 self , self ._terminate_pool ,
257262 args = (self ._taskqueue , self ._inqueue , self ._outqueue , self ._pool ,
258263 self ._change_notifier , self ._worker_handler , self ._task_handler ,
259- self ._result_handler , self ._cache ),
264+ self ._result_handler , self ._cache ,
265+ self ._taskqueue_buffersize_semaphores ),
260266 exitpriority = 15
261267 )
262268 self ._state = RUN
@@ -382,73 +388,43 @@ def starmap_async(self, func, iterable, chunksize=None, callback=None,
382388 return self ._map_async (func , iterable , starmapstar , chunksize ,
383389 callback , error_callback )
384390
385- def _guarded_task_generation (self , result_job , func , iterable ):
391+ def _guarded_task_generation (self , result_job , func , iterable , sema = None ):
386392 '''Provides a generator of tasks for imap and imap_unordered with
387393 appropriate handling for iterables which throw exceptions during
388394 iteration.'''
389395 try :
390396 i = - 1
391- for i , x in enumerate (iterable ):
392- yield (result_job , i , func , (x ,), {})
397+
398+ if sema is None :
399+ for i , x in enumerate (iterable ):
400+ yield (result_job , i , func , (x ,), {})
401+
402+ else :
403+ enumerated_iter = iter (enumerate (iterable ))
404+ while True :
405+ sema .acquire ()
406+ try :
407+ i , x = next (enumerated_iter )
408+ except StopIteration :
409+ break
410+ yield (result_job , i , func , (x ,), {})
411+
393412 except Exception as e :
394413 yield (result_job , i + 1 , _helper_reraises_exception , (e ,), {})
395414
396- def imap (self , func , iterable , chunksize = 1 ):
415+ def imap (self , func , iterable , chunksize = 1 , * , buffersize = None ):
397416 '''
398417 Equivalent of `map()` -- can be MUCH slower than `Pool.map()`.
399418 '''
400- self ._check_running ()
401- if chunksize == 1 :
402- result = IMapIterator (self )
403- self ._taskqueue .put (
404- (
405- self ._guarded_task_generation (result ._job , func , iterable ),
406- result ._set_length
407- ))
408- return result
409- else :
410- if chunksize < 1 :
411- raise ValueError (
412- "Chunksize must be 1+, not {0:n}" .format (
413- chunksize ))
414- task_batches = Pool ._get_tasks (func , iterable , chunksize )
415- result = IMapIterator (self )
416- self ._taskqueue .put (
417- (
418- self ._guarded_task_generation (result ._job ,
419- mapstar ,
420- task_batches ),
421- result ._set_length
422- ))
423- return (item for chunk in result for item in chunk )
419+ return self ._imap (IMapIterator , func , iterable , chunksize ,
420+ buffersize = buffersize )
424421
425- def imap_unordered (self , func , iterable , chunksize = 1 ):
422+ def imap_unordered (self , func , iterable , chunksize = 1 , * , buffersize = None ):
426423 '''
427424 Like `imap()` method but ordering of results is arbitrary.
428425 '''
429- self ._check_running ()
430- if chunksize == 1 :
431- result = IMapUnorderedIterator (self )
432- self ._taskqueue .put (
433- (
434- self ._guarded_task_generation (result ._job , func , iterable ),
435- result ._set_length
436- ))
437- return result
438- else :
439- if chunksize < 1 :
440- raise ValueError (
441- "Chunksize must be 1+, not {0!r}" .format (chunksize ))
442- task_batches = Pool ._get_tasks (func , iterable , chunksize )
443- result = IMapUnorderedIterator (self )
444- self ._taskqueue .put (
445- (
446- self ._guarded_task_generation (result ._job ,
447- mapstar ,
448- task_batches ),
449- result ._set_length
450- ))
451- return (item for chunk in result for item in chunk )
426+ return self ._imap (IMapUnorderedIterator , func , iterable , chunksize ,
427+ buffersize = buffersize )
452428
453429 def apply_async (self , func , args = (), kwds = {}, callback = None ,
454430 error_callback = None ):
@@ -497,6 +473,41 @@ def _map_async(self, func, iterable, mapper, chunksize=None, callback=None,
497473 )
498474 return result
499475
476+ def _imap (self , iterator_cls , func , iterable , chunksize = 1 ,
477+ * , buffersize = None ):
478+ self ._check_running ()
479+ if chunksize < 1 :
480+ raise ValueError (
481+ f"Chunksize must be 1+, not { chunksize } "
482+ )
483+ if buffersize is not None :
484+ if not isinstance (buffersize , int ):
485+ raise TypeError ("buffersize must be an integer or None" )
486+ if buffersize < 1 :
487+ raise ValueError ("buffersize must be None or > 0" )
488+
489+ result = iterator_cls (self , buffersize = buffersize )
490+ if chunksize == 1 :
491+ self ._taskqueue .put (
492+ (
493+ self ._guarded_task_generation (result ._job , func , iterable ,
494+ result ._buffersize_sema ),
495+ result ._set_length ,
496+ )
497+ )
498+ return result
499+ else :
500+ task_batches = Pool ._get_tasks (func , iterable , chunksize )
501+ self ._taskqueue .put (
502+ (
503+ self ._guarded_task_generation (result ._job , mapstar ,
504+ task_batches ,
505+ result ._buffersize_sema ),
506+ result ._set_length ,
507+ )
508+ )
509+ return (item for chunk in result for item in chunk )
510+
500511 @staticmethod
501512 def _wait_for_updates (sentinels , change_notifier , timeout = None ):
502513 wait (sentinels , timeout = timeout )
@@ -679,7 +690,8 @@ def _help_stuff_finish(inqueue, task_handler, size):
679690
680691 @classmethod
681692 def _terminate_pool (cls , taskqueue , inqueue , outqueue , pool , change_notifier ,
682- worker_handler , task_handler , result_handler , cache ):
693+ worker_handler , task_handler , result_handler , cache ,
694+ taskqueue_buffersize_semaphores ):
683695 # this is guaranteed to only be called once
684696 util .debug ('finalizing pool' )
685697
@@ -690,6 +702,10 @@ def _terminate_pool(cls, taskqueue, inqueue, outqueue, pool, change_notifier,
690702 change_notifier .put (None )
691703
692704 task_handler ._state = TERMINATE
705+ # Release all semaphores to wake up task_handler to stop.
706+ for buffersize_sema in tuple (taskqueue_buffersize_semaphores ):
707+ buffersize_sema .release ()
708+ taskqueue_buffersize_semaphores .discard (buffersize_sema )
693709
694710 util .debug ('helping task handler/workers to finish' )
695711 cls ._help_stuff_finish (inqueue , task_handler , len (pool ))
@@ -836,7 +852,7 @@ def _set(self, i, success_result):
836852
837853class IMapIterator (object ):
838854
839- def __init__ (self , pool ):
855+ def __init__ (self , pool , * , buffersize = None ):
840856 self ._pool = pool
841857 self ._cond = threading .Condition (threading .Lock ())
842858 self ._job = next (job_counter )
@@ -846,6 +862,11 @@ def __init__(self, pool):
846862 self ._length = None
847863 self ._unsorted = {}
848864 self ._cache [self ._job ] = self
865+ if buffersize is None :
866+ self ._buffersize_sema = None
867+ else :
868+ self ._buffersize_sema = threading .Semaphore (buffersize )
869+ self ._pool ._taskqueue_buffersize_semaphores .add (self ._buffersize_sema )
849870
850871 def __iter__ (self ):
851872 return self
@@ -856,22 +877,30 @@ def next(self, timeout=None):
856877 item = self ._items .popleft ()
857878 except IndexError :
858879 if self ._index == self ._length :
859- self ._pool = None
860- raise StopIteration from None
880+ self ._stop_iterator ()
861881 self ._cond .wait (timeout )
862882 try :
863883 item = self ._items .popleft ()
864884 except IndexError :
865885 if self ._index == self ._length :
866- self ._pool = None
867- raise StopIteration from None
886+ self ._stop_iterator ()
868887 raise TimeoutError from None
869888
889+ if self ._buffersize_sema is not None :
890+ self ._buffersize_sema .release ()
891+
870892 success , value = item
871893 if success :
872894 return value
873895 raise value
874896
897+ def _stop_iterator (self ):
898+ if self ._pool is not None :
899+ # `self._pool` could be set to `None` in previous `.next()` calls
900+ self ._pool ._taskqueue_buffersize_semaphores .discard (self ._buffersize_sema )
901+ self ._pool = None
902+ raise StopIteration from None
903+
875904 __next__ = next # XXX
876905
877906 def _set (self , i , obj ):
0 commit comments