[Tutor] Threads, locking and race condition

Branimir Petrovic BranimirP@cpas.com
Wed Jan 15 23:12:02 2003


Flipping through wonderful 'Python Cookbook' I stumbled across this
example that rang my 'alarm bells'. Looks like there is 'something
little' having to do with multithreading that I do not quite get...

I'll post the whole example here as it is brief. The cookbook recipe
6.1 - 'Storing Per-Thread Information':

try:
	import thread
except:
	"""Single-threaded OS, return standard dictionary"""
	_tss = {}
	def get_thread_storage():
		return _tss
else:
	"""OS supports multithreading, so - to work:"""
	_tss = {}
	_tss_lock = thread.allocate_lock()

	def get_thread_storage():
		thread_id = thread.get_ident()
		tss = _tss.get(thread_id)

		if tss is None:	# First time being called by this thread
			try:		# Entering critical section
				_tss_lock.acquire()
				_tss[thread_id] = tss = {}
			finally:
				_tss_lock.release()

		return tss

The idea behind this piece of code is fairly straightforward - namely
to use the dictionary object to store other - thread specific
dictionaries. Assuming we have number of concurrent threads and
assuming each one gets its own little piece inside shared dictionary
everything seems to be simple and clear. Each thread will be allowed
to use/access only its own storage area, while unique ThreadID will be
the key or 'pointer' to the proper 'piece of pie'.

Soon after thinking a bit deeper about this concept, looking at the
above code snippet, and pondering about locking and synchronizing
access to shared object, I started feeling uneasy. Longer I thought
about it - more questions I had. It got to the point where I am not
sure about anything related to this subject.

I must admit that I do not plan to use above example, but I do need
to fully understand any issues related to concurrence and locking.

What does not 'sit' with me very well in the above example is late
locking - where 'late' is just my nagging impression that prompted
this lengthy post. What will happen in following scenario:

- Say Thread-3 needs to access its storage and calls the function:

	def get_thread_storage():
		thread_id = thread.get_ident()
		tss = _tss.get(thread_id)

At this very point having reached its 10-th byte code instruction,
Thread-3 is suspended, so that other threads will get their chance
to run.

- By pure chance/coincidence Thread-6 is awaken, and being just
about to call get_thread_storage() - this time it calls it indeed,
and not only that it calls the function, but also it manages to exit
from it by the time Thread-6 'fades' and goes to forced 'sleep',

- By another remarkable turn of events, Thread-3 is awaken again,
only to find variable tss that is now pointing to a value set by run of
interrupting Tread-6. Thread-3 will not have slightest 'clue' that its
own value was overwritten 'behind its back' while its execution was
suspended. As a result Thread-3 will now return wrong dictionary?

Do I see the problem that does not exist? If you spot anything wrong
with my (contrived?) assumption/scenario - please let me know.



Let's examine other option:

- Say we change function in such a way as to obtain lock with very
first step into the function:

	def get_thread_storage():
		tss_lock.acquire()
		thread_id = thread.get_ident()

#	...and so on ending with:

			finally:
				_tss_lock.release()

		return tss

Is it possible that Thread-3 does its job and release the lock object,
but just before returning tss - scheduler suspends it and run Thread-6
that calls the function, and manages to finish it and exit the
function? Wouldn't Thread-3 if awaken now return the tss as left by
Thread-6?

The way I see two proposed scenarios - both are possible, second one
less but nevertheless - still possible. Given long enough time - 
'mishap' will happen for sure. Right or wrong?

Which brings me to next nagging question:
- How 'granular' are Python's byte code instructions?
Is this:	
	thread_id = thread.get_ident() 
one byte code instruction or more than one byte code instruction?

To slightly re-phrase the question:
- Is the 'line' of code (such as thread_id = thread.get_ident()) 
guaranteed to execute and fully finish before Python switches to
another thread or not?

- If 'yes' is answer to the above question, what would happen in 
this (admittedly - artificial) case:

	return choice and [yes][0] or [no][0]

How many byte codes would that be?

- And what about:
	return mySillyChoice() and toughChoiceA() or toughChoiceB()

This will 'break' (be switched) at any convenient spot in any of three
function calls yes/no?

I would very much like to hear back (hopefully) on any of outlined
questions.

If you as far as this line, thanks for your patience.

Branimir