Best Practices when designing Pyro objects related to Threads
Here are my rules-of-thumb which may help others who are designing Pyro remote objects:
- Never design threads such that they use the same reference to a Pyro proxy. This is a documented no-no and leads to deadlocks. If you stick to rule 1 then this will not be a problem. (Update: this is no longer a requirement since Pyro 3.8, because they contain automatic thread locking inside the proxy)
- Avoid keeping persistent references to Pyro proxies in long-running servers. If the socket connection gets closed then the remote object will stop working. A more robust approach re-acquires the proxy each time it is used. There is a trade off here between robustness and performance.
Be careful about using Pyro.core.SynchronizedObjBase. This class synchronizes access around the entire remote object method. If the method engages in a conversation with another server a deadlock is possible. The alternative is to derive from Pyro.core.ObjBase and use semaphores around the data accesses that must be synchronized.
- Rules of thumb for using Python semaphores in Pyro.
Use the Python threading.BoundedSemaphore class for locking.
- Always Use try, finally to ensure the acquired semaphore gets released.
- Use a different semaphore for each object that needs to be synced, but only acquire one lock at a time. For example...
def GetPhoneAndEmail(self, user):
# Get phone
self.phoneLock.acquire()
try:
phone = self.phones[user]
finally:
self.phoneLock.release()
# Get email
self.emailLock.acquire()
try:
email = self.emails[user]
finally:
self.emailLock.release()
return phone, email- It is sometimes not necessary to synchronize simple reads of remote data objects. For example...
def GetCurrentLogins(self):
# Just returning current state, no lock needed
return self.logins.keys()- When iterating over a sequence of shared data, always acquire/release around the entire iteration. Even if you are only reading the data. For example...
def GetAdmins(self):
admins = []
self.userLock.acquire()
try:
for user in self.users:
if user.privileges == ADMIN:
admins.append(user)
finally:
self.userLock.release()
return admins- Do not implement a pathological conversation. When two or more remote objects talk, it is possible to implement a deadlock scenario even if all other practices you are following are good. For example...
class Obj1:
def GetFoosLikeABar(self):
foos = []
obj2Proxy = self.GetObj2Proxy()
self.fooLock.acquire()
try:
for foo in self.foos:
if obj2Proxy.IsFooLikeABar(foo):
foos.append(foo)
finally:
self.fooLock.release()
return foos
class Obj2:
def IsFooLikeABar(self, foo):
self.barLock.acquire()
try:
for bar in self.bars:
if foo == bar:
return True
finally:
self.barLock.release()
return False
def ComposeBars(self):
modBars = []
obj1Proxy = self.GetObj1Proxy()
self.barLock.acquire()
try:
foos = obj1Proxy.GetFoosLikeABar() # <--- Deadlock
for bar in self.bars:
modBars.append(self.ModBar(bar, foos))
finally:
self.barLock.release()
return modBarsIn the above example, the call to Obj2.ComposeBars() results in a deadlock because the conversation leads to the Obj2.barLock being doubly acquired.
-Jason
