Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

threadpool.py 10.0KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. # -*- test-case-name: twisted.test.test_threadpool -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. twisted.python.threadpool: a pool of threads to which we dispatch tasks.
  6. In most cases you can just use C{reactor.callInThread} and friends
  7. instead of creating a thread pool directly.
  8. """
  9. from threading import Thread, current_thread
  10. from typing import List
  11. from twisted._threads import pool as _pool
  12. from twisted.python import context, log
  13. from twisted.python.deprecate import deprecated
  14. from twisted.python.failure import Failure
  15. from twisted.python.versions import Version
  16. WorkerStop = object()
  17. class ThreadPool:
  18. """
  19. This class (hopefully) generalizes the functionality of a pool of threads
  20. to which work can be dispatched.
  21. L{callInThread} and L{stop} should only be called from a single thread.
  22. @ivar started: Whether or not the thread pool is currently running.
  23. @type started: L{bool}
  24. @ivar threads: List of workers currently running in this thread pool.
  25. @type threads: L{list}
  26. @ivar _pool: A hook for testing.
  27. @type _pool: callable compatible with L{_pool}
  28. """
  29. min = 5
  30. max = 20
  31. joined = False
  32. started = False
  33. name = None
  34. threadFactory = Thread
  35. currentThread = staticmethod(
  36. deprecated(
  37. version=Version("Twisted", 22, 1, 0),
  38. replacement="threading.current_thread",
  39. )(current_thread)
  40. )
  41. _pool = staticmethod(_pool)
  42. def __init__(self, minthreads=5, maxthreads=20, name=None):
  43. """
  44. Create a new threadpool.
  45. @param minthreads: minimum number of threads in the pool
  46. @type minthreads: L{int}
  47. @param maxthreads: maximum number of threads in the pool
  48. @type maxthreads: L{int}
  49. @param name: The name to give this threadpool; visible in log messages.
  50. @type name: native L{str}
  51. """
  52. assert minthreads >= 0, "minimum is negative"
  53. assert minthreads <= maxthreads, "minimum is greater than maximum"
  54. self.min = minthreads
  55. self.max = maxthreads
  56. self.name = name
  57. self.threads: List[Thread] = []
  58. def trackingThreadFactory(*a, **kw):
  59. thread = self.threadFactory( # type: ignore[misc]
  60. *a, name=self._generateName(), **kw
  61. )
  62. self.threads.append(thread)
  63. return thread
  64. def currentLimit():
  65. if not self.started:
  66. return 0
  67. return self.max
  68. self._team = self._pool(currentLimit, trackingThreadFactory)
  69. @property
  70. def workers(self):
  71. """
  72. For legacy compatibility purposes, return a total number of workers.
  73. @return: the current number of workers, both idle and busy (but not
  74. those that have been quit by L{ThreadPool.adjustPoolsize})
  75. @rtype: L{int}
  76. """
  77. stats = self._team.statistics()
  78. return stats.idleWorkerCount + stats.busyWorkerCount
  79. @property
  80. def working(self):
  81. """
  82. For legacy compatibility purposes, return the number of busy workers as
  83. expressed by a list the length of that number.
  84. @return: the number of workers currently processing a work item.
  85. @rtype: L{list} of L{None}
  86. """
  87. return [None] * self._team.statistics().busyWorkerCount
  88. @property
  89. def waiters(self):
  90. """
  91. For legacy compatibility purposes, return the number of idle workers as
  92. expressed by a list the length of that number.
  93. @return: the number of workers currently alive (with an allocated
  94. thread) but waiting for new work.
  95. @rtype: L{list} of L{None}
  96. """
  97. return [None] * self._team.statistics().idleWorkerCount
  98. @property
  99. def _queue(self):
  100. """
  101. For legacy compatibility purposes, return an object with a C{qsize}
  102. method that indicates the amount of work not yet allocated to a worker.
  103. @return: an object with a C{qsize} method.
  104. """
  105. class NotAQueue:
  106. def qsize(q):
  107. """
  108. Pretend to be a Python threading Queue and return the
  109. number of as-yet-unconsumed tasks.
  110. @return: the amount of backlogged work not yet dispatched to a
  111. worker.
  112. @rtype: L{int}
  113. """
  114. return self._team.statistics().backloggedWorkCount
  115. return NotAQueue()
  116. q = _queue # Yes, twistedchecker, I want a single-letter
  117. # attribute name.
  118. def start(self):
  119. """
  120. Start the threadpool.
  121. """
  122. self.joined = False
  123. self.started = True
  124. # Start some threads.
  125. self.adjustPoolsize()
  126. backlog = self._team.statistics().backloggedWorkCount
  127. if backlog:
  128. self._team.grow(backlog)
  129. def startAWorker(self):
  130. """
  131. Increase the number of available workers for the thread pool by 1, up
  132. to the maximum allowed by L{ThreadPool.max}.
  133. """
  134. self._team.grow(1)
  135. def _generateName(self):
  136. """
  137. Generate a name for a new pool thread.
  138. @return: A distinctive name for the thread.
  139. @rtype: native L{str}
  140. """
  141. return f"PoolThread-{self.name or id(self)}-{self.workers}"
  142. def stopAWorker(self):
  143. """
  144. Decrease the number of available workers by 1, by quitting one as soon
  145. as it's idle.
  146. """
  147. self._team.shrink(1)
  148. def __setstate__(self, state):
  149. setattr(self, "__dict__", state)
  150. ThreadPool.__init__(self, self.min, self.max)
  151. def __getstate__(self):
  152. state = {}
  153. state["min"] = self.min
  154. state["max"] = self.max
  155. return state
  156. def callInThread(self, func, *args, **kw):
  157. """
  158. Call a callable object in a separate thread.
  159. @param func: callable object to be called in separate thread
  160. @param args: positional arguments to be passed to C{func}
  161. @param kw: keyword args to be passed to C{func}
  162. """
  163. self.callInThreadWithCallback(None, func, *args, **kw)
  164. def callInThreadWithCallback(self, onResult, func, *args, **kw):
  165. """
  166. Call a callable object in a separate thread and call C{onResult} with
  167. the return value, or a L{twisted.python.failure.Failure} if the
  168. callable raises an exception.
  169. The callable is allowed to block, but the C{onResult} function must not
  170. block and should perform as little work as possible.
  171. A typical action for C{onResult} for a threadpool used with a Twisted
  172. reactor would be to schedule a L{twisted.internet.defer.Deferred} to
  173. fire in the main reactor thread using C{.callFromThread}. Note that
  174. C{onResult} is called inside the separate thread, not inside the
  175. reactor thread.
  176. @param onResult: a callable with the signature C{(success, result)}.
  177. If the callable returns normally, C{onResult} is called with
  178. C{(True, result)} where C{result} is the return value of the
  179. callable. If the callable throws an exception, C{onResult} is
  180. called with C{(False, failure)}.
  181. Optionally, C{onResult} may be L{None}, in which case it is not
  182. called at all.
  183. @param func: callable object to be called in separate thread
  184. @param args: positional arguments to be passed to C{func}
  185. @param kw: keyword arguments to be passed to C{func}
  186. """
  187. if self.joined:
  188. return
  189. ctx = context.theContextTracker.currentContext().contexts[-1]
  190. def inContext():
  191. try:
  192. result = inContext.theWork() # type: ignore[attr-defined]
  193. ok = True
  194. except BaseException:
  195. result = Failure()
  196. ok = False
  197. inContext.theWork = None # type: ignore[attr-defined]
  198. if inContext.onResult is not None: # type: ignore[attr-defined]
  199. inContext.onResult(ok, result) # type: ignore[attr-defined]
  200. inContext.onResult = None # type: ignore[attr-defined]
  201. elif not ok:
  202. log.err(result)
  203. # Avoid closing over func, ctx, args, kw so that we can carefully
  204. # manage their lifecycle. See
  205. # test_threadCreationArgumentsCallInThreadWithCallback.
  206. inContext.theWork = lambda: context.call( # type: ignore[attr-defined]
  207. ctx, func, *args, **kw
  208. )
  209. inContext.onResult = onResult # type: ignore[attr-defined]
  210. self._team.do(inContext)
  211. def stop(self):
  212. """
  213. Shutdown the threads in the threadpool.
  214. """
  215. self.joined = True
  216. self.started = False
  217. self._team.quit()
  218. for thread in self.threads:
  219. thread.join()
  220. def adjustPoolsize(self, minthreads=None, maxthreads=None):
  221. """
  222. Adjust the number of available threads by setting C{min} and C{max} to
  223. new values.
  224. @param minthreads: The new value for L{ThreadPool.min}.
  225. @param maxthreads: The new value for L{ThreadPool.max}.
  226. """
  227. if minthreads is None:
  228. minthreads = self.min
  229. if maxthreads is None:
  230. maxthreads = self.max
  231. assert minthreads >= 0, "minimum is negative"
  232. assert minthreads <= maxthreads, "minimum is greater than maximum"
  233. self.min = minthreads
  234. self.max = maxthreads
  235. if not self.started:
  236. return
  237. # Kill of some threads if we have too many.
  238. if self.workers > self.max:
  239. self._team.shrink(self.workers - self.max)
  240. # Start some threads if we have too few.
  241. if self.workers < self.min:
  242. self._team.grow(self.min - self.workers)
  243. def dumpStats(self):
  244. """
  245. Dump some plain-text informational messages to the log about the state
  246. of this L{ThreadPool}.
  247. """
  248. log.msg(f"waiters: {self.waiters}")
  249. log.msg(f"workers: {self.working}")
  250. log.msg(f"total: {self.threads}")