3

I've come accross functionality which required the following pattern:

from threading import Lock

the_list = []
the_list_lock = Lock()

and to use it:

with the_list_lock:
     the_list.append("New Element")

Unfortunately, this does not require me to acquire the lock, I could just access the object directly. I would like some protection against that (I'm only human.) Is there a standard way of doing this? My own approach is to create a HidingLock class that can be used like this:

the_list = HidingLock([])
with the_list as l:
    l.append("New Element")

But it feels so basic that either it should exist in the standard library or it's a very unconventional way to use locks.

Georg Schölly
  • 124,188
  • 49
  • 220
  • 267
  • Meaning requiring that a lock is acquired before something is accessed? Not really, what you have is the expected solution, make an interface (ie, a class) for it. Instead of accessing the list directly you go via the class interface. – danny Aug 16 '16 at 15:30
  • How far do you want to protect access? What if the object is passed to another thread, or is continued to be accessed after the with block ends? – Dunes Aug 16 '16 at 15:34
  • @Dunes: Just against accidentally forgetting to lock. I don't think it's possible to do more than that in Python. – Georg Schölly Aug 16 '16 at 15:34

3 Answers3

8

I think the reason there's nothing in the standard library is because for it to be there it would need to make cast iron access guarantees. To provide anything less would give a false sense of security that could lead to just as many concurrency issues.

It's also nearly impossible to make these guarantees, without making substantial performance sacrifices. As such, it is left up to the user to consider how they will manage concurrency issues. This is in line with one of Python's the philosophies of "we're all consenting adults". That is, if you're writing a class I think it's reasonable that you should know which attributes you need to acquire a lock before accessing the attribute. Or, if you're really that concerned, write a wrapper/proxy class that controls all access to the underlying object.

With your example there are a number of ways in which the target object could accidentally escape. If the programmer isn't paying enough attention to the code they're writing/maintaining, then this HiddenLock could provide that false sense of security. For instance:

with the_lock as obj:
    pass
obj.func() # erroneous

with the_lock as obj:
    return obj.func() # possibly erroneous
# What if the return value of `func' contains a self reference?

with the_lock as obj:
    obj_copy = obj[:]

obj_copy[0] = 2 # erroneous?

This last one is particularly pernicious. Whether this code is thread safe depends not on the code within the with block, or even the code after the block. Instead, it is the implementation of the class of obj that will mean this code is thread safe or not. For instance, if obj is a list then this is safe as obj[:] creates a copy. However, if obj is a numpy.ndarray then obj[:] creates a view and so the operation is unsafe.

Actually, if the contents of obj were mutable then this could be unsafe as regardless (eg. obj_copy[0].mutate()).

Dunes
  • 37,291
  • 7
  • 81
  • 97
  • I understand your argument, but it's still much easier to shoot yourself in the foot having only locks. Some of the problems you mention could even be fixed by exposing a proxy object instead of the real object. – Georg Schölly Aug 17 '16 at 08:22
1

My current solution (the one I talk about in the question) looks like this:

import threading

class HidingLock(object):
    def __init__(self, obj, lock=None):
        self.lock = lock or threading.RLock()
        self._obj = obj

    def __enter__(self):
        self.lock.acquire()
        return self._obj

    def __exit__(self, exc_type, exc_value, traceback):
        self.lock.release()

    def set(self, obj):
        with self:
            self._obj = obj

and here's how one would use it:

locked_list = HidingLock(["A"])
with locked_list as l:
    l.append("B")
Georg Schölly
  • 124,188
  • 49
  • 220
  • 267
  • in 'set' what is the use of the if condition. Considering Thread1 has acquired the lock and Thread2 called set without acquiring, the if condition would still be true and Thread2 will be allowed to change _obj. – Akilesh Aug 16 '16 at 16:03
  • I have edited the code to use reentrant lock. The various thread can now just call set and only one thread that has acquired the lock shall be allowed to change _obj – Akilesh Aug 16 '16 at 16:06
  • @Akilesh: I thought about that too. However, *if* every `HidingLock.set` usage is wrapped inside a `with` block, using a `RLock` is unnecessary because when invoking `set()` the lock is acquired. – Georg Schölly Aug 16 '16 at 21:43
0

What about creating a shared_list that has a list and implements the desired class methods using a threading.Lock:

import threading

class SharedList(object):
    def __init__(self, iterable=None):
        if iterable is not None:
            self.list = list(iterable)
        else:
            self.list = list()
        self.lock = threading.Lock()
        self.index = None

    def append(self, x):
        with self.lock:
            self.list.append(x)

    def __iter__(self):
        shared_iterator = SharedList()
        shared_iterator.list = self.list
        shared_iterator.lock = self.lock
        shared_iterator.index = 0
        return shared_iterator

    def next(self):
        with self.lock:
            if self.index < len(self.list):
                result = self.list[self.index]
                self.index += 1
            else:
                raise StopIteration
            return result

    # Override other methods

if __name__ == '__main__':
    shared_list = SharedList()
    for x in range(1, 4):
        shared_list.append(x)
    for entry in shared_list:
        print entry

Output

1
2
3

As Georg Shölly pointed out in the comments, this would require a lot of work to implement every method. However, if all you need is a list you can append to and then iterate over, this example provides the starting point.

Then you can just write

the_list = SharedList()
the_list.append("New Element")
Community
  • 1
  • 1
  • It's incredibly difficult to override all attributes I would say. And even if we could, methods like `__iter__()` are not trivial to make thread-safe. – Georg Schölly Aug 17 '16 at 07:09