_threadedselect.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. # -*- test-case-name: twisted.test.test_internet -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Threaded select reactor
  6. The threadedselectreactor is a specialized reactor for integrating with
  7. arbitrary foreign event loop, such as those you find in GUI toolkits.
  8. There are three things you'll need to do to use this reactor.
  9. Install the reactor at the beginning of your program, before importing
  10. the rest of Twisted::
  11. | from twisted.internet import _threadedselect
  12. | _threadedselect.install()
  13. Interleave this reactor with your foreign event loop, at some point after
  14. your event loop is initialized::
  15. | from twisted.internet import reactor
  16. | reactor.interleave(foreignEventLoopWakerFunction)
  17. | self.addSystemEventTrigger('after', 'shutdown', foreignEventLoopStop)
  18. Instead of shutting down the foreign event loop directly, shut down the
  19. reactor::
  20. | from twisted.internet import reactor
  21. | reactor.stop()
  22. In order for Twisted to do its work in the main thread (the thread that
  23. interleave is called from), a waker function is necessary. The waker function
  24. will be called from a "background" thread with one argument: func.
  25. The waker function's purpose is to call func() from the main thread.
  26. Many GUI toolkits ship with appropriate waker functions.
  27. Some examples of this are wxPython's wx.callAfter (may be wxCallAfter in
  28. older versions of wxPython) or PyObjC's PyObjCTools.AppHelper.callAfter.
  29. These would be used in place of "foreignEventLoopWakerFunction" in the above
  30. example.
  31. The other integration point at which the foreign event loop and this reactor
  32. must integrate is shutdown. In order to ensure clean shutdown of Twisted,
  33. you must allow for Twisted to come to a complete stop before quitting the
  34. application. Typically, you will do this by setting up an after shutdown
  35. trigger to stop your foreign event loop, and call reactor.stop() where you
  36. would normally have initiated the shutdown procedure for the foreign event
  37. loop. Shutdown functions that could be used in place of
  38. "foreignEventloopStop" would be the ExitMainLoop method of the wxApp instance
  39. with wxPython, or the PyObjCTools.AppHelper.stopEventLoop function.
  40. """
  41. import select
  42. import sys
  43. from errno import EBADF, EINTR
  44. from functools import partial
  45. from queue import Empty, Queue
  46. from threading import Thread
  47. from zope.interface import implementer
  48. from twisted.internet import posixbase
  49. from twisted.internet.interfaces import IReactorFDSet
  50. from twisted.internet.posixbase import _NO_FILEDESC, _NO_FILENO
  51. from twisted.internet.selectreactor import _select
  52. from twisted.python import failure, log, threadable
  53. def dictRemove(dct, value):
  54. try:
  55. del dct[value]
  56. except KeyError:
  57. pass
  58. def raiseException(e):
  59. raise e
  60. @implementer(IReactorFDSet)
  61. class ThreadedSelectReactor(posixbase.PosixReactorBase):
  62. """A threaded select() based reactor - runs on all POSIX platforms and on
  63. Win32.
  64. """
  65. def __init__(self):
  66. threadable.init(1)
  67. self.reads = {}
  68. self.writes = {}
  69. self.toThreadQueue = Queue()
  70. self.toMainThread = Queue()
  71. self.workerThread = None
  72. self.mainWaker = None
  73. posixbase.PosixReactorBase.__init__(self)
  74. self.addSystemEventTrigger("after", "shutdown", self._mainLoopShutdown)
  75. def wakeUp(self):
  76. # we want to wake up from any thread
  77. self.waker.wakeUp()
  78. def callLater(self, *args, **kw):
  79. tple = posixbase.PosixReactorBase.callLater(self, *args, **kw)
  80. self.wakeUp()
  81. return tple
  82. def _sendToMain(self, msg, *args):
  83. self.toMainThread.put((msg, args))
  84. if self.mainWaker is not None:
  85. self.mainWaker()
  86. def _sendToThread(self, fn, *args):
  87. self.toThreadQueue.put((fn, args))
  88. def _preenDescriptorsInThread(self):
  89. log.msg("Malformed file descriptor found. Preening lists.")
  90. readers = self.reads.keys()
  91. writers = self.writes.keys()
  92. self.reads.clear()
  93. self.writes.clear()
  94. for selDict, selList in ((self.reads, readers), (self.writes, writers)):
  95. for selectable in selList:
  96. try:
  97. select.select([selectable], [selectable], [selectable], 0)
  98. except BaseException:
  99. log.msg("bad descriptor %s" % selectable)
  100. else:
  101. selDict[selectable] = 1
  102. def _workerInThread(self):
  103. try:
  104. while 1:
  105. fn, args = self.toThreadQueue.get()
  106. fn(*args)
  107. except SystemExit:
  108. pass # Exception indicates this thread should exit
  109. except BaseException:
  110. f = failure.Failure()
  111. self._sendToMain("Failure", f)
  112. def _doSelectInThread(self, timeout):
  113. """Run one iteration of the I/O monitor loop.
  114. This will run all selectables who had input or output readiness
  115. waiting for them.
  116. """
  117. reads = self.reads
  118. writes = self.writes
  119. while 1:
  120. try:
  121. r, w, ignored = _select(reads.keys(), writes.keys(), [], timeout)
  122. break
  123. except ValueError:
  124. # Possibly a file descriptor has gone negative?
  125. log.err()
  126. self._preenDescriptorsInThread()
  127. except TypeError:
  128. # Something *totally* invalid (object w/o fileno, non-integral
  129. # result) was passed
  130. log.err()
  131. self._preenDescriptorsInThread()
  132. except OSError as se:
  133. # select(2) encountered an error
  134. if se.args[0] in (0, 2):
  135. # windows does this if it got an empty list
  136. if (not reads) and (not writes):
  137. return
  138. else:
  139. raise
  140. elif se.args[0] == EINTR:
  141. return
  142. elif se.args[0] == EBADF:
  143. self._preenDescriptorsInThread()
  144. else:
  145. # OK, I really don't know what's going on. Blow up.
  146. raise
  147. self._sendToMain("Notify", r, w)
  148. def _process_Notify(self, r, w):
  149. reads = self.reads
  150. writes = self.writes
  151. _drdw = self._doReadOrWrite
  152. _logrun = log.callWithLogger
  153. for selectables, method, dct in ((r, "doRead", reads), (w, "doWrite", writes)):
  154. for selectable in selectables:
  155. # if this was disconnected in another thread, kill it.
  156. if selectable not in dct:
  157. continue
  158. # This for pausing input when we're not ready for more.
  159. _logrun(selectable, _drdw, selectable, method, dct)
  160. def _process_Failure(self, f):
  161. f.raiseException()
  162. _doIterationInThread = _doSelectInThread
  163. def ensureWorkerThread(self):
  164. if self.workerThread is None or not self.workerThread.isAlive():
  165. self.workerThread = Thread(target=self._workerInThread)
  166. self.workerThread.start()
  167. def doThreadIteration(self, timeout):
  168. self._sendToThread(self._doIterationInThread, timeout)
  169. self.ensureWorkerThread()
  170. msg, args = self.toMainThread.get()
  171. getattr(self, "_process_" + msg)(*args)
  172. doIteration = doThreadIteration
  173. def _interleave(self):
  174. while self.running:
  175. self.runUntilCurrent()
  176. t2 = self.timeout()
  177. t = self.running and t2
  178. self._sendToThread(self._doIterationInThread, t)
  179. yield None
  180. msg, args = self.toMainThread.get_nowait()
  181. getattr(self, "_process_" + msg)(*args)
  182. def interleave(self, waker, *args, **kw):
  183. """
  184. interleave(waker) interleaves this reactor with the
  185. current application by moving the blocking parts of
  186. the reactor (select() in this case) to a separate
  187. thread. This is typically useful for integration with
  188. GUI applications which have their own event loop
  189. already running.
  190. See the module docstring for more information.
  191. """
  192. self.startRunning(*args, **kw)
  193. loop = self._interleave()
  194. def mainWaker(waker=waker, loop=loop):
  195. waker(partial(next, loop))
  196. self.mainWaker = mainWaker
  197. next(loop)
  198. self.ensureWorkerThread()
  199. def _mainLoopShutdown(self):
  200. self.mainWaker = None
  201. if self.workerThread is not None:
  202. self._sendToThread(raiseException, SystemExit)
  203. self.wakeUp()
  204. try:
  205. while 1:
  206. msg, args = self.toMainThread.get_nowait()
  207. except Empty:
  208. pass
  209. self.workerThread.join()
  210. self.workerThread = None
  211. try:
  212. while 1:
  213. fn, args = self.toThreadQueue.get_nowait()
  214. if fn is self._doIterationInThread:
  215. log.msg("Iteration is still in the thread queue!")
  216. elif fn is raiseException and args[0] is SystemExit:
  217. pass
  218. else:
  219. fn(*args)
  220. except Empty:
  221. pass
  222. def _doReadOrWrite(self, selectable, method, dict):
  223. try:
  224. why = getattr(selectable, method)()
  225. handfn = getattr(selectable, "fileno", None)
  226. if not handfn:
  227. why = _NO_FILENO
  228. elif handfn() == -1:
  229. why = _NO_FILEDESC
  230. except BaseException:
  231. why = sys.exc_info()[1]
  232. log.err()
  233. if why:
  234. self._disconnectSelectable(selectable, why, method == "doRead")
  235. def addReader(self, reader):
  236. """Add a FileDescriptor for notification of data available to read."""
  237. self._sendToThread(self.reads.__setitem__, reader, 1)
  238. self.wakeUp()
  239. def addWriter(self, writer):
  240. """Add a FileDescriptor for notification of data available to write."""
  241. self._sendToThread(self.writes.__setitem__, writer, 1)
  242. self.wakeUp()
  243. def removeReader(self, reader):
  244. """Remove a Selectable for notification of data available to read."""
  245. self._sendToThread(dictRemove, self.reads, reader)
  246. def removeWriter(self, writer):
  247. """Remove a Selectable for notification of data available to write."""
  248. self._sendToThread(dictRemove, self.writes, writer)
  249. def removeAll(self):
  250. return self._removeAll(self.reads, self.writes)
  251. def getReaders(self):
  252. return list(self.reads.keys())
  253. def getWriters(self):
  254. return list(self.writes.keys())
  255. def stop(self):
  256. """
  257. Extend the base stop implementation to also wake up the select thread so
  258. that C{runUntilCurrent} notices the reactor should stop.
  259. """
  260. posixbase.PosixReactorBase.stop(self)
  261. self.wakeUp()
  262. def run(self, installSignalHandlers=True):
  263. self.startRunning(installSignalHandlers=installSignalHandlers)
  264. self.mainLoop()
  265. def mainLoop(self):
  266. q = Queue()
  267. self.interleave(q.put)
  268. while self.running:
  269. try:
  270. q.get()()
  271. except StopIteration:
  272. break
  273. def install():
  274. """Configure the twisted mainloop to be run using the select() reactor."""
  275. reactor = ThreadedSelectReactor()
  276. from twisted.internet.main import installReactor
  277. installReactor(reactor)
  278. return reactor
  279. __all__ = ["install"]