Programmer's Python Async - Threads |
Written by Mike James | |||||
Tuesday, 31 January 2023 | |||||
Page 4 of 4
Thread Local StorageIt is obvious that global variable are always shared and in most cases this isn’t a problem. However, suppose you have some existing code that makes use of a global variable to store its state and we want to make it thread-safe. To do this we have to create a global variable that is thread-local. The threading module provides the local class which is a global object with thread-local attributes. That is, when you create an instance of threading.local any attributes that a thread creates are thread-local, for example: import threading myObject=threading.local() def myThread(sym): myObject.temp=sym while True: print(myObject.temp) t1=threading.Thread(target=myThread,args=("A",)) t2=threading.Thread(target=myThread,args=("B",)) t1.start() t2.start() t1.join() This repeatedly prints runs of As and Bs. The local class is used to create a thread-local object, myObject, and t1 and t2 use this to create their own thread-local temp attributes. If myObject was just a general object, i.e. not thread-local, then t1 and t2 would share a single copy of the attribute. At this point you should be wondering why anyone would want to use threading.local? Notice that a local object cannot be used to persist the state of a function that is repeatedly run using a thread simply because it really is thread-local and different for each thread. For example, you cannot use it to write a function that counts the number of times it has been called irrespective of the thread that calls it. For that you need a simple global variable that is the same for all threads. There really is no point in using threading.local if you are writing the code from scratch. If you need anything that is local to a thread then create it within the thread and it will automatically be thread-local. To see that this is true compare this example with the previous example – they achieve the same result, but the first one doesn’t use threading.local. To summarize:
Computing Pi with Multiple ThreadsIt is informative to compare the multi-process computation of pi given in the previous chapter with a multi-threaded computation: import threading import time def myPi(m,n): pi = 0 for k in range(m,n+1): s = 1 if k%2 else -1 pi += s / (2 * k - 1) print(4*pi) N=10000000 thread1=threading.Thread(target=myPi,args=(N//2+1,N)) t1=time.perf_counter() thread1.start() myPi(1,N//2) thread1.join() t2=time.perf_counter() print((t2-t1)*1000) You can see that this is virtually the same program, but using equivalent thread methods. If you try this out you will discover that, compared to the single-threaded version, the computation is slower. For example, on a dual-core Windows machine the time increased from 1700 ms to 1800 ms and on a Pi 4 from 4500 ms to 5000 ms. This increase in time lag shouldn't come as a surprise. The GIL means that the threads cannot run at the same time and hence they cannot make use of the additional cores. The multi-threaded program takes longer simply because of the overhead in switching between threads. The computation of Pi is a CPU-bound task and so there is no hope that using multiple threads can speed it up while the GIL is in force. However, when it comes to I/O-bound threads, the story is very different. In Chapter but not in this extract
Summary
Programmer's Python:
|
|||||
Last Updated ( Tuesday, 31 January 2023 ) |