threading.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. """ This module reimplements Python's native threading module using Panda
  2. threading constructs. It's designed as a drop-in replacement for the
  3. threading module for code that works with Panda; it is necessary because
  4. in some compilation models, Panda's threading constructs are
  5. incompatible with the OS-provided threads used by Python's thread
  6. module.
  7. This module implements the threading module with a thin layer over
  8. Panda's threading constructs. As such, the semantics are close to,
  9. but not precisely, the semantics documented for Python's standard
  10. threading module. If you really do require strict adherence to
  11. Python's semantics, see the threading2 module instead.
  12. However, if you don't need such strict adherence to Python's original
  13. semantics, this module is probably a better choice. It is likely to
  14. be slighly faster than the threading2 module (and even slightly faster
  15. than Python's own threading module). It is also better integrated
  16. with Panda's threads, so that Panda's thread debug mechanisms will be
  17. easier to use and understand.
  18. It is permissible to mix-and-match both threading and threading2
  19. within the same application. """
  20. from __future__ import annotations
  21. from panda3d import core
  22. from direct.stdpy import thread as _thread
  23. import sys as _sys
  24. import weakref
  25. from collections.abc import Callable, Iterable, Mapping
  26. from typing import Any, NoReturn
  27. __all__ = [
  28. 'Thread',
  29. 'Lock', 'RLock',
  30. 'Condition',
  31. 'Semaphore', 'BoundedSemaphore',
  32. 'Event',
  33. 'Timer',
  34. 'ThreadError',
  35. 'local',
  36. 'current_thread',
  37. 'main_thread',
  38. 'enumerate', 'active_count',
  39. 'settrace', 'setprofile', 'stack_size',
  40. 'TIMEOUT_MAX',
  41. ]
  42. TIMEOUT_MAX = _thread.TIMEOUT_MAX
  43. local = _thread._local
  44. _newname = _thread._newname
  45. ThreadError = _thread.error
  46. class ThreadBase:
  47. """ A base class for both Thread and ExternalThread in this
  48. module. """
  49. name: str
  50. ident: int
  51. daemon: bool
  52. def __init__(self) -> None:
  53. pass
  54. def getName(self) -> str:
  55. return self.name
  56. def isDaemon(self):
  57. return self.daemon
  58. def setDaemon(self, daemon):
  59. if self.is_alive():
  60. raise RuntimeError
  61. self.__dict__['daemon'] = daemon
  62. def __setattr__(self, key, value):
  63. if key == 'name':
  64. self.setName(value)
  65. elif key == 'ident':
  66. raise AttributeError
  67. elif key == 'daemon':
  68. self.setDaemon(value)
  69. else:
  70. self.__dict__[key] = value
  71. # Copy these static methods from Panda's Thread object. These are
  72. # useful if you may be running in Panda's SIMPLE_THREADS compilation
  73. # mode.
  74. ThreadBase.forceYield = core.Thread.forceYield # type: ignore[attr-defined]
  75. ThreadBase.considerYield = core.Thread.considerYield # type: ignore[attr-defined]
  76. class Thread(ThreadBase):
  77. """ This class provides a wrapper around Panda's PythonThread
  78. object. The wrapper is designed to emulate Python's own
  79. threading.Thread object. """
  80. def __init__(
  81. self,
  82. group: None = None,
  83. target: Callable[..., object] | None = None,
  84. name: str | None = None,
  85. args: Iterable[Any] = (),
  86. kwargs: Mapping[str, Any] = {},
  87. daemon: bool | None = None,
  88. ) -> None:
  89. ThreadBase.__init__(self)
  90. assert group is None
  91. self.__target = target
  92. self.__args = args
  93. self.__kwargs = kwargs
  94. if not name:
  95. name = _newname()
  96. current = current_thread()
  97. if daemon is not None:
  98. self.__dict__['daemon'] = daemon
  99. else:
  100. self.__dict__['daemon'] = current.daemon
  101. self.__dict__['name'] = name
  102. def call_run():
  103. # As soon as the thread is done, break the circular reference.
  104. try:
  105. self.run()
  106. finally:
  107. self.__thread = None
  108. _thread._remove_thread_id(self.ident)
  109. self.__thread = core.PythonThread(call_run, None, name, name)
  110. threadId = _thread._add_thread(self.__thread, weakref.proxy(self))
  111. self.__dict__['ident'] = threadId
  112. def __del__(self):
  113. _thread._remove_thread_id(self.ident)
  114. def is_alive(self):
  115. thread = self.__thread
  116. return thread is not None and thread.is_started()
  117. isAlive = is_alive
  118. def start(self) -> None:
  119. thread = self.__thread
  120. if thread is None or thread.is_started():
  121. raise RuntimeError
  122. if not thread.start(core.TPNormal, True):
  123. raise RuntimeError
  124. def run(self):
  125. if _settrace_func:
  126. _sys.settrace(_settrace_func)
  127. if _setprofile_func:
  128. _sys.setprofile(_setprofile_func)
  129. self.__target(*self.__args, **self.__kwargs)
  130. def join(self, timeout: float | None = None) -> None:
  131. # We don't support a timed join here, sorry.
  132. assert timeout is None
  133. thread = self.__thread
  134. if thread is not None:
  135. thread.join()
  136. # Clear the circular reference.
  137. self.__thread = None
  138. _thread._remove_thread_id(self.ident)
  139. def setName(self, name: str) -> None:
  140. self.__dict__['name'] = name
  141. self.__thread.setName(name)
  142. class ExternalThread(ThreadBase):
  143. """ Returned for a Thread object that wasn't created by this
  144. interface. """
  145. def __init__(self, extThread: core.Thread, threadId: int) -> None:
  146. ThreadBase.__init__(self)
  147. self.__thread = extThread
  148. self.__dict__['daemon'] = True
  149. self.__dict__['name'] = self.__thread.getName()
  150. self.__dict__['ident'] = threadId
  151. def is_alive(self):
  152. return self.__thread.isStarted()
  153. def isAlive(self):
  154. return self.__thread.isStarted()
  155. def start(self):
  156. raise RuntimeError
  157. def run(self):
  158. raise RuntimeError
  159. def join(self, timeout = None):
  160. raise RuntimeError
  161. def setDaemon(self, daemon):
  162. raise RuntimeError
  163. class MainThread(ExternalThread):
  164. """ Returned for the MainThread object. """
  165. def __init__(self, extThread: core.Thread, threadId: int) -> None:
  166. ExternalThread.__init__(self, extThread, threadId)
  167. self.__dict__['daemon'] = False
  168. class Lock(core.Mutex):
  169. """ This class provides a wrapper around Panda's Mutex object.
  170. The wrapper is designed to emulate Python's own threading.Lock
  171. object. """
  172. def __init__(self, name: str = "PythonLock") -> None:
  173. core.Mutex.__init__(self, name)
  174. class RLock(core.ReMutex):
  175. """ This class provides a wrapper around Panda's ReMutex object.
  176. The wrapper is designed to emulate Python's own threading.RLock
  177. object. """
  178. def __init__(self, name = "PythonRLock"):
  179. core.ReMutex.__init__(self, name)
  180. class Condition(core.ConditionVar):
  181. """ This class provides a wrapper around Panda's ConditionVar
  182. object. The wrapper is designed to emulate Python's own
  183. threading.Condition object. """
  184. def __init__(self, lock: Lock | RLock | None = None) -> None:
  185. if not lock:
  186. lock = Lock()
  187. # Panda doesn't support RLock objects used with condition
  188. # variables.
  189. assert isinstance(lock, Lock)
  190. self.__lock = lock
  191. core.ConditionVar.__init__(self, self.__lock)
  192. def acquire(self, *args, **kw):
  193. return self.__lock.acquire(*args, **kw)
  194. def release(self):
  195. self.__lock.release()
  196. def wait(self, timeout: float | None = None) -> None:
  197. if timeout is None:
  198. core.ConditionVar.wait(self)
  199. else:
  200. core.ConditionVar.wait(self, timeout)
  201. def notifyAll(self):
  202. core.ConditionVar.notifyAll(self)
  203. notify_all = notifyAll
  204. __enter__ = acquire
  205. def __exit__(self, t, v, tb):
  206. self.release()
  207. class Semaphore(core.Semaphore):
  208. """ This class provides a wrapper around Panda's Semaphore
  209. object. The wrapper is designed to emulate Python's own
  210. threading.Semaphore object. """
  211. def __init__(self, value = 1):
  212. core.Semaphore.__init__(self, value)
  213. def acquire(self, blocking = True):
  214. if blocking:
  215. core.Semaphore.acquire(self)
  216. return True
  217. else:
  218. return core.Semaphore.tryAcquire(self)
  219. __enter__ = acquire
  220. def __exit__(self, t, v, tb):
  221. self.release()
  222. class BoundedSemaphore(Semaphore):
  223. """ This class provides a wrapper around Panda's Semaphore
  224. object. The wrapper is designed to emulate Python's own
  225. threading.BoundedSemaphore object. """
  226. def __init__(self, value = 1):
  227. self.__max = value
  228. Semaphore.__init__(value)
  229. def release(self):
  230. if self.getCount() > self.__max:
  231. raise ValueError
  232. Semaphore.release(self)
  233. class Event:
  234. """ This class is designed to emulate Python's own threading.Event
  235. object. """
  236. def __init__(self):
  237. self.__lock = core.Mutex("Python Event")
  238. self.__cvar = core.ConditionVar(self.__lock)
  239. self.__flag = False
  240. def is_set(self):
  241. return self.__flag
  242. isSet = is_set
  243. def set(self):
  244. self.__lock.acquire()
  245. try:
  246. self.__flag = True
  247. self.__cvar.notifyAll()
  248. finally:
  249. self.__lock.release()
  250. def clear(self):
  251. self.__lock.acquire()
  252. try:
  253. self.__flag = False
  254. finally:
  255. self.__lock.release()
  256. def wait(self, timeout = None):
  257. self.__lock.acquire()
  258. try:
  259. if timeout is None:
  260. while not self.__flag:
  261. self.__cvar.wait()
  262. else:
  263. clock = core.TrueClock.getGlobalPtr()
  264. expires = clock.getShortTime() + timeout
  265. while not self.__flag:
  266. wait = expires - clock.getShortTime()
  267. if wait < 0:
  268. return
  269. self.__cvar.wait(wait)
  270. finally:
  271. self.__lock.release()
  272. class Timer(Thread):
  273. """Call a function after a specified number of seconds:
  274. t = Timer(30.0, f, args=[], kwargs={})
  275. t.start()
  276. t.cancel() # stop the timer's action if it's still waiting
  277. """
  278. def __init__(self, interval, function, args=[], kwargs={}):
  279. Thread.__init__(self)
  280. self.interval = interval
  281. self.function = function
  282. self.args = args
  283. self.kwargs = kwargs
  284. self.finished = Event()
  285. def cancel(self):
  286. """Stop the timer if it hasn't finished yet"""
  287. self.finished.set()
  288. def run(self):
  289. self.finished.wait(self.interval)
  290. if not self.finished.isSet():
  291. self.function(*self.args, **self.kwargs)
  292. self.finished.set()
  293. def _create_thread_wrapper(t: core.Thread, threadId: int) -> ExternalThread:
  294. """ Creates a thread wrapper for the indicated external thread. """
  295. pyt: ExternalThread
  296. if isinstance(t, core.MainThread):
  297. pyt = MainThread(t, threadId)
  298. else:
  299. pyt = ExternalThread(t, threadId)
  300. return pyt
  301. def current_thread() -> ThreadBase:
  302. t = core.Thread.getCurrentThread()
  303. return _thread._get_thread_wrapper(t, _create_thread_wrapper)
  304. def main_thread():
  305. t = core.Thread.getMainThread()
  306. return _thread._get_thread_wrapper(t, _create_thread_wrapper)
  307. currentThread = current_thread
  308. def enumerate():
  309. tlist = []
  310. _thread._threadsLock.acquire()
  311. try:
  312. for thread, locals, wrapper in list(_thread._threads.values()):
  313. if wrapper and wrapper.is_alive():
  314. tlist.append(wrapper)
  315. return tlist
  316. finally:
  317. _thread._threadsLock.release()
  318. def active_count():
  319. return len(enumerate())
  320. activeCount = active_count
  321. _settrace_func = None
  322. def settrace(func):
  323. global _settrace_func
  324. _settrace_func = func
  325. _setprofile_func = None
  326. def setprofile(func):
  327. global _setprofile_func
  328. _setprofile_func = func
  329. def stack_size(size: object = None) -> NoReturn:
  330. raise ThreadError