1- import ctypes
1+ from gevent import Greenlet
22import datetime
33import logging
44import traceback
55import uuid
66
7- from gevent .monkey import get_original
8-
9- # Guarantee that Task threads will always be proper system threads, regardless of Gevent patches
10- Thread = get_original ("threading" , "Thread" )
11- Event = get_original ("threading" , "Event" )
12- Lock = get_original ("threading" , "Lock" )
13-
147_LOG = logging .getLogger (__name__ )
158
169
1710class ThreadTerminationError (SystemExit ):
1811 """Sibling of SystemExit, but specific to thread termination."""
1912
2013
21- class TaskThread (Thread ):
22- def __init__ (self , target = None , name = None , args = None , kwargs = None , daemon = True ):
23- Thread .__init__ (
24- self ,
25- group = None ,
26- target = target ,
27- name = name ,
28- args = args ,
29- kwargs = kwargs ,
30- daemon = daemon ,
31- )
14+ class TaskKillException (Exception ):
15+ """Sibling of SystemExit, but specific to thread termination."""
16+
17+
18+ class TaskThread (Greenlet ):
19+ def __init__ (self , target = None , args = None , kwargs = None ):
20+ Greenlet .__init__ (self )
3221 # Handle arguments
3322 if args is None :
3423 args = ()
@@ -56,10 +45,6 @@ def __init__(self, target=None, name=None, args=None, kwargs=None, daemon=True):
5645 self .progress : int = None # Percent progress of the task
5746 self .data = {} # Dictionary of custom data added during the task
5847
59- # Stuff for handling termination
60- self ._running_lock = Lock () # Lock obtained while self._target is running
61- self ._killed = Event () # Event triggered when thread is manually terminated
62-
6348 @property
6449 def id (self ):
6550 """Return ID of current TaskThread"""
@@ -86,6 +71,9 @@ def update_data(self, data: dict):
8671 # Store data to be used before task finishes (eg for real-time plotting)
8772 self .data .update (data )
8873
74+ def _run (self ):
75+ return self ._thread_proc (self ._target )(* self ._args , ** self ._kwargs )
76+
8977 def _thread_proc (self , f ):
9078 """
9179 Wraps the target function to handle recording `status` and `return` to `state`.
@@ -110,95 +98,12 @@ def wrapped(*args, **kwargs):
11098
11199 return wrapped
112100
113- def run (self ):
114- """Overrides default threading.Thread run() method"""
115- logging .debug ((self ._args , self ._kwargs ))
116- try :
117- with self ._running_lock :
118- if self ._killed .is_set ():
119- raise ThreadTerminationError ()
120- if self ._target :
121- self ._thread_proc (self ._target )(* self ._args , ** self ._kwargs )
122- finally :
123- # Avoid a refcycle if the thread is running a function with
124- # an argument that has a member that points to the thread.
125- del self ._target , self ._args , self ._kwargs
126-
127- def wait (self ):
128- """Start waiting for the task to finish before returning"""
129- print ("Joining thread {}" .format (self ))
130- self .join ()
131- return self ._return_value
132-
133- def async_raise (self , exc_type ):
134- """Raise an exception in this thread."""
135- # Should only be called on a started thread, so raise otherwise.
136- if self .ident is None :
137- raise RuntimeError (
138- "Cannot halt a thread that hasn't started. "
139- "No valid running thread identifier."
140- )
141-
142- # If the thread has died we don't want to raise an exception so log.
143- if not self .is_alive ():
144- _LOG .debug (
145- "Not raising %s because thread %s (%s) is not alive" ,
146- exc_type ,
147- self .name ,
148- self .ident ,
149- )
150- return
151-
152- result = ctypes .pythonapi .PyThreadState_SetAsyncExc (
153- ctypes .c_long (self .ident ), ctypes .py_object (exc_type )
154- )
155- if result == 0 and self .is_alive ():
156- # Don't raise an exception an error unnecessarily if the thread is dead.
157- raise ValueError ("Thread ID was invalid." , self .ident )
158- elif result > 1 :
159- # Something bad happened, call with a NULL exception to undo.
160- ctypes .pythonapi .PyThreadState_SetAsyncExc (self .ident , None )
161- raise RuntimeError (
162- "Error: PyThreadState_SetAsyncExc %s %s (%s) %s"
163- % (exc_type , self .name , self .ident , result )
164- )
165-
166- def _is_thread_proc_running (self ):
167- """
168- Test if thread funtion (_thread_proc) is running,
169- by attemtping to acquire the lock _thread_proc acquires at runtime.
170- Returns:
171- bool: If _thread_proc is currently running
172- """
173- could_acquire = self ._running_lock .acquire (False ) # skipcq: PYL-E1111
174- if could_acquire :
175- self ._running_lock .release ()
176- return False
177- return True
178-
179- def terminate (self ):
180- """
181- Raise ThreadTerminatedException in the context of the given thread,
182- which should cause the thread to exit silently.
183- """
184- _LOG .warning (f"Terminating thread { self } " )
185- self ._killed .set ()
186- if not self .is_alive ():
187- logging .debug ("Cannot kill thread that is no longer running." )
188- return
189- if not self ._is_thread_proc_running ():
190- logging .debug (
191- "Thread's _thread_proc function is no longer running, "
192- "will not kill; letting thread exit gracefully."
193- )
194- return
195- self .async_raise (ThreadTerminationError )
196-
197- # Wait for the thread for finish closing. If the threaded function has cleanup code in a try-except,
198- # this pause allows it to finish running before the main process can continue.
199- while self ._is_thread_proc_running ():
200- pass
201-
101+ def kill (self , exception = TaskKillException , block = True , timeout = None ):
102+ # Kill the greenlet
103+ Greenlet .kill (self , exception = exception , block = block , timeout = timeout )
202104 # Set state to terminated
203105 self ._status = "terminated"
204106 self .progress = None
107+
108+ def terminate (self ):
109+ return self .kill ()
0 commit comments