direct.stdpy.threading.Lock behaves like RLock instead

direct.stdpy.threading defines both Lock and RLock. I was surprised to find that a direct.stdpy.threading.Lock() behaves like RLock, not Lock. I.e. in a standard Python program:

from threading import Lock
test = Lock()
print(f"Lock created. About to acquire")
print(f"test.acquire() -> {test.acquire()}")
print(f"lock acquired. About to acquire again")
print(f"test.acquire(True)={test.acquire(True)}")

produces the following output and correctly deadlocks forever on its attempt to acquire a locked lock:
Lock created. About to acquire
test.acquire() → True
lock acquired. About to acquire again

However, within a Panda 1.10.9 test program,

from direct.task import Task
from direct.showbase.ShowBase import ShowBase, taskMgr
from direct.stdpy.threading import Lock


class Appl(ShowBase):
    def __init__(self):
        super().__init__()
        # Chan.initialize()
        taskMgr.add(self.main, 'main')

    async def main(self, task: Task):
        test = Lock()
        print(f"Lock created. About to acquire")
        print(f"test.acquire() -> {test.acquire()}")
        print(f"lock acquired. About to acquire again")
        print(f"test.acquire(True)={test.acquire(True)}")
        return task.done


ap = Appl()
ap.run()

the lock acquisition succeeds in acquiring a locked lock, with the following output.
Known pipe types:
wglGraphicsPipe
(all display modules loaded.)
Lock created. About to acquire
test.acquire() → True
lock acquired. About to acquire again
test.acquire(True)=True

direct.stdpy.threading2.Lock() behaves properly. I don’t know if direct.stdpy.threading.Lock() is intended to behave like RLock. If so, maybe update the documentation to warn the next person. Obviously, one would not try to acquire a lock twice in succession in code. The above test program is a simplification of the actual problem I ran into when I successfully acquired the lock in one task, then to my surprise successfully acquired the already locked lock in another task on the same (main) taskChain.

You should think of direct.stdpy.threading.RLock as a lock that is guaranteed to be re-entrant, and direct.stdpy.threading.Lock as one that may or may not be re-entrant.

Guaranteeing a deadlock would be possible with additional code, but that would reduce efficiency, and the whole point of the direct.stdpy.threading module is that it directly maps to the underlying structures, whereas direct.stdpy.threading2 exists for people who need the semantics to precisely match that of Python.

In Panda3D 1.10, Panda uses Windows’ CRITICAL_SECTION to implement mutex locks. These are always re-entrant. Windows XP, which Panda3D 1.10 still supports, does not offer an efficient non-reentrant mutex.

Panda3D 1.11 instead switches to the more efficient SRWLock that was introduced with Windows Vista. It is able to so because Panda3D 1.11 drops support for Windows XP. This lock is not re-entrant, so in Panda3D 1.11 it will no longer behave this way. RLock still uses the older CRITICAL_SECTION.

Thanks.
I’m trying to coordinate between async tasks and the polling oriented HTTPChannel service. In some situations, such as SC_timeout or a complaint from the server that it is overloaded, I need to wait a few seconds before proceeding. panda3d.core.Thread.sleep(seconds) seems like an appropriate way to sleep a task without loosing the context within its defining function. However. the documentation says that sleep
“Suspends the current thread for at least the indicated amount of time. It might be suspended for longer”. I want to suspend the task, but not the running thread that is serving it and other tasks. Does …Thread.sleep() suspend the task, or the entire thread?

Thanks for your help. I’m no longer considering Thread.sleep() or threading2.Lock. I changed approach to using await taskMgr.doMethodLater() for sleeping and AsyncFuture() for coordination between async-oriented higher layer and a low level polling for HTTPChanel completions. Problem solved and proof of concept working fine.

Thread.sleep suspends the thread, not the task (it’s like time.sleep() in that sense, rather than asyncio.sleep()). It’s not really a great idea to use it with asynchronous tasks.

For future reference, you can use await Task.pause(123) to suspend only the current task. But there are usually better alternatives, such as coordinating with AsyncFuture, as you have found.

We really do ought to add an async interface to HTTPChannel.