threads in python

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.

Ce contenu a été publié dans Non classé. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *