Skip to content

Commit 13e96cc

Browse files
authored
Fix bpo-30596: Add close() method to multiprocessing.Process (#2010)
* Fix bpo-30596: Add close() method to multiprocessing.Process * Raise ValueError if close() is called before the Process is finished running * Add docs * Add NEWS blurb
1 parent 0ee32c1 commit 13e96cc

File tree

9 files changed

+106
-8
lines changed

9 files changed

+106
-8
lines changed

Doc/library/multiprocessing.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,16 @@ The :mod:`multiprocessing` package mostly replicates the API of the
598598
acquired a lock or semaphore etc. then terminating it is liable to
599599
cause other processes to deadlock.
600600

601+
.. method:: close()
602+
603+
Close the :class:`Process` object, releasing all resources associated
604+
with it. :exc:`ValueError` is raised if the underlying process
605+
is still running. Once :meth:`close` returns successfully, most
606+
other methods and attributes of the :class:`Process` object will
607+
raise :exc:`ValueError`.
608+
609+
.. versionadded:: 3.7
610+
601611
Note that the :meth:`start`, :meth:`join`, :meth:`is_alive`,
602612
:meth:`terminate` and :attr:`exitcode` methods should only be called by
603613
the process that created the process object.

Lib/multiprocessing/forkserver.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,12 @@ def sigchld_handler(*_unused):
210210
else:
211211
assert os.WIFEXITED(sts)
212212
returncode = os.WEXITSTATUS(sts)
213-
# Write the exit code to the pipe
214-
write_signed(child_w, returncode)
213+
# Send exit code to client process
214+
try:
215+
write_signed(child_w, returncode)
216+
except BrokenPipeError:
217+
# client vanished
218+
pass
215219
os.close(child_w)
216220
else:
217221
# This shouldn't happen really
@@ -241,8 +245,12 @@ def sigchld_handler(*_unused):
241245
finally:
242246
os._exit(code)
243247
else:
244-
# Send pid to client processes
245-
write_signed(child_w, pid)
248+
# Send pid to client process
249+
try:
250+
write_signed(child_w, pid)
251+
except BrokenPipeError:
252+
# client vanished
253+
pass
246254
pid_to_fd[pid] = child_w
247255
os.close(child_r)
248256
for fd in fds:

Lib/multiprocessing/popen_fork.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def __init__(self, process_obj):
1717
sys.stdout.flush()
1818
sys.stderr.flush()
1919
self.returncode = None
20+
self.finalizer = None
2021
self._launch(process_obj)
2122

2223
def duplicate_for_child(self, fd):
@@ -70,5 +71,9 @@ def _launch(self, process_obj):
7071
os._exit(code)
7172
else:
7273
os.close(child_w)
73-
util.Finalize(self, os.close, (parent_r,))
74+
self.finalizer = util.Finalize(self, os.close, (parent_r,))
7475
self.sentinel = parent_r
76+
77+
def close(self):
78+
if self.finalizer is not None:
79+
self.finalizer()

Lib/multiprocessing/popen_forkserver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def _launch(self, process_obj):
4949
set_spawning_popen(None)
5050

5151
self.sentinel, w = forkserver.connect_to_new_process(self._fds)
52-
util.Finalize(self, os.close, (self.sentinel,))
52+
self.finalizer = util.Finalize(self, os.close, (self.sentinel,))
5353
with open(w, 'wb', closefd=True) as f:
5454
f.write(buf.getbuffer())
5555
self.pid = forkserver.read_signed(self.sentinel)

Lib/multiprocessing/popen_spawn_posix.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def _launch(self, process_obj):
6262
f.write(fp.getbuffer())
6363
finally:
6464
if parent_r is not None:
65-
util.Finalize(self, os.close, (parent_r,))
65+
self.finalizer = util.Finalize(self, os.close, (parent_r,))
6666
for fd in (child_r, child_w, parent_w):
6767
if fd is not None:
6868
os.close(fd)

Lib/multiprocessing/popen_spawn_win32.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(self, process_obj):
5656
self.returncode = None
5757
self._handle = hp
5858
self.sentinel = int(hp)
59-
util.Finalize(self, _winapi.CloseHandle, (self.sentinel,))
59+
self.finalizer = util.Finalize(self, _winapi.CloseHandle, (self.sentinel,))
6060

6161
# send information to child
6262
set_spawning_popen(self)
@@ -96,3 +96,6 @@ def terminate(self):
9696
except OSError:
9797
if self.wait(timeout=1.0) is None:
9898
raise
99+
100+
def close(self):
101+
self.finalizer()

Lib/multiprocessing/process.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def __init__(self, group=None, target=None, name=None, args=(), kwargs={},
7676
self._config = _current_process._config.copy()
7777
self._parent_pid = os.getpid()
7878
self._popen = None
79+
self._closed = False
7980
self._target = target
8081
self._args = tuple(args)
8182
self._kwargs = dict(kwargs)
@@ -85,6 +86,10 @@ def __init__(self, group=None, target=None, name=None, args=(), kwargs={},
8586
self.daemon = daemon
8687
_dangling.add(self)
8788

89+
def _check_closed(self):
90+
if self._closed:
91+
raise ValueError("process object is closed")
92+
8893
def run(self):
8994
'''
9095
Method to be run in sub-process; can be overridden in sub-class
@@ -96,6 +101,7 @@ def start(self):
96101
'''
97102
Start child process
98103
'''
104+
self._check_closed()
99105
assert self._popen is None, 'cannot start a process twice'
100106
assert self._parent_pid == os.getpid(), \
101107
'can only start a process object created by current process'
@@ -110,12 +116,14 @@ def terminate(self):
110116
'''
111117
Terminate process; sends SIGTERM signal or uses TerminateProcess()
112118
'''
119+
self._check_closed()
113120
self._popen.terminate()
114121

115122
def join(self, timeout=None):
116123
'''
117124
Wait until child process terminates
118125
'''
126+
self._check_closed()
119127
assert self._parent_pid == os.getpid(), 'can only join a child process'
120128
assert self._popen is not None, 'can only join a started process'
121129
res = self._popen.wait(timeout)
@@ -126,6 +134,7 @@ def is_alive(self):
126134
'''
127135
Return whether process is alive
128136
'''
137+
self._check_closed()
129138
if self is _current_process:
130139
return True
131140
assert self._parent_pid == os.getpid(), 'can only test a child process'
@@ -134,6 +143,23 @@ def is_alive(self):
134143
self._popen.poll()
135144
return self._popen.returncode is None
136145

146+
def close(self):
147+
'''
148+
Close the Process object.
149+
150+
This method releases resources held by the Process object. It is
151+
an error to call this method if the child process is still running.
152+
'''
153+
if self._popen is not None:
154+
if self._popen.poll() is None:
155+
raise ValueError("Cannot close a process while it is still running. "
156+
"You should first call join() or terminate().")
157+
self._popen.close()
158+
self._popen = None
159+
del self._sentinel
160+
_children.discard(self)
161+
self._closed = True
162+
137163
@property
138164
def name(self):
139165
return self._name
@@ -174,6 +200,7 @@ def exitcode(self):
174200
'''
175201
Return exit code of process or `None` if it has yet to stop
176202
'''
203+
self._check_closed()
177204
if self._popen is None:
178205
return self._popen
179206
return self._popen.poll()
@@ -183,6 +210,7 @@ def ident(self):
183210
'''
184211
Return identifier (PID) of process or `None` if it has yet to start
185212
'''
213+
self._check_closed()
186214
if self is _current_process:
187215
return os.getpid()
188216
else:
@@ -196,6 +224,7 @@ def sentinel(self):
196224
Return a file descriptor (Unix) or handle (Windows) suitable for
197225
waiting for process termination.
198226
'''
227+
self._check_closed()
199228
try:
200229
return self._sentinel
201230
except AttributeError:
@@ -204,6 +233,8 @@ def sentinel(self):
204233
def __repr__(self):
205234
if self is _current_process:
206235
status = 'started'
236+
elif self._closed:
237+
status = 'closed'
207238
elif self._parent_pid != os.getpid():
208239
status = 'unknown'
209240
elif self._popen is None:
@@ -295,6 +326,7 @@ def __init__(self):
295326
self._name = 'MainProcess'
296327
self._parent_pid = None
297328
self._popen = None
329+
self._closed = False
298330
self._config = {'authkey': AuthenticationString(os.urandom(32)),
299331
'semprefix': '/mp'}
300332
# Note that some versions of FreeBSD only allow named
@@ -307,6 +339,9 @@ def __init__(self):
307339
# Everything in self._config will be inherited by descendant
308340
# processes.
309341

342+
def close(self):
343+
pass
344+
310345

311346
_current_process = _MainProcess()
312347
_process_counter = itertools.count(1)

Lib/test/_test_multiprocessing.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,42 @@ def test_sentinel(self):
403403
p.join()
404404
self.assertTrue(wait_for_handle(sentinel, timeout=1))
405405

406+
@classmethod
407+
def _test_close(cls, rc=0, q=None):
408+
if q is not None:
409+
q.get()
410+
sys.exit(rc)
411+
412+
def test_close(self):
413+
if self.TYPE == "threads":
414+
self.skipTest('test not appropriate for {}'.format(self.TYPE))
415+
q = self.Queue()
416+
p = self.Process(target=self._test_close, kwargs={'q': q})
417+
p.daemon = True
418+
p.start()
419+
self.assertEqual(p.is_alive(), True)
420+
# Child is still alive, cannot close
421+
with self.assertRaises(ValueError):
422+
p.close()
423+
424+
q.put(None)
425+
p.join()
426+
self.assertEqual(p.is_alive(), False)
427+
self.assertEqual(p.exitcode, 0)
428+
p.close()
429+
with self.assertRaises(ValueError):
430+
p.is_alive()
431+
with self.assertRaises(ValueError):
432+
p.join()
433+
with self.assertRaises(ValueError):
434+
p.terminate()
435+
p.close()
436+
437+
wr = weakref.ref(p)
438+
del p
439+
gc.collect()
440+
self.assertIs(wr(), None)
441+
406442
def test_many_processes(self):
407443
if self.TYPE == 'threads':
408444
self.skipTest('test not appropriate for {}'.format(self.TYPE))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a ``close()`` method to ``multiprocessing.Process``.

0 commit comments

Comments
 (0)