Skip to content

gh-59705: Add _thread.set_name() function #127338

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c6d324d
gh-59705: Add _thread.set_name() function
vstinner Nov 27, 2024
63b5d52
Port to macOS
vstinner Nov 27, 2024
9f6a8ab
Add tests
vstinner Nov 27, 2024
d79e7af
Try to fix macOS _get_name()
vstinner Nov 27, 2024
ebd9752
Truncate to 15 bytes; add error handling
vstinner Nov 28, 2024
a7f5651
Address review
vstinner Nov 28, 2024
97ea645
Add test on non-ASCII name truncation
vstinner Nov 28, 2024
78a9ab9
Add test on non-ASCII name
vstinner Nov 28, 2024
dcf13f4
Test long name on non-Linux platforms
vstinner Nov 28, 2024
6ea7e5a
macOS is limited to 63 bytes
vstinner Nov 28, 2024
46721bb
Catch UnicodeEncodeError when seting the name
vstinner Nov 28, 2024
6962116
Add tests
vstinner Nov 28, 2024
5d27da0
Use "replace" error handler
vstinner Nov 28, 2024
b713910
Address review
vstinner Nov 29, 2024
5c20ea1
Use PyInterpreterState filesystem encoding
vstinner Nov 29, 2024
6088b37
FreeBSD truncates to 98 bytes silently
vstinner Dec 2, 2024
3c12d17
Merge branch 'main' into thread_set_name
vstinner Dec 3, 2024
bde935f
Fix test_threading on iOS
vstinner Dec 3, 2024
0584ca3
Fix create_test(): always encode using "replace"
vstinner Dec 3, 2024
7508b6c
Solaris truncates to 31 bytes
vstinner Dec 3, 2024
f4a9f40
Solaris always uses UTF-8
vstinner Dec 4, 2024
ac6d726
Add PYTHREAD_NAME_MAXLEN macro
vstinner Dec 4, 2024
08e5922
Fix configure.ac if PYTHREAD_NAME_MAXLEN is not set
vstinner Dec 4, 2024
681624e
Address Serhiy's review
vstinner Dec 5, 2024
f57339f
Solaris always use UTF-8
vstinner Dec 6, 2024
3941123
Simplify tests
vstinner Dec 6, 2024
2e27043
Use @unittest.skipUnless
vstinner Dec 6, 2024
6ab2944
Solaris uses UTF-8
vstinner Dec 6, 2024
b370c49
Update Lib/test/test_threading.py
vstinner Dec 6, 2024
beeae59
add test on embedded null character
vstinner Dec 6, 2024
ae956a0
Optimize code truncating the name
vstinner Dec 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -2103,6 +2103,61 @@ def test__all__(self):
support.check__all__(self, threading, ('threading', '_thread'),
extra=extra, not_exported=not_exported)

def test_set_name(self):
try:
get_name = _thread._get_name
_thread.set_name
except AttributeError:
self.skipTest("need thread._get_name() and thread.set_name()")

def work():
nonlocal work_name
work_name = get_name()

# name not too long to fit into Linux 15 bytes limit
name = "CustomName"
tests = [(name, name)]

# Test non-ASCII short name
name = "namé€"
try:
os.fsencode(name)
except UnicodeEncodeError:
# name cannot be encoded to the filesystem encoding
pass
else:
tests.append((name, name))

if sys.platform == "linux":
# On Linux, set_name() truncates the name to 15 bytes.

# Test ASCII name
name = "x" * 100
tests.append((name, name[:15]))

# Test non-ASCII name
name = "x" * 14 + "é€"
try:
encoded = os.fsencode(name)
except UnicodeEncodeError:
# name cannot be encoded to the filesystem encoding
pass
else:
expected = os.fsdecode(encoded[:15])
tests.append((name, expected))
else:
# Test long name
name = "x" * 100
tests.append((name, name))

for name, expected in tests:
with self.subTest(name=name, expected=expected):
work_name = None
thread = threading.Thread(target=work, name=name)
thread.start()
thread.join()
self.assertEqual(work_name, expected)


class InterruptMainTests(unittest.TestCase):
def check_interrupt_main_with_signal_handler(self, signum):
Expand Down
6 changes: 6 additions & 0 deletions Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
__all__.append('get_native_id')
except AttributeError:
_HAVE_THREAD_NATIVE_ID = False
try:
_set_name = _thread.set_name
except AttributeError:
_set_name = None
ThreadError = _thread.error
try:
_CRLock = _thread.RLock
Expand Down Expand Up @@ -1027,6 +1031,8 @@ def _bootstrap_inner(self):
self._set_ident()
if _HAVE_THREAD_NATIVE_ID:
self._set_native_id()
if _set_name is not None and self._name:
_set_name(self._name)
self._started.set()
with _active_limbo_lock:
_active[self._ident] = self
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
On Linux, :class:`threading.Thread` now sets the thread name to the
operating system. Patch by Victor Stinner.
79 changes: 79 additions & 0 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
# include <signal.h> // SIGINT
#endif

#include "clinic/_threadmodule.c.h"

// ThreadError is just an alias to PyExc_RuntimeError
#define ThreadError PyExc_RuntimeError

Expand Down Expand Up @@ -44,6 +46,13 @@ get_thread_state(PyObject *module)
return (thread_module_state *)state;
}


/*[clinic input]
module _thread
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=be8dbe5cc4b16df7]*/


// _ThreadHandle type

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


#ifdef HAVE_PTHREAD_GETNAME_NP
/*[clinic input]
_thread._get_name

Get the name of the current thread.
[clinic start generated code]*/

static PyObject *
_thread__get_name_impl(PyObject *module)
/*[clinic end generated code: output=20026e7ee3da3dd7 input=35cec676833d04c8]*/
{
char name[17];
size_t size = Py_ARRAY_LENGTH(name) - 1;
pthread_t thread = pthread_self();
int rc = pthread_getname_np(thread, name, size);
if (rc) {
errno = rc;
return PyErr_SetFromErrno(PyExc_OSError);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pthread_getname_np should add a trailing NUL byte, but like everything here, that's platform-specific. I suggest being defensive here.

Suggested change
name[Py_ARRAY_LENGTH(name)-1] = 0;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On what platform it does not add the null byte?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null byte is added on all supported platforms. Before I made sure that the buffer always ended with a null byte, but @serhiy-storchaka asked me to remove it. Let's be optimistic. We can adjust the code later if needed.

name[size] = 0;
return PyUnicode_DecodeFSDefault(name);
}
#endif // HAVE_PTHREAD_GETNAME_NP


#ifdef HAVE_PTHREAD_SETNAME_NP
/*[clinic input]
_thread.set_name

name as name_obj: object(converter="PyUnicode_FSConverter")

Set the name of the current thread.
[clinic start generated code]*/

static PyObject *
_thread_set_name_impl(PyObject *module, PyObject *name_obj)
/*[clinic end generated code: output=402b0c68e0c0daed input=a0459bd64f771808]*/
{
const char *name = PyBytes_AS_STRING(name_obj);
#ifdef __APPLE__
int rc = pthread_setname_np(name);
#else

#if defined(__linux__)
// Truncate to 16 bytes including the NUL byte
char buffer[16];
size_t len = strlen(name);
if (len > 15) {
memcpy(buffer, name, 15);
buffer[15] = 0;
name = buffer;
}
#endif

pthread_t thread = pthread_self();
int rc = pthread_setname_np(thread, name);
#endif
if (rc) {
errno = rc;
return PyErr_SetFromErrno(PyExc_OSError);
}
Py_RETURN_NONE;
}
#endif // HAVE_PTHREAD_SETNAME_NP


static PyMethodDef thread_methods[] = {
{"start_new_thread", (PyCFunction)thread_PyThread_start_new_thread,
METH_VARARGS, start_new_thread_doc},
Expand Down Expand Up @@ -2393,6 +2470,8 @@ static PyMethodDef thread_methods[] = {
METH_O, thread__make_thread_handle_doc},
{"_get_main_thread_ident", thread__get_main_thread_ident,
METH_NOARGS, thread__get_main_thread_ident_doc},
_THREAD_SET_NAME_METHODDEF
_THREAD__GET_NAME_METHODDEF
{NULL, NULL} /* sentinel */
};

Expand Down
102 changes: 102 additions & 0 deletions Modules/clinic/_threadmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions configure

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -5105,8 +5105,10 @@ AC_CHECK_FUNCS([ \
mknod mknodat mktime mmap mremap nice openat opendir pathconf pause pipe \
pipe2 plock poll posix_fadvise posix_fallocate posix_openpt posix_spawn posix_spawnp \
posix_spawn_file_actions_addclosefrom_np \
pread preadv preadv2 process_vm_readv pthread_cond_timedwait_relative_np pthread_condattr_setclock pthread_init \
pthread_kill ptsname ptsname_r pwrite pwritev pwritev2 readlink readlinkat readv realpath renameat \
pread preadv preadv2 process_vm_readv \
pthread_cond_timedwait_relative_np pthread_condattr_setclock pthread_init \
pthread_kill pthread_getname_np pthread_setname_np \
ptsname ptsname_r pwrite pwritev pwritev2 readlink readlinkat readv realpath renameat \
rtpSpawn sched_get_priority_max sched_rr_get_interval sched_setaffinity \
sched_setparam sched_setscheduler sem_clockwait sem_getvalue sem_open \
sem_timedwait sem_unlink sendfile setegid seteuid setgid sethostname \
Expand Down
6 changes: 6 additions & 0 deletions pyconfig.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,9 @@
/* Define to 1 if you have the `pthread_getcpuclockid' function. */
#undef HAVE_PTHREAD_GETCPUCLOCKID

/* Define to 1 if you have the `pthread_getname_np' function. */
#undef HAVE_PTHREAD_GETNAME_NP

/* Define to 1 if you have the <pthread.h> header file. */
#undef HAVE_PTHREAD_H

Expand All @@ -990,6 +993,9 @@
/* Define to 1 if you have the `pthread_kill' function. */
#undef HAVE_PTHREAD_KILL

/* Define to 1 if you have the `pthread_setname_np' function. */
#undef HAVE_PTHREAD_SETNAME_NP

/* Define to 1 if you have the `pthread_sigmask' function. */
#undef HAVE_PTHREAD_SIGMASK

Expand Down
Loading