22import traceback
33from threading import Thread , Lock
44import outcome
5+ import ctypes
6+ import ctypes .util
57from itertools import count
68
9+ from typing import Callable , Optional , Tuple
10+ from functools import partial
11+
12+
13+ def _to_os_thread_name (name : str ) -> bytes :
14+ # ctypes handles the trailing \00
15+ return name .encode ("ascii" , errors = "replace" )[:15 ]
16+
17+
18+ # used to construct the method used to set os thread name, or None, depending on platform.
19+ # called once on import
20+ def get_os_thread_name_func () -> Optional [Callable [[Optional [int ], str ], None ]]:
21+ def namefunc (setname : Callable [[int , bytes ], int ], ident : Optional [int ], name : str ):
22+ # Thread.ident is None "if it has not been started". Unclear if that can happen
23+ # with current usage.
24+ if ident is not None : # pragma: no cover
25+ setname (ident , _to_os_thread_name (name ))
26+
27+ # namefunc on mac also takes an ident, even if pthread_setname_np doesn't/can't use it
28+ # so the caller don't need to care about platform.
29+ def darwin_namefunc (
30+ setname : Callable [[bytes ], int ], ident : Optional [int ], name : str
31+ ):
32+ # I don't know if Mac can rename threads that hasn't been started, but default
33+ # to no to be on the safe side.
34+ if ident is not None : # pragma: no cover
35+ setname (_to_os_thread_name (name ))
36+
37+ # find the pthread library
38+ # this will fail on windows
39+ libpthread_path = ctypes .util .find_library ("pthread" )
40+ if not libpthread_path :
41+ return None
42+ libpthread = ctypes .CDLL (libpthread_path )
43+
44+ # get the setname method from it
45+ # afaik this should never fail
46+ pthread_setname_np = getattr (libpthread , "pthread_setname_np" , None )
47+ if pthread_setname_np is None : # pragma: no cover
48+ return None
49+
50+ # specify function prototype
51+ pthread_setname_np .restype = ctypes .c_int
52+
53+ # on mac OSX pthread_setname_np does not take a thread id,
54+ # it only lets threads name themselves, which is not a problem for us.
55+ # Just need to make sure to call it correctly
56+ if sys .platform == "darwin" :
57+ pthread_setname_np .argtypes = [ctypes .c_char_p ]
58+ return partial (darwin_namefunc , pthread_setname_np )
59+
60+ # otherwise assume linux parameter conventions. Should also work on *BSD
61+ pthread_setname_np .argtypes = [ctypes .c_void_p , ctypes .c_char_p ]
62+ return partial (namefunc , pthread_setname_np )
63+
64+
65+ # construct os thread name method
66+ set_os_thread_name = get_os_thread_name_func ()
67+
768# The "thread cache" is a simple unbounded thread pool, i.e., it automatically
869# spawns as many threads as needed to handle all the requests its given. Its
970# only purpose is to cache worker threads so that they don't have to be
44105
45106class WorkerThread :
46107 def __init__ (self , thread_cache ):
47- self ._job = None
108+ self ._job : Optional [ Tuple [ Callable , Callable , str ]] = None
48109 self ._thread_cache = thread_cache
49110 # This Lock is used in an unconventional way.
50111 #
@@ -54,16 +115,34 @@ def __init__(self, thread_cache):
54115 # Initially we have no job, so it starts out in locked state.
55116 self ._worker_lock = Lock ()
56117 self ._worker_lock .acquire ()
57- thread = Thread (target = self ._work , daemon = True )
58- thread .name = f"Trio worker thread { next (name_counter )} "
59- thread .start ()
118+ self ._default_name = f"Trio thread { next (name_counter )} "
119+
120+ self ._thread = Thread (target = self ._work , name = self ._default_name , daemon = True )
121+
122+ if set_os_thread_name :
123+ set_os_thread_name (self ._thread .ident , self ._default_name )
124+ self ._thread .start ()
60125
61126 def _handle_job (self ):
62127 # Handle job in a separate method to ensure user-created
63128 # objects are cleaned up in a consistent manner.
64- fn , deliver = self ._job
129+ assert self ._job is not None
130+ fn , deliver , name = self ._job
65131 self ._job = None
132+
133+ # set name
134+ if name is not None :
135+ self ._thread .name = name
136+ if set_os_thread_name :
137+ set_os_thread_name (self ._thread .ident , name )
66138 result = outcome .capture (fn )
139+
140+ # reset name if it was changed
141+ if name is not None :
142+ self ._thread .name = self ._default_name
143+ if set_os_thread_name :
144+ set_os_thread_name (self ._thread .ident , self ._default_name )
145+
67146 # Tell the cache that we're available to be assigned a new
68147 # job. We do this *before* calling 'deliver', so that if
69148 # 'deliver' triggers a new job, it can be assigned to us
@@ -102,19 +181,19 @@ class ThreadCache:
102181 def __init__ (self ):
103182 self ._idle_workers = {}
104183
105- def start_thread_soon (self , fn , deliver ):
184+ def start_thread_soon (self , fn , deliver , name : Optional [ str ] = None ):
106185 try :
107186 worker , _ = self ._idle_workers .popitem ()
108187 except KeyError :
109188 worker = WorkerThread (self )
110- worker ._job = (fn , deliver )
189+ worker ._job = (fn , deliver , name )
111190 worker ._worker_lock .release ()
112191
113192
114193THREAD_CACHE = ThreadCache ()
115194
116195
117- def start_thread_soon (fn , deliver ):
196+ def start_thread_soon (fn , deliver , name : Optional [ str ] = None ):
118197 """Runs ``deliver(outcome.capture(fn))`` in a worker thread.
119198
120199 Generally ``fn`` does some blocking work, and ``deliver`` delivers the
@@ -174,4 +253,4 @@ def start_thread_soon(fn, deliver):
174253 limit how many threads they're using then it's polite to respect that.
175254
176255 """
177- THREAD_CACHE .start_thread_soon (fn , deliver )
256+ THREAD_CACHE .start_thread_soon (fn , deliver , name )
0 commit comments