Purpose and overview
This module constructs higher-level threading interfaces on top of the lower level
_thread
module.
We have other alternatives to the threading module, but the module is still an appropriate model
if you want to run multiple I/O-bound tasks simultaneously.
limitations
– The design of this module is loosely based on Java’s threading model but with important
limitations since Python’s Thread class supports a subset of the behavior of Java’s Thread
class: there are no priorities, no thread groups, and threads cannot be destroyed, stopped,
suspended, resumed, or interrupted.
– Similarly to Java thread, thread is a limited api: it is a low level API, that is we have to
perform several operations to execute our logic and sometimes it can be brittle and create a
mess(thread exhaustion, deadlocks, performance issues, complex and unclear code to maintain…).
A very basic example is the no way of returning a result from a thread.
To mimic that behavior we have to trick or otherwise we can use a higher level api.
threading module level functions
threading.active_count()
:
Return the number of Thread objects currently alive.
That is equal to the length of the list returned by enumerate()
.
threading.current_thread()
:
Return the current Thread
object, corresponding to the caller’s thread of
control.
If the caller’s thread of control was not created through the threading module, a dummy thread
object with limited functionality is returned.
threading.excepthook(args, /)
:
Handle uncaught exception raised by Thread.run()
.
The args argument has the following attributes:
– exc_type
: Exception type.
– exc_value
: Exception value, can be None.
– exc_traceback
: Exception traceback, can be None.
– thread
: Thread which raised the exception, can be None.
Advanced information:
– If exc_type
is SystemExit
, the exception is silently ignored.
Otherwise, the exception is printed out on sys.stderr.
– If this function raises an exception, sys.excepthook()
is called to handle
it.
– threading.excepthook()
can be overridden to control how uncaught exceptions
raised by
Thread.run() are handled.
threading.get_ident()
:
Return the ‘thread identifier’ of the current thread. This is a nonzero integer.
Its value has no direct meaning; it is intended as a magic cookie to be used e.g. to index a
dictionary of thread-specific data.
threading.get_native_id()
:
Return the native integral Thread ID of the current thread assigned by the kernel. This is a
non-negative integer.
Its value may be used to uniquely identify this particular thread system-wide.
threading.enumerate()
:
Return a list of all Thread objects currently active.
The list includes daemonic threads and dummy thread objects created by current_thread().
important:
– It excludes terminated threads and threads that have not yet been started.
– the main thread is always part of the result, even when terminated.
threading.main_thread()
:
Return the main Thread object. In normal conditions, the main thread is the thread from which
the Python interpreter was started.
Thread-Local Data
Thread-local data is data whose values are thread specific. To manage thread-local data, just create an instance of local (or a subclass) and store attributes on it:
mydata = threading.local() mydata.x = 1 |
The instance’s values will be different for separate threads.
Thread Objects
Generality
The Thread class represents an activity that is run in a separate thread of control.
There are two ways to specify the activity:
– passing a callable object to the constructor.
– or by overriding the run() method in a subclass.
Methods we can override in the thread class:
__init__()
and run()
methods.
Methods and properties with a quite similar behavior as in Java:
– start(),run(),join(),is_alive()
, name
property, daemon
property
Exception handing inside threads:
– We can handle it inside the run() function
– If the run() method raises an exception, threading.excepthook() is called to handle it. By
default, threading.excepthook() ignores silently SystemExit.
Example: daemon and non daemon threads hand how to specify the target and the args of the thread
import threading from threading import Thread from time import sleep def main(): thread_daemon = Thread(daemon=True, target=lambda x: (print('start daemon operation'), sleep(10), print('end1 daemon operation')), args=('David',)) thread_one = Thread(name='Thread David', target=say_hello, args=('David',)) thread_two = Thread(name='Thread Orion', target=say_hello, args=('Orion',)) thread_daemon.start() thread_one.start() thread_two.start() print(f'threading.enumerate() during execution={threading.enumerate()}') def say_hello(name: str): print(f'threading.current_thread()={threading.current_thread()}') sleep(0.5) print(f'hello {name}') if __name__ == '__main__': main() sleep(1) print(f'threading.enumerate() after execution of the non daemon threads=' f'{threading.enumerate()}') |
Output:
start daemon operation threading.current_thread()=<Thread(Thread David, started 20544)> threading.current_thread()=<Thread(Thread Orion, started 7392)> threading.enumerate() during execution=[<_MainThread(MainThread, started 668)>, <Thread(Thread-1, started daemon 17604)>, <Thread(Thread David, started 20544)>, <Thread(Thread Orion, started 7392)>] hello Orionhello David threading.enumerate() after execution of the non daemon threads=[<_MainThread(MainThread, started 668)>, <Thread(Thread-1, started daemon 17604)>] |
Remarks on the code
– we start 3 threads: 2 non daemon threads and 1 daemon thread.
– the non daemon threads have a very fast processing while the daemon thread has a longer
processing but we see that as soon as non daemon threads are finished, the program exits and so
the daemon thread is abruptly terminated.
– args
argument has to contain a tuple, so if we have only one argument to pass we
have to specify a trailing comma at the end.
– As well as in Java we have to invoke the start()
method to run a thread.
Example: use the join()
method on a thread object to block the current
execution(generally the main()
thread) until the thread is terminated
import time from threading import Thread from time import sleep thread_result: dict[str, float] = {} def main(): thread_one = Thread(target=fine_best_price_in_west_coast, args=('toothbrush',)) thread_two = Thread(target=fine_best_price_in_east_coast, args=('toothbrush',)) print(f"threads started at {time.strftime('%X')}") thread_one.start() thread_two.start() thread_one.join() thread_two.join() print(f"threads finished at {time.strftime('%X')}") minimum_price = min(thread_result['one'], thread_result['two']) print(f'minimum_price={minimum_price}') def fine_best_price_in_west_coast(name: str): sleep(2) # Fake value thread_result['one'] = 2.5 def fine_best_price_in_east_coast(name: str): sleep(4) # Fake value thread_result['two'] = 4 if __name__ == '__main__': main() |
Output:
threads started at 13:29:28 threads finished at 13:29:32 minimum_price=2.5 |
Remarks on the code:
– If we want to have a concurrent execution of the threads, we have to execute the
join()
method after we started them.
– Since the thread cannot return a result, we trick by storing the result of each thread
execution in a module level variable. It is quite tricky because any part of the code can
overwrite the result.