Skip to content

Commit 441ada9

Browse files
committed
gh-89240: limit multiprocessing.Pool to 61 workers on windows
1 parent d1a89ce commit 441ada9

File tree

4 files changed

+28
-0
lines changed

4 files changed

+28
-0
lines changed

Doc/library/multiprocessing.rst

+4
Original file line numberDiff line numberDiff line change
@@ -2209,6 +2209,10 @@ with the :class:`Pool` class.
22092209

22102210
*processes* is the number of worker processes to use. If *processes* is
22112211
``None`` then the number returned by :func:`os.cpu_count` is used.
2212+
On Windows, *processes* must be equal or lower than ``61``. If it is not
2213+
then :exc:`ValueError` will be raised. If *processes* is ``None``, then
2214+
the default chosen will be at most ``61``, even if more processors are
2215+
available.
22122216

22132217
If *initializer* is not ``None`` then each worker process will call
22142218
``initializer(*initargs)`` when it starts.

Lib/multiprocessing/pool.py

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import itertools
1818
import os
1919
import queue
20+
import sys
2021
import threading
2122
import time
2223
import traceback
@@ -175,6 +176,10 @@ class Pool(object):
175176
Class which supports an async version of applying functions to arguments.
176177
'''
177178
_wrap_exception = True
179+
# On Windows, WaitForMultipleObjects is used to wait for processes to
180+
# finish. It can wait on, at most, 64 objects. There is an overhead of three
181+
# objects.
182+
_MAX_WINDOWS_WORKERS = 64 - 3
178183

179184
@staticmethod
180185
def Process(ctx, *args, **kwds):
@@ -201,8 +206,12 @@ def __init__(self, processes=None, initializer=None, initargs=(),
201206

202207
if processes is None:
203208
processes = os.cpu_count() or 1
209+
if sys.platform == 'win32':
210+
processes = min(processes, self._MAX_WINDOWS_WORKERS)
204211
if processes < 1:
205212
raise ValueError("Number of processes must be at least 1")
213+
if sys.platform == 'win32' and processes > self._MAX_WINDOWS_WORKERS:
214+
raise ValueError(f"max_workers must be <= {self._MAX_WINDOWS_WORKERS}")
206215
if maxtasksperchild is not None:
207216
if not isinstance(maxtasksperchild, int) or maxtasksperchild <= 0:
208217
raise ValueError("maxtasksperchild must be a positive int or None")
@@ -920,6 +929,7 @@ def _set(self, i, obj):
920929

921930
class ThreadPool(Pool):
922931
_wrap_exception = False
932+
_MAX_WINDOWS_WORKERS = float("inf")
923933

924934
@staticmethod
925935
def Process(ctx, *args, **kwds):

Lib/test/_test_multiprocessing.py

+12
Original file line numberDiff line numberDiff line change
@@ -2772,6 +2772,18 @@ def test_resource_warning(self):
27722772
pool = None
27732773
support.gc_collect()
27742774

2775+
class TestPoolMaxWorkers(unittest.TestCase):
2776+
@unittest.skipUnless(sys.platform=='win32', 'Windows-only process limit')
2777+
def test_max_workers_too_large(self):
2778+
with self.assertRaisesRegex(ValueError, "max_workers must be <= 61"):
2779+
multiprocessing.pool.Pool(62)
2780+
2781+
# ThreadPool have no limit.
2782+
p = multiprocessing.pool.ThreadPool(62)
2783+
p.close()
2784+
p.join()
2785+
2786+
27752787
def raising():
27762788
raise KeyError("key")
27772789

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Limit ``processes`` in :class:`multiprocessing.Pool` to 61 to work around a
2+
WaitForMultipleObjects limitation.

0 commit comments

Comments
 (0)