Skip to content

Question: is the thread_local=False works only when use the same Lock instance? #3540

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
shenxiangzhuang opened this issue Mar 4, 2025 · 2 comments

Comments

@shenxiangzhuang
Copy link
Contributor

shenxiangzhuang commented Mar 4, 2025

Question: is the thread_local=False works only when use the same Lock instance?
(Here the 'work' means that in thread1 release a lock owned by thread2.)

I'm trying to reproduce the behavior here in the doc:

redis-py/redis/lock.py

Lines 115 to 126 in 2fb2f47

time: 0, thread-1 acquires `my-lock`, with a timeout of 5 seconds.
thread-1 sets the token to "abc"
time: 1, thread-2 blocks trying to acquire `my-lock` using the
Lock instance.
time: 5, thread-1 has not yet completed. redis expires the lock
key.
time: 5, thread-2 acquired `my-lock` now that it's available.
thread-2 sets the token to "xyz"
time: 6, thread-1 finishes its work and calls release(). if the
token is *not* stored in thread local storage, then
thread-1 would see the token value as "xyz" and would be
able to successfully release the thread-2's lock.

Here is my first try(failed):

import redis
import threading
import time


r = redis.Redis()
lock_name = "lock:thread_local:example"


def thread1_function():
    print("Thread 1: Starting")
    lock = r.lock(lock_name, timeout=5, thread_local=False)
    if lock.acquire():
        print(f"Thread 1: Lock token: {lock.local.token}")
        time.sleep(7)
    print(f"Thread 1: Lock token: {lock.local.token}")
    try:
        lock.release()
    except Exception as e:
        print(f"Thread 1: {e}")


def thread2_function():
    print("Thread 2: Starting")
    lock = r.lock(lock_name, thread_local=False)
    lock.acquire()
    print(f"Thread 2: Lock token: {lock.local.token}")
    time.sleep(10)


# Create and start threads
t1 = threading.Thread(target=thread1_function)
t2 = threading.Thread(target=thread2_function)

t1.start()
time.sleep(1)
t2.start()

# Wait for threads to complete
t1.join()
t2.join()

# clean up
r.delete(lock_name)

I got the output:

Thread 1: Starting
Thread 1: Lock token: b'a1431dd2f8e111efb50f2fe1b06b7cb0'
Thread 2: Starting
Thread 2: Lock token: b'a1dbe1b6f8e111efb50f2fe1b06b7cb0'
Thread 1: Lock token: b'a1431dd2f8e111efb50f2fe1b06b7cb0'
Thread 1: Cannot release a lock that's no longer owned

So it seems that I can not release the lock from Thread1 and the reason from error message is clear.

Then, I try to use a global Lock instance, and it works:

import redis
import threading
import time


r = redis.Redis()
lock_name = "lock:thread_local:example"
lock = r.lock(lock_name, timeout=5, thread_local=False)


def thread1_function():
    print("Thread 1: Starting")
    if lock.acquire():
        print(f"Thread 1: Lock token: {lock.local.token}")
        time.sleep(7)
    print(f"Thread 1: Lock token: {lock.local.token}")
    lock.release()
    print("Thread 1: Lock released")


def thread2_function():
    print("Thread 2: Starting")
    print(f"Thread 2: Lock token: {lock.local.token}")
    lock.acquire()
    print(f"Thread 2: Lock token: {lock.local.token}")
    time.sleep(10)


# Create and start threads
t1 = threading.Thread(target=thread1_function)
t2 = threading.Thread(target=thread2_function)

t1.start()
time.sleep(1)
t2.start()

# Wait for threads to complete
t1.join()
t2.join()

# clean up
r.delete(lock_name)

The output:

Thread 1: Starting
Thread 1: Lock token: b'd5aec030f8e111ef8dd2cd2710b884db'
Thread 2: Starting
Thread 2: Lock token: b'd5aec030f8e111ef8dd2cd2710b884db'
Thread 2: Lock token: b'd64788ecf8e111ef8dd2cd2710b884db'
Thread 1: Lock token: b'd64788ecf8e111ef8dd2cd2710b884db'
Thread 1: Lock released

So I have the question at begin. I'm not sure the statement is accurate or not, any help will be appreciated!

@petyaslavova
Copy link
Collaborator

Hi @shenxiangzhuang,

To share state when thread_local=False, you still need to use the same lock instance.
This is why the behavior in your example is expected.

When you call r.lock(lock_name, timeout=5, thread_local=False) a new lock object is created. The token information is stored in its self.local object, which is defined as: self.local = threading.local() if self.thread_local else SimpleNamespace()

Since thread_local=False, self.local is a SimpleNamespace() instance. This means attributes can be freely shared between threads but only within the same lock instance. If you create a second lock instance, it will have a separate SimpleNamespace() object, and the state will not be shared between the two locks.

Let me know if you need further clarifications.

@shenxiangzhuang
Copy link
Contributor Author

@petyaslavova Thanks a lot! Very clear explanation!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants