[python-uk] [pyconuk] Minimalistic software transactional memory

Richard Taylor r.taylor at eris.qinetiq.com
Wed Dec 12 09:43:14 CET 2007


Mike

On Tuesday 11 December 2007, Michael Sparks wrote:
> Hi Richard,
> 
> 
> On Tuesday 11 December 2007 13:36, Richard Taylor wrote:
> > I don't think that you can rely on the threadsafety of these functions.
> > Even if they are threadsafe in C Python (which I doubt that 'set' is), the
> > locking in Jython in more fine grained and would likely catch you out.
> 
> It's perhaps worth noting in the version of the code I posted, in this 
thread, 
> where it said...
> 
>    """What key areas appear least threadsafe, and any general suggestions
>       around that."""
> 
> ...I knew that set and using were not threadsafe, but wondered about other 
> parts. I perhaps should've been more explicit on that point. (I wanted to 
> simply post some ideas which showed the core logic without locking. Perhaps 
a 
> mistake :)
> 

I did miss the subtlety :-) but at least it prompted me to reply!

> Anyhow, the current version is here:
> 
> 
https://kamaelia.svn.sourceforge.net/svnroot/kamaelia/branches/private_MPS_Scratch/Bindings/STM/Axon/STM.py
> 
> In that version, "set" now looks like this:
>     def set(self, key, value):
>         success = False
>         if self.lock.acquire(0):
>             try:
>                 if not (self.store[key].version > value.version):
>                     self.store[key] = Value(value.version+1,
>                                             copy.deepcopy(value.value),
>                                             self, key)
>                     value.version= value.version+1
>                     success = True
>             finally:
>                 self.lock.release()
>         else:
>             raise BusyRetry
> 
>         if not success:
>             raise ConcurrentUpdate
> 
> and "using" has changed to "usevar: (using now relates to a collection)
> 
>     def usevar(self, key):
>         try:
>             return self.get(key)
>         except KeyError:
>             if self.lock.acquire(0):
>                 try:
>                     self.store[key] = Value(0, None,self,key)
>                 finally:
>                     self.lock.release()
>             else:
>                 raise BusyRetry
> 
>             return self.get(key)
> 
> Since mutations of the store rely on acquiring the lock on the store, that 
> should be safe(r). User code doesn't have to worry about locks however - 
> which is course the point of the code :-)
> 

I like to use explicit read locks for shared data structures, mainly because 
it makes it much safer when someone comes along and adds functionality to the 
methods later on. The next developer may not realise that the method needs to 
be threadsafe so the explicit locking code will help. I like to use 
decorators for the same reason, it is very easy to get the try/finally wrong 
end up with deadlocks.

I think that I would split the usevar method in two and use decorators to 
acquire the read and write locks e.g.

@read_lock()
def usevar(self, key):
      try:
          return self.get(key)
      except KeyError:
          return self.make(key)

@write_lock()
def make(self,key):
      self.store[key] = Value(0, None,self,key)
      return self.get(key)

> The reason for specifically using the acquire(0) call rather than acquire() 
> call is because I want it to fail hard if the lock can't be acquired. I know 
> it'd be nicer to have a finer grained lock here, but I'm personally 
primarily 
> going to be using this for rare operations rather than common operations.
>

Personally I am not sure that I would bother with the non-blocking acquire 
here. It will complicate the client code and the length of time that the lock 
will be held will be so small that almost all client code will simply retry 
the set. So I would go for a blocking acquire or maybe add a timeout based 
exception to catch a deadlock.
 
> These locks above are of course in relation to write locking. I'll think 
about 
> the read locking you've suggested.
> 
> Your locking looks incorrect on using since it both allows reading and 
writing 
> of the store. (retrieve value & if not present create & initialise)

Yep, I did say that it was not tested :-) It is wrong on 'set' as well, 
as 'set' should acquire both the read and the write lock.

> I also think the independent locks are a misnomer, but they're useful for 
> thinking about it.
> 
> > I would suggest that you should routinely wrap shared datamodels like 
these
> > in thread locks to be certain about things.
> 
> Indeed. It makes the code look worse, so for this example I was really after 
> suggestions (like yours :-) of "OK, where does this break badly" as well as 
> "does the logic look sane?".

Decorators help to hide the details from the main code path. I think that 
decorators are over used sometimes, but in this case I think that are very 
useful.

> > I would also suggest that a small change to the Value class would make it
> > possible for client code to subclass it, which might make it more 
flexible.
> 
> I'm not convinced by the changes to Value - its there for storing arbitrary 
> values, rather than extending Value itself. It's probably worth noting 
> that .clone has changed in my version to this:
> 
>     def clone(self):
>         return Value(self.version,
>                      copy.deepcopy(self.value),self.store,self.key)
> 
> Which includes deepcopy on the value stored by Value. I'm beginning to think 
> that Value should be called "Variable" to make this clearer...
> 

I just have a tendency to like to enable flexibility from client code where it 
is possible. I think I have fought with library code that was inflexible one 
too many times :-)

> It's interesting though, after having developed large amounts of code of 
code 
> based on no-shared-data & read-only/write-only pipes with data handoff and 
> not having had any major concurrency issues (despite mixing threads and non 
> threads) switching to a shared data model instantly causes problems. 
> 
> The difference is really stark. One is simple, natural and easy and the 
other 
> is subtle & problematic. I'm not shocked, but find it amusing :-)

I agree. Where possible we use 'pipe and filter' or 'message queue' patterns 
for inter-thread communication. We have a library based around the MASCOT 
design method that uses concepts of 'queues' and 'pools', which has proved 
very powerful for large multi-threaded applications. We have found that the 
best way to avoid thread related problems is to indulge in a little design 
work to explicitly limit all thread communication to a few well thought 
through mechanisms and then police their use rigidly.

Regards

Richard


-- 
QinetiQ                                  
B009 Woodward Building
St. Andrews Road
Malvern
Worcs WR14 3PS
Jabber: RichardTaylor at jabber.org
PGPKey: http://pgp.mit.edu:11371/pks/lookup?op=get&search=0xA7DA9FD9
Key fingerprint = D051 A121 E7C3 485F 3C0E  1593 ED9E D868 A7DA 9FD9
-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 189 bytes
Desc: This is a digitally signed message part.
Url : http://mail.python.org/pipermail/python-uk/attachments/20071212/949a3f7c/attachment-0001.pgp 


More information about the python-uk mailing list