Oddity question

Andrae Muys amuys at shortech.com.au
Wed Feb 27 01:58:25 EST 2002


Mariano Corsunsky <core.lists.python at core-sdi.com> wrote in message news:<mailman.1014745204.15257.python-list at python.org>...
> Hi there!
> 
> I was fooling around with sockets last night, using a thread to deal 
> with the read part and another method to do the print.
> 
> like this:
> 
> def thread read (self):
>   while 1:
>     self.buf=self.buf+self.my socket.recv (8)
> 
> def print buf(self):
>   if buf:
>     print "bufeer-->",buf
>     buf='""
> 
> But, what happened to me was that the -buf = ""- line seemed to be not 
> working because I got outputs like this
> 
> buffer->a
> buffer->ab
> buffer->abpleasework
> buffer->abpleaseworkpleaseflush!
> 
> So, I dont know why I changed thread read function to:
> 
>   while 1:
>     aux=self.my socket.recv (8)
>     self.buf=self.buf+aux
> 
> And this version worked properly..... but I still do not know why. To 
> me, I've only split the line in two, using an auiliar variable... but 
> THAT made the trick! 

<aside>
Heh, I seem to recall discussing the dangers of newbies messing with
threads without an understanding of concurrency just last week :)
</aside>

As I aluded above, the problem is that you haven't considered how your
two threads are interacting.  As far as I can tell you have two
threads, a producer (reading from network->buffer) and a consumer
(reading from buffer->stdout).

You must remember that the threads run *concurrently* this means that
the atomic operations that make up the statements in each thread are
interleaved in a non-deterministic fashion (or rather, any reliance on
a given order is a bug).

The code you're having problems with is appears to be effectively the
following...

thread1:
    while 1:
        buf = buf + socket.recv(8)

thread2:
    while 1:
        print buf
        buf = ""

Now when discussing threading bugs, I'm perfectly entitled to order
these operations anyway I like.  In this case I will order them
understanding that the socket.recv() call is likely to trigger a
schedule(); assignment, concatination is not; and print may or maynot
trigger depending on how agressive stdout is buffered.

However do remember that *ANY* interleving is valid, so your code
*MUST* behave correctly for *EVERY* possible ordering of instructions
between threads!

On the otherhand the instructions inside each thread execute in a
known order (well most of the time ;).  So the challange is to use the
known ordering of instructions inside each thread to impose an
ordering (where required) on the instructions *between* each thread.

But first a look at your code (so you can understand what you have
seen).

Consider

buf = buf + socket.recv(8)

This is three independent python operations, use dis to uncover how
many CPython instructions, and gdb can show you how many assembly
instructions, not all of which are atomic themselves.  For now we will
only concern ourselves with python operations/instructions (however
for correct operation, your code *MUST* handle the problem of
non-atomic assembly instruction execution)

Let's consider the following code:

>>> def read():
	return "World"

>>> def concat(a, b):
	return a + b

>>> def test():
	buf = "Hello "
	print concat(buf, read())

	
>>> test()
Hello World
>>> import dis
>>> dis.dis(test)
          0 SET_LINENO               1

          3 SET_LINENO               2
          6 LOAD_CONST               1 ('Hello ')
          9 STORE_FAST               0 (buf)

         12 SET_LINENO               3
         15 LOAD_GLOBAL              1 (concat)
         18 LOAD_FAST                0 (buf)
         21 LOAD_GLOBAL              2 (read)
         24 CALL_FUNCTION            0
         27 CALL_FUNCTION            2
         30 PRINT_ITEM          
         31 PRINT_NEWLINE       
         32 LOAD_CONST               0 (None)
         35 RETURN_VALUE        
>>> 

Note the order of the instructions @18->27.  Specifically that buf is
loaded @18, then read() is called @24, then concat is called @27.

So your analogous code is correctly ordered:

tmp1 = buf             # @18
tmp2 = socket.recv(8)  # @24
tmp3 = tmp1 + tmp2     # @27
buf = tmp3             # @35 (return vs. assign, but analogous [see @6
& @9])

(note tmp variables are references to the stack, so require no
operation to access)

For now we will treat these as atomic (I believe this is the case atm,
although the language spec provides no guarantee to that effect).

In a similar manner thread2's code:

>>> def blank():
	buf = "Hello World"
	print buf
	buf = ""

	
>>> blank()
Hello World
>>> dis.dis(blank)
          0 SET_LINENO               1

          3 SET_LINENO               2
          6 LOAD_CONST               1 ('Hello World')
          9 STORE_FAST               0 (buf)

         12 SET_LINENO               3
         15 LOAD_FAST                0 (buf)
         18 PRINT_ITEM          
         19 PRINT_NEWLINE       

         20 SET_LINENO               4
         23 LOAD_CONST               2 ('')
         26 STORE_FAST               0 (buf)
         29 LOAD_CONST               0 (None)
         32 RETURN_VALUE        
>>> 

So ordered:

tmp4 = buf  # @15
print tmp4  # @18
tmp5 = ""   # @23
buf = tmp5  # @26

So we have to interleave the following operations:

(A1) tmp1 = buf              (B1) tmp4 = buf
(A2) tmp2 = socket.recv(8)   (B2) print tmp4
(A3) tmp3 = tmp1 + tmp2      (B3) tmp5 = ""
(A4) buf = tmp3              (B4) buf = tmp5

remembering that A2 will almost certianly trigger a schedule().

Consider the ordering:
    A1,A2,B1,B2,B3,B4,A3,A4,repeat
Assuming buf starts equal to "", and do three loops:
first-loop:
tmp1 = buf              # buf == ""; tmp1 == ""; tmp2 == "";
tmp2 = socket.recv(8)   # buf == ""; tmp1 == ""; tmp2 == "foo";
tmp4 = buf              # buf == ""; tmp1 == ""; tmp2 == "foo";
print tmp4              # buf == ""; tmp1 == ""; tmp2 == "foo"; 
    output>>> ""
tmp 5 = ""              # buf == ""; tmp1 == ""; tmp2 == "foo"; 
buf = tmp5              # buf == ""; tmp1 == ""; tmp2 == "foo";
tmp3 = tmp1 + tmp2      # buf == ""; tmp1 == ""; tmp2 == "foo";
buf = tmp3              # buf == "foo"; tmp1 == ""; tmp2 == "foo";

second-loop:

tmp1 = buf              # buf == "foo"; tmp1 == "foo"; tmp2 == "foo";
tmp2 = socket.recv(8)   # buf == "foo"; tmp1 == "foo"; tmp2 == "bar";
tmp4 = buf              # buf == "foo"; tmp1 == "foo"; tmp2 == "bar";
print tmp4              # buf == "foo"; tmp1 == "foo"; tmp2 == "bar";
    output>> "foo"
tmp 5 = ""              # buf == "foo"; tmp1 == "foo"; tmp2 == "bar"; 
buf = tmp5              # buf == "foo"; tmp1 == "foo"; tmp2 == "bar";
tmp3 = tmp1 + tmp2      # buf == "foo"; tmp1 == "foo"; tmp2 == "bar";
buf = tmp3              # buf == "foobar"; tmp1 == "foo"; tmp2 ==
"bar";

third-loop:

tmp1 = buf              # buf == "foobar"; tmp1 == "foobar"; tmp2 ==
"foo";
tmp2 = socket.recv(8)   # buf == "foobar"; tmp1 == "foobar"; tmp2 ==
"baz";
tmp4 = buf              # buf == "foobar"; tmp1 == "foobar"; tmp2 ==
"baz";
print tmp4              # buf == "foobar"; tmp1 == "foobar"; tmp2 ==
"baz";
    output>> "foobar"
tmp 5 = ""              # buf == "foobar"; tmp1 == "foobar"; tmp2 ==
"baz";
buf = tmp5              # buf == "foobar"; tmp1 == "foobar"; tmp2 ==
"baz";
tmp3 = tmp1 + tmp2      # buf == "foobar"; tmp1 == "foobar"; tmp2 ==
"baz";
buf = tmp3              # buf == "foobarbaz"; tmp1 == "foo"; tmp2 ==
"baz";

and of course next time the print statement comes along you'll get
"foobarbaz".

So how do you deal with this problem?

Well the first, and most successful, approach is "Don't use threads"
:).  And that's not really a joke.  Most of the time threading is
overkill, there are often other non-threaded designes that are both
faster *and* simpler.

If however the nature of your program is such that threads are the
preferred approach, then you have use the features of the threading
module to impose your desired ordering on the instructions.

The problem you are currently facing is called a data-race.  You have
two threads both accessing the same variable (when talking threads,
variables, files, db-connections, et al are called resources).  The
traditional example used is a bank account:

balance = 1000

def get_balance():
    return balance
def put_balance(amount):
    balance = amount

Thread 1                             Thread 2
def deposit(amount):                 def withdraw(amount):
    log_deposit_start()                  log_withdraw_start()
    balance = get_balance()              balance = get_balance()
    balance = balance + amount           balance = balance - amount
    put_balance(balance)                 put_balance(balance)
    log_deposit_finish()                 log_withdraw_finish()

deposit(100)                         withdraw(100)

Which should naturally leave you with a final balance of 1000.

Now you should be able to use the same sort of thinking I used above
on your code to demonstrate that as the code stands possible answers
include 900, 1000, and 1100 (be aware these are not the only possible
answers, just the most likely ones).

The solution of course is to prevent deposit from reading or writing
to balance while withdraw is using it (ie between withdraw's
get_balance and put_balance calls), and vis-versa.  It is also
important to notice that deposit/withdraw can be executing any of the
four log_* functions while the other is modifying balance without
causing any problems.  In concurrency parlance, the balance modifying
code is a "critical section", while the log_* code is a "non-critical
section".

The important thing then when dealing with data-races is to ensure
"mutual exclusion" inside critical section, also refered to as
"serialising access" to the critical section.  The most common
approach to achiving this is via locking.

In general this works like so:

def deposit(amount):                    def withdraw(amount):
    non_critical_code()                     non_critical_code()
    balance_lock = get_balance_lock()       balance_lock =
get_balance_lock()
    balance_lock.lock()                     balance_lock.lock()
    critical_code()                         critical_code()
    balance_lock.unlock()                   balance_lock.unlock()
    non_critical_code()                     non_critical_code()

Now I haven't ever written a threaded python application before ---
I've never had the need ;).  However from the documentation for the
threading module it appears that balance_lock would be a Lock() or
RLock() [preferably the latter] and lock() and unlock() map to
aquire() and release() respectively.

So returning to your code, a minimal change, corrected version would
look like:

def __init__(self):
  self.buf = ""
  self.buflock = RLock()

def thread read (self):
  while 1:
    self.buflock.aquire()
    self.buf=self.buf+self.my_socket.recv (8)
    self.buflock.release()
 
def print buf(self):
  self.buflock.aquire()
  if self.buf:
    print "bufeer-->",self.buf
    self.buf=""
  self.buflock.release()

you should then see the behaviour you expect.

However:

a) holding a lock while performing a blocking operation is a very
*bad* thing, so at the very least read() should be rewritten:

def thread read (self):
  while 1:
    tmp = self.my_socket.recv(8)
    self.buflock.aquire()
    self.buf=self.buf + tmp
    self.buflock.release()

b) If this is a server please consider why you need threads to handle
your network io, especially when nfsd, sendmail, apache, wuftpd, lpd,
and tcpd don't.

c) If this is a gui client, understand that threads have a tendency to
interact poorly with many widget libraries, which generally provide
select/poll based event-loops with support for socket-io already.

d) If you still decide you want to use threads, consider using the
battery's included Queue module, instead of rolling your own with
string concatenation.

Anyway, regardless good-luck.

Andrae Muys



More information about the Python-list mailing list