Consider this MWE:
from multiprocessing import RLock
class TheSetup:
def __init__(self):
self._hardware_lock = RLock()
def hold_hardware(self):
return self._hardware_lock
def do_thing_with_hardware(self):
with self._hardware_lock:
print('Doing thing...')
def do_other_thing_with_hardware(self):
with self._hardware_lock:
print('Doing other thing...')
if __name__=='__main__':
from multiprocessing.managers import BaseManager
class TheSetupManager(BaseManager):
pass
the_setup = TheSetup()
TheSetupManager.register('get_the_setup', callable=lambda:the_setup)
m = TheSetupManager(address=('', 50000), authkey=b'abracadabra')
s = m.get_server()
print('Serving...')
s.serve_forever()
When this script is executed, it instantiates the_setup and serves it. Then I want clients to be able to do things like this from other scripts:
from multiprocessing.managers import BaseManager
class TheSetup(BaseManager):
pass
TheSetup.register('get_the_setup')
m = TheSetup(address=('', 50000), authkey=b'abracadabra')
m.connect()
the_setup = m.get_the_setup()
with the_setup.hold_hardware(): # Removing this `with` it works fine, but I cannot guarantee to the client that nobody else will use this hardware in the meantime.
the_setup.do_thing_with_hardware()
the_setup.do_other_thing_with_hardware()
However, I get RuntimeError: RLock objects should only be shared between processes through inheritance. If the with the_setup.hold_hardware(): is removed, it works fine but then I cannot guarantee that the hardware wasn't used by someone else in the middle.
Is it possible to do what I want? I.e. having the_setup running 24/7 and allowing interaction with it at any time from other Python instances. How?
Update : I have included a patch for
multiprocessing.managersto assimilateRLocksseamlessly within. Scroll below to the next section.Multiprocessing does not like it when you pass objects used for synchronization inside other such objects. That means you cannot put semaphores, locks, queues, pipes inside other queues and pipes (those offered through multiprocessing library). When you create a manager using
BaseManager, it uses pipes internally to establish communication between different processes and the manager process. So when you dothe_setup.hold_hardware(), you are essentially attempting to pass anRlockthrough a pipe, which as discussed, does not work.Why workarounds don't work
The most simplest fix one would think of using here would be to create a manager and use
manager.Rlock. This usesthreading.Rlockinstead of the one available through multiprocessing (and therefore can be shared using queues/pipes), but works in a multiprocessing environment because access to it is synchronized though pipes (again, it's using a manager).Hence, this code should at least execute:
server.py
client.py
Note that we need to set the manager and the client's authkey to the same value otherwise there will be an authentication error when attempting to unpickle the lock.
But regardless, even though the code will run, it won't do what you think it should do. It will block when trying to run
the_setup.do_thing_with_hardware(). This is because theRlockis actually inside manager process, and when you get the lock usingwith the_setup.hold_hardware()you are actually getting a proxy of the lock instead (try doingprint(type(the_setup.hold_hardware()))). When you attempt to acquire the lock using the proxy, the appropriate function name is sent to the manager process and is executed on the managed object via threading. This defeats the whole purpose ofRlockand the implementation ofRlockinside managers is useless.Patching multiprocessing to work with RLocks
If you really want to use RLocks inside managers, then you will need to make your own managers and proxies, by subclassing
BaseManagerandBaseProxy, to relay process identifiers (like pids) which can then be used to create RLocks. Consider the below "patch":Over here,
PIDProxyis identical toBaseProxy, except for the fact that apart from only sending the method name and arguments to be called on the managed object, it sends an additional identifier with the valuestr(os.getpid())as well. Similarly,ForwardPIDManageris identical toBaseManager, except for the fact the it uses a modified subclass ofmultiprocessing.managers.Serverrather than the default parent, to start the server process.PIDServermodifies it's parent to accept an extra variable (the process identifier) when unpacking requests from proxies and stores it's value inside the current thread's local storage (created after executinginitinside the server process; further reading about thread local storage here). This happens before the requested function is executed, meaning that all methods of the managed object will have access to this storage. Lastly,MyAutoProxyandMyMakeProxyTypeoverride the default methods to create proxies using our subclasses instead.All you need to do now is to use
ForwardPIDManagerinstead ofBaseManager, and specify theproxytypekwarg explicitly asMyAutoProxy. All managed objects will then have access to the pid of the process that called the function by doingfrom within the functions on the managed object.
Creating RLock
Using this, we can create our own implementation of RLocks, which closely mirrors that of
threading.RLock, but uses thispid_registry.forwarded_forto verify owners instead ofthreading.get_ident:Keep in mind that objects of
ManagerRLockare not picklable, and therefore should not be passed around by value from manager to proxies. You can, however, expose it's methods (acquire,release) to proxies without risk (example in the below section). Also, these are meant to be created directly, therefore do not use another manager to create them.Example implementation
Using our patch and
ManagerRLock, our implementation ofTheSetupclass becomes like this:One important thing to notice here is that you are not exposing the whole lock, only its methods (look at
hold_hardwareandrelease_hardware). This is important becauseManagerRLockjust likemultiprocessing.RLock, is unpicklable. One side effect of this is that you can't directly use context managers for the lock and you would have to doinside client.py instead of using a with block. However, if this bothers you can create a wrapper for the lock from within the client and use that instead (example given in the next section).
Lastly, also notice how we call
initfrom inside the main module itself. This is becauseinitmust be executed in the server process, and since we are retrieving the server and callingserve_foreverinside the main process itself, we must executeinitthere too. If instead you are using the.start()method of managers to create the server in another process, here is the equivalent code:Final solution
Combine the whole patch provided above (classes
PIDProxy,PIDServer,ForwardPIDManager, and functionsinit,MyMakeProxyType,MyAutoProxy), theManagerRLockclass, and code related to your implementation (classTheSetupalong with theif __name__...block) into one single server.py file. Then, an example client.py which uses the RLock can be like below:client.py
Notice the use of
LockWrapperas a way to use the lock with a context manager.A word about threads...
The solution relies on using
threading.local, therefore, trying to accesspid_registry.forwarded_forfrom inside another thread would fail. Hence, if you are using threading inside your shared class, then make sure you explicitly pass thepidto thread upon starting.Additionally,
ManagerRLockexpects single-threaded processes (processes can be more than one) to access it. This means that if you are running multiple processes, which each are also multi-threaded, then usingManagerRLockmight be unsafe (untested). However, if this is the case then you can trivially extend the patch by passing not only the pid, but the thread identifer as well (from insidePIDProxy) and store this insidepid_registry(from insidePIDServer). Then you will have access to the thread as well as the pid which sent the request to the manager insideManagerRLock, and you can then decide the current owner of the lock based on both these variables.Edit: A quick note about proxies, if you want to use your own (and not use
MyAutoProxy), then you can do so, just make sure that the proxy subclassesPIDProxy.