Locking and try/finally

xtian xtian at toysinabag.com
Tue Dec 3 23:54:42 EST 2002


Hi -

While reading http://www.metaslash.com/brochure/recall.html I was
interested by the following observation:

[quote]
Although it would appear that the C++ version is always more complex
and more verbose than Python, there are times when C++ is shorter and
more convenient. A useful idiom for threaded applications is a mutex
variable that protects data from simultaneous updates. In Python, the
code looks something like this:

	  class Object:

	    def critical_function(self):
		self._mutex.acquire()
		try:
		    # Manipulate data shared by threads
		finally:
		    self._mutex.release()

The finally clause is used to release the lock, in case the critical
code should throw some sort of exception. Java requires the same
technique.

In C++, a wrapper is used around the variable to automatically lock
and unlock the mutex using the constructor and destructor. To mutually
exclude other threads, the code typically looks like this:

       void Object::critical_function()  {
	    Lock lock(&this->mutex_);

	    // Manipulate data shared by threads
       }

In C++, locally scoped objects are guaranteed to be destroyed at the
end of the enclosing block, even in the presence of exceptions. The
Lock class looks something like this:

       class Lock { 
           MutexVariable * mutex_;
	public:
           Lock(MutexVariable *mutex) : mutex_(mutex) {
mutex_->acquire(); }
	   ~Lock() { mutex_->release(); }
       };

Obtaining locks is simpler in C++ than in Python. The equivalent
Python code is always 3 lines longer. In fact, about 9% of the Python
implementation of Recall is simply obtaining and releasing locks
safely. Yet, overall, the Python code is smaller.
[/quote]

It seemed to me that, given the reference counting basis of Python GC,
a class with a __del__ method would behave roughly the same:

>>> class ConvenientLock(object):
...   def __init__(self, lock):
...     # lock the lock...
...     print "locked"
...   def __del__(self):
...     # unlock...
...     print "unlocked"
...
>>> def criticalSection():
...   l = ConvenientLock(None)
...   print "this is the critical section"
...
>>> criticalSection()
locked
this is the critical section
unlocked

I can only see one technical problem with this - if an exception was
raised in the body of the criticalSection function, the traceback
holds a reference to the frame, which holds the lock, so the lock
isn't deleted.

>>> def criticalSection2():
...   l = ConvenientLock(None)
...   print "in critical section"
...   raise Exception
...
>>> criticalSection2()
locked
in critical section
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 4, in criticalSection2
Exception
>>> raise Exception
unlocked
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
Exception
>>>

This is alright, though, I think, as long as you aren't storing the
traceback away somewhere, like the interactive interpreter does with
the last traceback here. Once it stops being the last traceback (when
I raise the second exception), the __del__ still runs.

Is there anything else?

(Hmm, it doesn't work in Jython, due to Java's gc not refcounting. I
guess that would make it a bad idea. Can you really do much
crossplatform work between Jython and CPython, though? Would you want
to (rather than using Jython for access to Java libraries, which
obviously aren't going to be available to CPython)?)

And I suppose there's the fact that anyone unfamiliar with the
ConvenientLock idiom would think that the code wasn't mutexed at all.
Not as explicit as it could be.

That said, if you had a significant chunk of your code in a large
project (large enough to support project-level idioms) riddled with
try/finally blocks doing this, and you didn't need to worry about
tracebacks, might it be worthwhile?

Just an idle thought, really.

xtian



More information about the Python-list mailing list