Skip to content

Commit d933065

Browse files
vstinnerserhiy-storchaka
authored andcommitted
pythongh-59705: Add _thread.set_name() function (python#127338)
On Linux, threading.Thread now sets the thread name to the operating system. * configure now checks if pthread_getname_np() and pthread_setname_np() functions are available. * Add PYTHREAD_NAME_MAXLEN macro. * Add _thread._NAME_MAXLEN constant for test_threading. Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 29eb0a8 commit d933065

File tree

8 files changed

+342
-2
lines changed

8 files changed

+342
-2
lines changed

Lib/test/test_threading.py

+60
Original file line numberDiff line numberDiff line change
@@ -2104,6 +2104,66 @@ def test__all__(self):
21042104
support.check__all__(self, threading, ('threading', '_thread'),
21052105
extra=extra, not_exported=not_exported)
21062106

2107+
@unittest.skipUnless(hasattr(_thread, 'set_name'), "missing _thread.set_name")
2108+
@unittest.skipUnless(hasattr(_thread, '_get_name'), "missing _thread._get_name")
2109+
def test_set_name(self):
2110+
# set_name() limit in bytes
2111+
truncate = getattr(_thread, "_NAME_MAXLEN", None)
2112+
limit = truncate or 100
2113+
2114+
tests = [
2115+
# test short ASCII name
2116+
"CustomName",
2117+
2118+
# test short non-ASCII name
2119+
"namé€",
2120+
2121+
# embedded null character: name is truncated
2122+
# at the first null character
2123+
"embed\0null",
2124+
2125+
# Test long ASCII names (not truncated)
2126+
"x" * limit,
2127+
2128+
# Test long ASCII names (truncated)
2129+
"x" * (limit + 10),
2130+
2131+
# Test long non-ASCII name (truncated)
2132+
"x" * (limit - 1) + "é€",
2133+
]
2134+
if os_helper.FS_NONASCII:
2135+
tests.append(f"nonascii:{os_helper.FS_NONASCII}")
2136+
if os_helper.TESTFN_UNENCODABLE:
2137+
tests.append(os_helper.TESTFN_UNENCODABLE)
2138+
2139+
if sys.platform.startswith("solaris"):
2140+
encoding = "utf-8"
2141+
else:
2142+
encoding = sys.getfilesystemencoding()
2143+
2144+
def work():
2145+
nonlocal work_name
2146+
work_name = _thread._get_name()
2147+
2148+
for name in tests:
2149+
encoded = name.encode(encoding, "replace")
2150+
if b'\0' in encoded:
2151+
encoded = encoded.split(b'\0', 1)[0]
2152+
if truncate is not None:
2153+
encoded = encoded[:truncate]
2154+
if sys.platform.startswith("solaris"):
2155+
expected = encoded.decode("utf-8", "surrogateescape")
2156+
else:
2157+
expected = os.fsdecode(encoded)
2158+
2159+
with self.subTest(name=name, expected=expected):
2160+
work_name = None
2161+
thread = threading.Thread(target=work, name=name)
2162+
thread.start()
2163+
thread.join()
2164+
self.assertEqual(work_name, expected,
2165+
f"{len(work_name)=} and {len(expected)=}")
2166+
21072167

21082168
class InterruptMainTests(unittest.TestCase):
21092169
def check_interrupt_main_with_signal_handler(self, signum):

Lib/threading.py

+9
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@
4848
__all__.append('get_native_id')
4949
except AttributeError:
5050
_HAVE_THREAD_NATIVE_ID = False
51+
try:
52+
_set_name = _thread.set_name
53+
except AttributeError:
54+
_set_name = None
5155
ThreadError = _thread.error
5256
try:
5357
_CRLock = _thread.RLock
@@ -1027,6 +1031,11 @@ def _bootstrap_inner(self):
10271031
self._set_ident()
10281032
if _HAVE_THREAD_NATIVE_ID:
10291033
self._set_native_id()
1034+
if _set_name is not None and self._name:
1035+
try:
1036+
_set_name(self._name)
1037+
except OSError:
1038+
pass
10301039
self._started.set()
10311040
with _active_limbo_lock:
10321041
_active[self._ident] = self
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
On Linux, :class:`threading.Thread` now sets the thread name to the
2+
operating system. Patch by Victor Stinner.

Modules/_threadmodule.c

+108
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
# include <signal.h> // SIGINT
1818
#endif
1919

20+
#include "clinic/_threadmodule.c.h"
21+
2022
// ThreadError is just an alias to PyExc_RuntimeError
2123
#define ThreadError PyExc_RuntimeError
2224

@@ -44,6 +46,13 @@ get_thread_state(PyObject *module)
4446
return (thread_module_state *)state;
4547
}
4648

49+
50+
/*[clinic input]
51+
module _thread
52+
[clinic start generated code]*/
53+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=be8dbe5cc4b16df7]*/
54+
55+
4756
// _ThreadHandle type
4857

4958
// Handles state transitions according to the following diagram:
@@ -2354,6 +2363,96 @@ PyDoc_STRVAR(thread__get_main_thread_ident_doc,
23542363
Internal only. Return a non-zero integer that uniquely identifies the main thread\n\
23552364
of the main interpreter.");
23562365

2366+
2367+
#ifdef HAVE_PTHREAD_GETNAME_NP
2368+
/*[clinic input]
2369+
_thread._get_name
2370+
2371+
Get the name of the current thread.
2372+
[clinic start generated code]*/
2373+
2374+
static PyObject *
2375+
_thread__get_name_impl(PyObject *module)
2376+
/*[clinic end generated code: output=20026e7ee3da3dd7 input=35cec676833d04c8]*/
2377+
{
2378+
// Linux and macOS are limited to respectively 16 and 64 bytes
2379+
char name[100];
2380+
pthread_t thread = pthread_self();
2381+
int rc = pthread_getname_np(thread, name, Py_ARRAY_LENGTH(name));
2382+
if (rc) {
2383+
errno = rc;
2384+
return PyErr_SetFromErrno(PyExc_OSError);
2385+
}
2386+
2387+
#ifdef __sun
2388+
return PyUnicode_DecodeUTF8(name, strlen(name), "surrogateescape");
2389+
#else
2390+
return PyUnicode_DecodeFSDefault(name);
2391+
#endif
2392+
}
2393+
#endif // HAVE_PTHREAD_GETNAME_NP
2394+
2395+
2396+
#ifdef HAVE_PTHREAD_SETNAME_NP
2397+
/*[clinic input]
2398+
_thread.set_name
2399+
2400+
name as name_obj: unicode
2401+
2402+
Set the name of the current thread.
2403+
[clinic start generated code]*/
2404+
2405+
static PyObject *
2406+
_thread_set_name_impl(PyObject *module, PyObject *name_obj)
2407+
/*[clinic end generated code: output=402b0c68e0c0daed input=7e7acd98261be82f]*/
2408+
{
2409+
#ifdef __sun
2410+
// Solaris always uses UTF-8
2411+
const char *encoding = "utf-8";
2412+
#else
2413+
// Encode the thread name to the filesystem encoding using the "replace"
2414+
// error handler
2415+
PyInterpreterState *interp = _PyInterpreterState_GET();
2416+
const char *encoding = interp->unicode.fs_codec.encoding;
2417+
#endif
2418+
PyObject *name_encoded;
2419+
name_encoded = PyUnicode_AsEncodedString(name_obj, encoding, "replace");
2420+
if (name_encoded == NULL) {
2421+
return NULL;
2422+
}
2423+
2424+
#ifdef PYTHREAD_NAME_MAXLEN
2425+
// Truncate to PYTHREAD_NAME_MAXLEN bytes + the NUL byte if needed
2426+
size_t len = PyBytes_GET_SIZE(name_encoded);
2427+
if (len > PYTHREAD_NAME_MAXLEN) {
2428+
PyObject *truncated;
2429+
truncated = PyBytes_FromStringAndSize(PyBytes_AS_STRING(name_encoded),
2430+
PYTHREAD_NAME_MAXLEN);
2431+
if (truncated == NULL) {
2432+
Py_DECREF(name_encoded);
2433+
return NULL;
2434+
}
2435+
Py_SETREF(name_encoded, truncated);
2436+
}
2437+
#endif
2438+
2439+
const char *name = PyBytes_AS_STRING(name_encoded);
2440+
#ifdef __APPLE__
2441+
int rc = pthread_setname_np(name);
2442+
#else
2443+
pthread_t thread = pthread_self();
2444+
int rc = pthread_setname_np(thread, name);
2445+
#endif
2446+
Py_DECREF(name_encoded);
2447+
if (rc) {
2448+
errno = rc;
2449+
return PyErr_SetFromErrno(PyExc_OSError);
2450+
}
2451+
Py_RETURN_NONE;
2452+
}
2453+
#endif // HAVE_PTHREAD_SETNAME_NP
2454+
2455+
23572456
static PyMethodDef thread_methods[] = {
23582457
{"start_new_thread", (PyCFunction)thread_PyThread_start_new_thread,
23592458
METH_VARARGS, start_new_thread_doc},
@@ -2393,6 +2492,8 @@ static PyMethodDef thread_methods[] = {
23932492
METH_O, thread__make_thread_handle_doc},
23942493
{"_get_main_thread_ident", thread__get_main_thread_ident,
23952494
METH_NOARGS, thread__get_main_thread_ident_doc},
2495+
_THREAD_SET_NAME_METHODDEF
2496+
_THREAD__GET_NAME_METHODDEF
23962497
{NULL, NULL} /* sentinel */
23972498
};
23982499

@@ -2484,6 +2585,13 @@ thread_module_exec(PyObject *module)
24842585

24852586
llist_init(&state->shutdown_handles);
24862587

2588+
#ifdef PYTHREAD_NAME_MAXLEN
2589+
if (PyModule_AddIntConstant(module, "_NAME_MAXLEN",
2590+
PYTHREAD_NAME_MAXLEN) < 0) {
2591+
return -1;
2592+
}
2593+
#endif
2594+
24872595
return 0;
24882596
}
24892597

Modules/clinic/_threadmodule.c.h

+104
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

configure

+30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)