Script below is abstracted. My question is about the use of threading.Lock()
Locking limits access to "shared resources" but I am nervous about how far that goes. I have objects attributes that are lists of objects which have attributes that are arrays in this example. In some cases the dependency will go farther.
Does Lock()
"know" for sure about everything that needs to be locked?
The output of the script below is also shown. The purpose of the script is mostly for discussion - It doesn't fail, but I am not confident that it is Locking everything it needs to.
start: [array([0, 1]), array([0, 1, 2]), array([0, 1, 2, 3])]
append an object
done!
finish: [array([505, 605]), array([10, 11, 12]), array([10, 11, 12, 13]), array([5])]
import time
from threading import Thread, Lock
import numpy as np
class Bucket(object):
def __init__(self, objects):
self.objects = objects
class Object(object):
def __init__(self, array):
self.array = array
class A(Thread):
def __init__(self, bucket):
Thread.__init__(self)
self.bucket = bucket
def run(self):
nloop = 0
locker = Lock()
n = 0
while n < 10:
with locker:
objects = self.bucket.objects[:] # makes a local copy of list each time
for i, obj in enumerate(objects):
with locker:
obj.array += 1
time.sleep(0.2)
n += 1
print 'n: ', n
print "done!"
return
objects = []
for i in range(3):
ob = Object(np.arange(i+2))
objects.append(ob)
bucket = Bucket(objects)
locker = Lock()
a = A(bucket)
print [o.array for o in bucket.objects]
a.start()
time.sleep(3)
with locker:
bucket.objects.append(Object(np.arange(1))) # abuse the bucket!
print 'append an object'
time.sleep(5)
print [o.array for o in bucket.objects]
A lock can be locked using the acquire() method. Once a thread has acquired the lock, all subsequent attempts to acquire the lock are blocked until it is released. The lock can be released using the release() method.
In fact, a Python process cannot run threads in parallel but it can run them concurrently through context switching during I/O bound operations. This limitation is actually enforced by GIL. The Python Global Interpreter Lock (GIL) prevents threads within the same process to be executed at the same time.
The main difference is that a Lock can only be acquired once. It cannot be acquired again, until it is released. (After it's been released, it can be re-acaquired by any thread). An RLock on the other hand, can be acquired multiple times, by the same thread.
At any moment, yes, only one thread is executing Python code (other threads may be executing some IO, NumPy, whatever). That is mostly true. However, this is trivially true on any single-processor system, and yet people still need locks on single-processor systems.
Locking is a synchronization mechanism for threads. Once a thread acquires a lock, no other thread can access the shared resource until and unless it releases it. It helps to avoid the race condition. What is a lock object in Python?
Once thread switching is ordered, access and modification of data between threads becomes controlled, so to ensure thread safety, locks must be used. The threading module provides the five most common types of locks, which are divided by function as follows.
This lock helps us in the synchronization of two or more threads. Lock class perhaps provides the simplest synchronization primitive in Python. Primitive lock can have two States: locked or unlocked and is initially created in unlocked state when we initialize the Lock object.
Lock class perhaps provides the simplest synchronization primitive in Python. Primitive lock can have two States: locked or unlocked and is initially created in unlocked state when we initialize the Lock object. It has two basic methods, acquire () and release ().
you seem to misunderstand how a lock works.
a lock doesn't lock any objects, it can just lock the thread execution.
The first thread which tries to enter a with locker:
block succeeds.
If another thread tries to enter a with locker:
block (with the same locker
object), it's delayed until the first thread exits the block, so both threads cannot change the value of the variable inside the block at the same time.
Here your "shared resources" are the variables you're changing in your blocks: as I can see, objects
and obj.array
. You're basically protecting them from concurrent access (that is - in a python version where there isn't a GIL for starters) just because only one thread can change them at a time
Old-timers call that a critical section, where only 1 thread can execute at a time.
Note that it's slightly dubious to use the same locker
object for different resources. This has more chance to deadlock / be slower that it needs to be.
(and if you nest 2 with locker
calls you get a deadlock - you need an RLock if you want to do that)
That's as simple as that.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With