@@ -160,6 +160,132 @@ def task():
160160 f"Exception ignored in thread started by { task !r} " )
161161 self .assertIsNotNone (cm .unraisable .exc_traceback )
162162
163+ def test_join_thread (self ):
164+ finished = []
165+
166+ def task ():
167+ time .sleep (0.05 )
168+ finished .append (thread .get_ident ())
169+
170+ with threading_helper .wait_threads_exit ():
171+ handle = thread .start_joinable_thread (task )
172+ handle .join ()
173+ self .assertEqual (len (finished ), 1 )
174+ self .assertEqual (handle .ident , finished [0 ])
175+
176+ def test_join_thread_already_exited (self ):
177+ def task ():
178+ pass
179+
180+ with threading_helper .wait_threads_exit ():
181+ handle = thread .start_joinable_thread (task )
182+ time .sleep (0.05 )
183+ handle .join ()
184+
185+ def test_join_several_times (self ):
186+ def task ():
187+ pass
188+
189+ with threading_helper .wait_threads_exit ():
190+ handle = thread .start_joinable_thread (task )
191+ handle .join ()
192+ with self .assertRaisesRegex (ValueError , "not joinable" ):
193+ handle .join ()
194+
195+ def test_joinable_not_joined (self ):
196+ handle_destroyed = thread .allocate_lock ()
197+ handle_destroyed .acquire ()
198+
199+ def task ():
200+ handle_destroyed .acquire ()
201+
202+ with threading_helper .wait_threads_exit ():
203+ handle = thread .start_joinable_thread (task )
204+ del handle
205+ handle_destroyed .release ()
206+
207+ def test_join_from_self (self ):
208+ errors = []
209+ handles = []
210+ start_joinable_thread_returned = thread .allocate_lock ()
211+ start_joinable_thread_returned .acquire ()
212+ task_tried_to_join = thread .allocate_lock ()
213+ task_tried_to_join .acquire ()
214+
215+ def task ():
216+ start_joinable_thread_returned .acquire ()
217+ try :
218+ handles [0 ].join ()
219+ except Exception as e :
220+ errors .append (e )
221+ finally :
222+ task_tried_to_join .release ()
223+
224+ with threading_helper .wait_threads_exit ():
225+ handle = thread .start_joinable_thread (task )
226+ handles .append (handle )
227+ start_joinable_thread_returned .release ()
228+ # Can still join after joining failed in other thread
229+ task_tried_to_join .acquire ()
230+ handle .join ()
231+
232+ assert len (errors ) == 1
233+ with self .assertRaisesRegex (RuntimeError , "Cannot join current thread" ):
234+ raise errors [0 ]
235+
236+ def test_detach_from_self (self ):
237+ errors = []
238+ handles = []
239+ start_joinable_thread_returned = thread .allocate_lock ()
240+ start_joinable_thread_returned .acquire ()
241+ thread_detached = thread .allocate_lock ()
242+ thread_detached .acquire ()
243+
244+ def task ():
245+ start_joinable_thread_returned .acquire ()
246+ try :
247+ handles [0 ].detach ()
248+ except Exception as e :
249+ errors .append (e )
250+ finally :
251+ thread_detached .release ()
252+
253+ with threading_helper .wait_threads_exit ():
254+ handle = thread .start_joinable_thread (task )
255+ handles .append (handle )
256+ start_joinable_thread_returned .release ()
257+ thread_detached .acquire ()
258+ with self .assertRaisesRegex (ValueError , "not joinable" ):
259+ handle .join ()
260+
261+ assert len (errors ) == 0
262+
263+ def test_detach_then_join (self ):
264+ lock = thread .allocate_lock ()
265+ lock .acquire ()
266+
267+ def task ():
268+ lock .acquire ()
269+
270+ with threading_helper .wait_threads_exit ():
271+ handle = thread .start_joinable_thread (task )
272+ # detach() returns even though the thread is blocked on lock
273+ handle .detach ()
274+ # join() then cannot be called anymore
275+ with self .assertRaisesRegex (ValueError , "not joinable" ):
276+ handle .join ()
277+ lock .release ()
278+
279+ def test_join_then_detach (self ):
280+ def task ():
281+ pass
282+
283+ with threading_helper .wait_threads_exit ():
284+ handle = thread .start_joinable_thread (task )
285+ handle .join ()
286+ with self .assertRaisesRegex (ValueError , "not joinable" ):
287+ handle .detach ()
288+
163289
164290class Barrier :
165291 def __init__ (self , num_threads ):
0 commit comments