Skip to content

Commit 86a7668

Browse files
committed
Fixes issue #6766: Updated multiprocessing Proxy Objects to support nesting
1 parent 1aa642f commit 86a7668

File tree

3 files changed

+192
-56
lines changed

3 files changed

+192
-56
lines changed

Doc/library/multiprocessing.rst

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,7 +1682,9 @@ their parent process exits. The manager classes are defined in the
16821682
of processes. Objects of this type are returned by
16831683
:func:`multiprocessing.Manager`.
16841684

1685-
It also supports creation of shared lists and dictionaries.
1685+
Its methods create and return :ref:`multiprocessing-proxy_objects` for a
1686+
number of commonly used data types to be synchronized across processes.
1687+
This notably includes shared lists and dictionaries.
16861688

16871689
.. method:: Barrier(parties[, action[, timeout]])
16881690

@@ -1745,31 +1747,17 @@ their parent process exits. The manager classes are defined in the
17451747
dict(mapping)
17461748
dict(sequence)
17471749

1748-
Create a shared ``dict`` object and return a proxy for it.
1750+
Create a shared :class:`dict` object and return a proxy for it.
17491751

17501752
.. method:: list()
17511753
list(sequence)
17521754

1753-
Create a shared ``list`` object and return a proxy for it.
1754-
1755-
.. note::
1756-
1757-
Modifications to mutable values or items in dict and list proxies will not
1758-
be propagated through the manager, because the proxy has no way of knowing
1759-
when its values or items are modified. To modify such an item, you can
1760-
re-assign the modified object to the container proxy::
1761-
1762-
# create a list proxy and append a mutable object (a dictionary)
1763-
lproxy = manager.list()
1764-
lproxy.append({})
1765-
# now mutate the dictionary
1766-
d = lproxy[0]
1767-
d['a'] = 1
1768-
d['b'] = 2
1769-
# at this point, the changes to d are not yet synced, but by
1770-
# reassigning the dictionary, the proxy is notified of the change
1771-
lproxy[0] = d
1755+
Create a shared :class:`list` object and return a proxy for it.
17721756

1757+
.. versionchanged:: 3.6
1758+
Shared objects are capable of being nested. For example, a shared
1759+
container object such as a shared list can contain other shared objects
1760+
which will all be managed and synchronized by the :class:`SyncManager`.
17731761

17741762
.. class:: Namespace
17751763

@@ -1881,6 +1869,8 @@ client to access it remotely::
18811869
>>> s = m.get_server()
18821870
>>> s.serve_forever()
18831871

1872+
.. _multiprocessing-proxy_objects:
1873+
18841874
Proxy Objects
18851875
~~~~~~~~~~~~~
18861876

@@ -1890,8 +1880,7 @@ proxy. Multiple proxy objects may have the same referent.
18901880

18911881
A proxy object has methods which invoke corresponding methods of its referent
18921882
(although not every method of the referent will necessarily be available through
1893-
the proxy). A proxy can usually be used in most of the same ways that its
1894-
referent can:
1883+
the proxy). In this way, a proxy can be used just like its referent can:
18951884

18961885
.. doctest::
18971886

@@ -1912,20 +1901,56 @@ the referent, whereas applying :func:`repr` will return the representation of
19121901
the proxy.
19131902

19141903
An important feature of proxy objects is that they are picklable so they can be
1915-
passed between processes. Note, however, that if a proxy is sent to the
1916-
corresponding manager's process then unpickling it will produce the referent
1917-
itself. This means, for example, that one shared object can contain a second:
1904+
passed between processes. As such, a referent can contain
1905+
:ref:`multiprocessing-proxy_objects`. This permits nesting of these managed
1906+
lists, dicts, and other :ref:`multiprocessing-proxy_objects`:
19181907

19191908
.. doctest::
19201909

19211910
>>> a = manager.list()
19221911
>>> b = manager.list()
19231912
>>> a.append(b) # referent of a now contains referent of b
19241913
>>> print(a, b)
1925-
[[]] []
1914+
[<ListProxy object, typeid 'list' at ...>] []
19261915
>>> b.append('hello')
1927-
>>> print(a, b)
1928-
[['hello']] ['hello']
1916+
>>> print(a[0], b)
1917+
['hello'] ['hello']
1918+
1919+
Similarly, dict and list proxies may be nested inside one another::
1920+
1921+
>>> l_outer = manager.list([ manager.dict() for i in range(2) ])
1922+
>>> d_first_inner = l_outer[0]
1923+
>>> d_first_inner['a'] = 1
1924+
>>> d_first_inner['b'] = 2
1925+
>>> l_outer[1]['c'] = 3
1926+
>>> l_outer[1]['z'] = 26
1927+
>>> print(l_outer[0])
1928+
{'a': 1, 'b': 2}
1929+
>>> print(l_outer[1])
1930+
{'c': 3, 'z': 26}
1931+
1932+
If standard (non-proxy) :class:`list` or :class:`dict` objects are contained
1933+
in a referent, modifications to those mutable values will not be propagated
1934+
through the manager because the proxy has no way of knowing when the values
1935+
contained within are modified. However, storing a value in a container proxy
1936+
(which triggers a ``__setitem__`` on the proxy object) does propagate through
1937+
the manager and so to effectively modify such an item, one could re-assign the
1938+
modified value to the container proxy::
1939+
1940+
# create a list proxy and append a mutable object (a dictionary)
1941+
lproxy = manager.list()
1942+
lproxy.append({})
1943+
# now mutate the dictionary
1944+
d = lproxy[0]
1945+
d['a'] = 1
1946+
d['b'] = 2
1947+
# at this point, the changes to d are not yet synced, but by
1948+
# updating the dictionary, the proxy is notified of the change
1949+
lproxy[0] = d
1950+
1951+
This approach is perhaps less convenient than employing nested
1952+
:ref:`multiprocessing-proxy_objects` for most use cases but also
1953+
demonstrates a level of control over the synchronization.
19291954

19301955
.. note::
19311956

Lib/multiprocessing/managers.py

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ def __init__(self, registry, address, authkey, serializer):
142142

143143
self.id_to_obj = {'0': (None, ())}
144144
self.id_to_refcount = {}
145-
self.mutex = threading.RLock()
145+
self.id_to_local_proxy_obj = {}
146+
self.mutex = threading.Lock()
146147

147148
def serve_forever(self):
148149
'''
@@ -227,7 +228,14 @@ def serve_client(self, conn):
227228
methodname = obj = None
228229
request = recv()
229230
ident, methodname, args, kwds = request
230-
obj, exposed, gettypeid = id_to_obj[ident]
231+
try:
232+
obj, exposed, gettypeid = id_to_obj[ident]
233+
except KeyError as ke:
234+
try:
235+
obj, exposed, gettypeid = \
236+
self.id_to_local_proxy_obj[ident]
237+
except KeyError as second_ke:
238+
raise ke
231239

232240
if methodname not in exposed:
233241
raise AttributeError(
@@ -308,7 +316,7 @@ def debug_info(self, c):
308316
'''
309317
with self.mutex:
310318
result = []
311-
keys = list(self.id_to_obj.keys())
319+
keys = list(self.id_to_refcount.keys())
312320
keys.sort()
313321
for ident in keys:
314322
if ident != '0':
@@ -321,7 +329,8 @@ def number_of_objects(self, c):
321329
'''
322330
Number of shared objects
323331
'''
324-
return len(self.id_to_obj) - 1 # don't count ident='0'
332+
# Doesn't use (len(self.id_to_obj) - 1) as we shouldn't count ident='0'
333+
return len(self.id_to_refcount)
325334

326335
def shutdown(self, c):
327336
'''
@@ -363,13 +372,9 @@ def create(self, c, typeid, *args, **kwds):
363372
self.id_to_obj[ident] = (obj, set(exposed), method_to_typeid)
364373
if ident not in self.id_to_refcount:
365374
self.id_to_refcount[ident] = 0
366-
# increment the reference count immediately, to avoid
367-
# this object being garbage collected before a Proxy
368-
# object for it can be created. The caller of create()
369-
# is responsible for doing a decref once the Proxy object
370-
# has been created.
371-
self.incref(c, ident)
372-
return ident, tuple(exposed)
375+
376+
self.incref(c, ident)
377+
return ident, tuple(exposed)
373378

374379
def get_methods(self, c, token):
375380
'''
@@ -387,15 +392,45 @@ def accept_connection(self, c, name):
387392

388393
def incref(self, c, ident):
389394
with self.mutex:
390-
self.id_to_refcount[ident] += 1
395+
try:
396+
self.id_to_refcount[ident] += 1
397+
except KeyError as ke:
398+
# If no external references exist but an internal (to the
399+
# manager) still does and a new external reference is created
400+
# from it, restore the manager's tracking of it from the
401+
# previously stashed internal ref.
402+
if ident in self.id_to_local_proxy_obj:
403+
self.id_to_refcount[ident] = 1
404+
self.id_to_obj[ident] = \
405+
self.id_to_local_proxy_obj[ident]
406+
obj, exposed, gettypeid = self.id_to_obj[ident]
407+
util.debug('Server re-enabled tracking & INCREF %r', ident)
408+
else:
409+
raise ke
391410

392411
def decref(self, c, ident):
412+
if ident not in self.id_to_refcount and \
413+
ident in self.id_to_local_proxy_obj:
414+
util.debug('Server DECREF skipping %r', ident)
415+
return
416+
393417
with self.mutex:
394418
assert self.id_to_refcount[ident] >= 1
395419
self.id_to_refcount[ident] -= 1
396420
if self.id_to_refcount[ident] == 0:
397-
del self.id_to_obj[ident], self.id_to_refcount[ident]
398-
util.debug('disposing of obj with id %r', ident)
421+
del self.id_to_refcount[ident]
422+
423+
if ident not in self.id_to_refcount:
424+
# Two-step process in case the object turns out to contain other
425+
# proxy objects (e.g. a managed list of managed lists).
426+
# Otherwise, deleting self.id_to_obj[ident] would trigger the
427+
# deleting of the stored value (another managed object) which would
428+
# in turn attempt to acquire the mutex that is already held here.
429+
self.id_to_obj[ident] = (None, (), None) # thread-safe
430+
util.debug('disposing of obj with id %r', ident)
431+
with self.mutex:
432+
del self.id_to_obj[ident]
433+
399434

400435
#
401436
# Class to represent state of a manager
@@ -658,7 +693,7 @@ class BaseProxy(object):
658693
_mutex = util.ForkAwareThreadLock()
659694

660695
def __init__(self, token, serializer, manager=None,
661-
authkey=None, exposed=None, incref=True):
696+
authkey=None, exposed=None, incref=True, manager_owned=False):
662697
with BaseProxy._mutex:
663698
tls_idset = BaseProxy._address_to_local.get(token.address, None)
664699
if tls_idset is None:
@@ -680,6 +715,12 @@ def __init__(self, token, serializer, manager=None,
680715
self._serializer = serializer
681716
self._Client = listener_client[serializer][1]
682717

718+
# Should be set to True only when a proxy object is being created
719+
# on the manager server; primary use case: nested proxy objects.
720+
# RebuildProxy detects when a proxy is being created on the manager
721+
# and sets this value appropriately.
722+
self._owned_by_manager = manager_owned
723+
683724
if authkey is not None:
684725
self._authkey = process.AuthenticationString(authkey)
685726
elif self._manager is not None:
@@ -738,6 +779,10 @@ def _getvalue(self):
738779
return self._callmethod('#GETVALUE')
739780

740781
def _incref(self):
782+
if self._owned_by_manager:
783+
util.debug('owned_by_manager skipped INCREF of %r', self._token.id)
784+
return
785+
741786
conn = self._Client(self._token.address, authkey=self._authkey)
742787
dispatch(conn, None, 'incref', (self._id,))
743788
util.debug('INCREF %r', self._token.id)
@@ -822,19 +867,19 @@ def __str__(self):
822867
def RebuildProxy(func, token, serializer, kwds):
823868
'''
824869
Function used for unpickling proxy objects.
825-
826-
If possible the shared object is returned, or otherwise a proxy for it.
827870
'''
828871
server = getattr(process.current_process(), '_manager_server', None)
829-
830872
if server and server.address == token.address:
831-
return server.id_to_obj[token.id][0]
832-
else:
833-
incref = (
834-
kwds.pop('incref', True) and
835-
not getattr(process.current_process(), '_inheriting', False)
836-
)
837-
return func(token, serializer, incref=incref, **kwds)
873+
util.debug('Rebuild a proxy owned by manager, token=%r', token)
874+
kwds['manager_owned'] = True
875+
if token.id not in server.id_to_local_proxy_obj:
876+
server.id_to_local_proxy_obj[token.id] = \
877+
server.id_to_obj[token.id]
878+
incref = (
879+
kwds.pop('incref', True) and
880+
not getattr(process.current_process(), '_inheriting', False)
881+
)
882+
return func(token, serializer, incref=incref, **kwds)
838883

839884
#
840885
# Functions to create proxies and proxy types

Lib/test/_test_multiprocessing.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1628,13 +1628,33 @@ def test_list(self):
16281628
d = [a, b]
16291629
e = self.list(d)
16301630
self.assertEqual(
1631-
e[:],
1631+
[element[:] for element in e],
16321632
[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]]
16331633
)
16341634

16351635
f = self.list([a])
16361636
a.append('hello')
1637-
self.assertEqual(f[:], [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'hello']])
1637+
self.assertEqual(f[0][:], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'hello'])
1638+
1639+
def test_list_proxy_in_list(self):
1640+
a = self.list([self.list(range(3)) for _i in range(3)])
1641+
self.assertEqual([inner[:] for inner in a], [[0, 1, 2]] * 3)
1642+
1643+
a[0][-1] = 55
1644+
self.assertEqual(a[0][:], [0, 1, 55])
1645+
for i in range(1, 3):
1646+
self.assertEqual(a[i][:], [0, 1, 2])
1647+
1648+
self.assertEqual(a[1].pop(), 2)
1649+
self.assertEqual(len(a[1]), 2)
1650+
for i in range(0, 3, 2):
1651+
self.assertEqual(len(a[i]), 3)
1652+
1653+
del a
1654+
1655+
b = self.list()
1656+
b.append(b)
1657+
del b
16381658

16391659
def test_dict(self):
16401660
d = self.dict()
@@ -1646,6 +1666,52 @@ def test_dict(self):
16461666
self.assertEqual(sorted(d.values()), [chr(i) for i in indices])
16471667
self.assertEqual(sorted(d.items()), [(i, chr(i)) for i in indices])
16481668

1669+
def test_dict_proxy_nested(self):
1670+
pets = self.dict(ferrets=2, hamsters=4)
1671+
supplies = self.dict(water=10, feed=3)
1672+
d = self.dict(pets=pets, supplies=supplies)
1673+
1674+
self.assertEqual(supplies['water'], 10)
1675+
self.assertEqual(d['supplies']['water'], 10)
1676+
1677+
d['supplies']['blankets'] = 5
1678+
self.assertEqual(supplies['blankets'], 5)
1679+
self.assertEqual(d['supplies']['blankets'], 5)
1680+
1681+
d['supplies']['water'] = 7
1682+
self.assertEqual(supplies['water'], 7)
1683+
self.assertEqual(d['supplies']['water'], 7)
1684+
1685+
del pets
1686+
del supplies
1687+
self.assertEqual(d['pets']['ferrets'], 2)
1688+
d['supplies']['blankets'] = 11
1689+
self.assertEqual(d['supplies']['blankets'], 11)
1690+
1691+
pets = d['pets']
1692+
supplies = d['supplies']
1693+
supplies['water'] = 7
1694+
self.assertEqual(supplies['water'], 7)
1695+
self.assertEqual(d['supplies']['water'], 7)
1696+
1697+
d.clear()
1698+
self.assertEqual(len(d), 0)
1699+
self.assertEqual(supplies['water'], 7)
1700+
self.assertEqual(pets['hamsters'], 4)
1701+
1702+
l = self.list([pets, supplies])
1703+
l[0]['marmots'] = 1
1704+
self.assertEqual(pets['marmots'], 1)
1705+
self.assertEqual(l[0]['marmots'], 1)
1706+
1707+
del pets
1708+
del supplies
1709+
self.assertEqual(l[0]['marmots'], 1)
1710+
1711+
outer = self.list([[88, 99], l])
1712+
self.assertIsInstance(outer[0], list) # Not a ListProxy
1713+
self.assertEqual(outer[-1][-1]['feed'], 3)
1714+
16491715
def test_namespace(self):
16501716
n = self.Namespace()
16511717
n.name = 'Bob'

0 commit comments

Comments
 (0)