Skip to content

Commit cf98e3d

Browse files
committed
Switch to standalone start_joinable_thread() function and rich thread handle object
1 parent 2cf0e73 commit cf98e3d

File tree

7 files changed

+288
-255
lines changed

7 files changed

+288
-255
lines changed

Lib/test/audit-tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,9 @@ def __call__(self):
454454
i = _thread.start_new_thread(test_func(), ())
455455
lock.acquire()
456456

457+
handle = _thread.start_joinable_thread(test_func())
458+
handle.join()
459+
457460

458461
def test_threading_abort():
459462
# Ensures that aborting PyThreadState_New raises the correct exception

Lib/test/test_audit.py

+2
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ def test_threading(self):
209209
expected = [
210210
("_thread.start_new_thread", "(<test_func>, (), None)"),
211211
("test.test_func", "()"),
212+
("_thread.start_joinable_thread", "(<test_func>,)"),
213+
("test.test_func", "()"),
212214
]
213215

214216
self.assertEqual(actual, expected)

Lib/test/test_concurrent_futures/test_process_pool.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,11 @@ def test_python_finalization_error(self):
194194

195195
context = self.get_context()
196196

197-
# gh-109047: Mock the threading.start_new_thread() function to inject
197+
# gh-109047: Mock the threading.start_joinable_thread() function to inject
198198
# RuntimeError: simulate the error raised during Python finalization.
199199
# Block the second creation: create _ExecutorManagerThread, but block
200200
# QueueFeederThread.
201-
orig_start_new_thread = threading._start_new_thread
201+
orig_start_new_thread = threading._start_joinable_thread
202202
nthread = 0
203203
def mock_start_new_thread(func, *args):
204204
nonlocal nthread
@@ -208,7 +208,7 @@ def mock_start_new_thread(func, *args):
208208
nthread += 1
209209
return orig_start_new_thread(func, *args)
210210

211-
with support.swap_attr(threading, '_start_new_thread',
211+
with support.swap_attr(threading, '_start_joinable_thread',
212212
mock_start_new_thread):
213213
executor = self.executor_type(max_workers=2, mp_context=context)
214214
with executor:

Lib/test/test_thread.py

+45-49
Original file line numberDiff line numberDiff line change
@@ -165,100 +165,98 @@ def test_join_thread(self):
165165

166166
def task():
167167
time.sleep(0.05)
168-
finished.append(None)
168+
finished.append(thread.get_ident())
169169

170170
with threading_helper.wait_threads_exit():
171-
joinable = True
172-
ident = thread.start_new_thread(task, (), {}, joinable)
173-
thread.join_thread(ident)
171+
handle = thread.start_joinable_thread(task)
172+
handle.join()
174173
self.assertEqual(len(finished), 1)
174+
self.assertEqual(handle.ident, finished[0])
175175

176176
def test_join_thread_already_exited(self):
177177
def task():
178178
pass
179179

180180
with threading_helper.wait_threads_exit():
181-
joinable = True
182-
ident = thread.start_new_thread(task, (), {}, joinable)
181+
handle = thread.start_joinable_thread(task)
183182
time.sleep(0.05)
184-
thread.join_thread(ident)
183+
handle.join()
185184

186-
def test_join_non_joinable(self):
185+
def test_join_several_times(self):
187186
def task():
188187
pass
189188

190189
with threading_helper.wait_threads_exit():
191-
ident = thread.start_new_thread(task, ())
190+
handle = thread.start_joinable_thread(task)
191+
handle.join()
192192
with self.assertRaisesRegex(ValueError, "not joinable"):
193-
thread.join_thread(ident)
193+
handle.join()
194+
195+
def test_joinable_not_joined(self):
196+
handle_destroyed = thread.allocate_lock()
197+
handle_destroyed.acquire()
194198

195-
def test_join_several_times(self):
196199
def task():
197-
pass
200+
handle_destroyed.acquire()
198201

199202
with threading_helper.wait_threads_exit():
200-
joinable = True
201-
ident = thread.start_new_thread(task, (), {}, joinable)
202-
thread.join_thread(ident)
203-
with self.assertRaisesRegex(ValueError, "not joinable"):
204-
thread.join_thread(ident)
203+
handle = thread.start_joinable_thread(task)
204+
del handle
205+
handle_destroyed.release()
205206

206207
def test_join_from_self(self):
207208
errors = []
208-
start_new_thread_returned = thread.allocate_lock()
209-
start_new_thread_returned.acquire()
209+
handles = []
210+
start_joinable_thread_returned = thread.allocate_lock()
211+
start_joinable_thread_returned.acquire()
210212
task_tried_to_join = thread.allocate_lock()
211213
task_tried_to_join.acquire()
212214

213215
def task():
214-
ident = thread.get_ident()
215-
# Wait for start_new_thread() to return so that the joinable threads
216-
# are populated with the ident, otherwise ValueError would be raised
217-
# instead.
218-
start_new_thread_returned.acquire()
216+
start_joinable_thread_returned.acquire()
219217
try:
220-
thread.join_thread(ident)
218+
handles[0].join()
221219
except Exception as e:
222220
errors.append(e)
223221
finally:
224222
task_tried_to_join.release()
225223

226224
with threading_helper.wait_threads_exit():
227-
joinable = True
228-
ident = thread.start_new_thread(task, (), {}, joinable)
229-
start_new_thread_returned.release()
230-
# Can still join after join_thread() failed in other thread
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
231229
task_tried_to_join.acquire()
232-
thread.join_thread(ident)
230+
handle.join()
233231

234232
assert len(errors) == 1
235233
with self.assertRaisesRegex(RuntimeError, "Cannot join current thread"):
236234
raise errors[0]
237235

238236
def test_detach_from_self(self):
239237
errors = []
240-
start_new_thread_returned = thread.allocate_lock()
241-
start_new_thread_returned.acquire()
238+
handles = []
239+
start_joinable_thread_returned = thread.allocate_lock()
240+
start_joinable_thread_returned.acquire()
242241
thread_detached = thread.allocate_lock()
243242
thread_detached.acquire()
244243

245244
def task():
246-
ident = thread.get_ident()
247-
start_new_thread_returned.acquire()
245+
start_joinable_thread_returned.acquire()
248246
try:
249-
thread.detach_thread(ident)
247+
handles[0].detach()
250248
except Exception as e:
251249
errors.append(e)
252250
finally:
253251
thread_detached.release()
254252

255253
with threading_helper.wait_threads_exit():
256-
joinable = True
257-
ident = thread.start_new_thread(task, (), {}, joinable)
258-
start_new_thread_returned.release()
254+
handle = thread.start_joinable_thread(task)
255+
handles.append(handle)
256+
start_joinable_thread_returned.release()
259257
thread_detached.acquire()
260258
with self.assertRaisesRegex(ValueError, "not joinable"):
261-
thread.join_thread(ident)
259+
handle.join()
262260

263261
assert len(errors) == 0
264262

@@ -270,25 +268,23 @@ def task():
270268
lock.acquire()
271269

272270
with threading_helper.wait_threads_exit():
273-
joinable = True
274-
ident = thread.start_new_thread(task, (), {}, joinable)
275-
# detach_thread() returns even though the thread is blocked on lock
276-
thread.detach_thread(ident)
277-
# join_thread() then cannot be called anymore
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
278275
with self.assertRaisesRegex(ValueError, "not joinable"):
279-
thread.join_thread(ident)
276+
handle.join()
280277
lock.release()
281278

282279
def test_join_then_detach(self):
283280
def task():
284281
pass
285282

286283
with threading_helper.wait_threads_exit():
287-
joinable = True
288-
ident = thread.start_new_thread(task, (), {}, joinable)
289-
thread.join_thread(ident)
284+
handle = thread.start_joinable_thread(task)
285+
handle.join()
290286
with self.assertRaisesRegex(ValueError, "not joinable"):
291-
thread.detach_thread(ident)
287+
handle.detach()
292288

293289

294290
class Barrier:

Lib/test/test_threading.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -376,16 +376,16 @@ def test_limbo_cleanup(self):
376376
# Issue 7481: Failure to start thread should cleanup the limbo map.
377377
def fail_new_thread(*args):
378378
raise threading.ThreadError()
379-
_start_new_thread = threading._start_new_thread
380-
threading._start_new_thread = fail_new_thread
379+
_start_joinable_thread = threading._start_joinable_thread
380+
threading._start_joinable_thread = fail_new_thread
381381
try:
382382
t = threading.Thread(target=lambda: None)
383383
self.assertRaises(threading.ThreadError, t.start)
384384
self.assertFalse(
385385
t in threading._limbo,
386386
"Failed to cleanup _limbo map on failure of Thread.start().")
387387
finally:
388-
threading._start_new_thread = _start_new_thread
388+
threading._start_joinable_thread = _start_joinable_thread
389389

390390
def test_finalize_running_thread(self):
391391
# Issue 1402: the PyGILState_Ensure / _Release functions may be called

Lib/threading.py

+9-15
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@
3434
'setprofile_all_threads','settrace_all_threads']
3535

3636
# Rename some stuff so "from threading import *" is safe
37-
_start_new_thread = _thread.start_new_thread
38-
_join_thread = _thread.join_thread
39-
_detach_thread = _thread.detach_thread
37+
_start_joinable_thread = _thread.start_joinable_thread
4038
_daemon_threads_allowed = _thread.daemon_threads_allowed
4139
_allocate_lock = _thread.allocate_lock
4240
_set_sentinel = _thread._set_sentinel
@@ -928,6 +926,7 @@ class is implemented.
928926
self._native_id = None
929927
self._tstate_lock = None
930928
self._join_lock = None
929+
self._handle = None
931930
self._started = Event()
932931
self._is_stopped = False
933932
self._initialized = True
@@ -993,19 +992,13 @@ def start(self):
993992
_limbo[self] = self
994993
try:
995994
# Start joinable thread
996-
_start_new_thread(self._bootstrap, (), {}, True)
995+
self._handle = _start_joinable_thread(self._bootstrap)
997996
except Exception:
998997
with _active_limbo_lock:
999998
del _limbo[self]
1000999
raise
10011000
self._started.wait() # Will set ident and native_id
10021001

1003-
# We need to make sure the OS thread is either explicitly joined or
1004-
# detached at some point, otherwise system resources can be leaked.
1005-
def _finalizer(wr, _detach_thread=_detach_thread, ident=self._ident):
1006-
_detach_thread(ident)
1007-
self._non_joined_finalizer = _weakref.ref(self, _finalizer)
1008-
10091002
def run(self):
10101003
"""Method representing the thread's activity.
10111004
@@ -1168,12 +1161,13 @@ def _join_os_thread(self):
11681161
if join_lock is None:
11691162
return
11701163
with join_lock:
1171-
# Calling join() multiple times simultaneously would raise
1172-
# an exception in one of the callers.
1173-
if self._join_lock is not None:
1174-
_join_thread(self._ident)
1164+
# Calling join() multiple times would raise an exception
1165+
# in one of the callers.
1166+
if self._handle is not None:
1167+
self._handle.join()
1168+
self._handle = None
1169+
# No need to keep this around
11751170
self._join_lock = None
1176-
self._non_joined_finalizer = None
11771171

11781172
def _wait_for_tstate_lock(self, block=True, timeout=-1):
11791173
# Issue #18808: wait for the thread state to be gone.

0 commit comments

Comments
 (0)