Programmer's Python Async - Threads
Written by Mike James   
Tuesday, 31 January 2023
Article Index
Programmer's Python Async - Threads
Daemons
Local Variables
Thread Local Storage

Local Variables

The idea that threads run in the same process and share the same memory is something that makes them different from using multiprocessing, even though they seem to provide a similar facility. The idea that threads share storage is much more subtle than you might expect and is rarely discussed. Some variables and the objects that they reference are unique to the thread that is using them. - they are “thread local”.

A thread starts running the code in any callable object, most usually a function object. Consider the following function and its use by two threads:

import threading
def myThread(sym):
    temp=sym
    while True:
        print(temp)
t1=threading.Thread(target=myThread,args=("A",))
t2=threading.Thread(target=myThread,args=("B",))
t1.start()
t2.start()
t1.join()

The function simply repeatedly prints whatever is passed to it as the first parameter. Thread t1 prints A and t2 prints B – but how is this possible? If threads share the same memory how is it that t1 gets a copy of the local variable temp and t2 gets a different copy of the variable temp? The answer is that when a function is called all of the local variables, including any parameters, are created and they exist for the time that the function is executing. To be exact, they are created on the stack and the stack is popped when the function finishes. Normally you cannot call the same function more than once “at the same time” but when you use a thread you can. Each time a thread starts executing a function all of its local variables are created on the stack and are only accessible by that thread.

threadstore

In other words, each thread gets its own copy of any local variables. Putting this another way, local variables are thread local. Compare this to the behavior of an attribute of the function object or indeed any object:

import threading
def myThread(sym):
    myThread.temp=sym
    while True:
        print(myThread.temp)
       
myThread.temp=""
t1=threading.Thread(target=myThread,args=("A",))
t2=threading.Thread(target=myThread,args=("B",))
t1.start()
t2.start()
t1.join()

In this case we create an attribute, temp, of the myThread function object. If you aren’t sure about this see Programmer’s Python: Everything Is An Object, ISBN: 978-1871962741. Attributes exist as long as the object that “hosts” them exists and a function object exists even when its code is not being executed.

This means that function object attributes have a longer lifetime than local variables and they are not created when the function is called and destroyed when it ends. Because of this when the function object is accessed via different threads there is only one function object which they share and only one set of attributes which they can work with. As a result what you see if you run the program is a few “A”s and then nothing but “B”s because once t2 starts to run it changes the attribute to B and it isn’t changed back to A when t1 runs again.
To summarize:

threadstore2

  • Threads have their own copies of local variables but share global variables and attributes of objects that they do not create.

This last phrase, “objects that they do not create” deserves some clarification. All objects are global, but within a function it is possible for a local variable to reference a newly created object. If such a function is run using a thread then a new object will be created and the local variable will reference the thread’s own version of the object, for example:

import threading
class MyClass():
    temp=""
def myThread(sym):
    myObject=MyClass()
    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()

Notice that the function now creates an instance of MyClass and stores the value of its parameter in its temp attribute. When t1 starts to run the function it creates an instance and a local variable myObject to reference it. When t2 starts, it creates another instance of MyClass and a new local variable called myObject to reference it. Hence both threads have their own objects and their own local variables which reference them. This means that t1 prints A and t2 prints B as before.

When the threads end the local variables are destroyed and the two objects are garbage-collected. If the instance was created in the main program then both threads would reference the same object and its temp attribute could only store an A or a B and so after seeing a few As printed you would see just Bs.

If all of this seems obvious then well done. A good grasp of what is shared between threads and what is local to a thread is essential to avoiding subtle errors.



Last Updated ( Tuesday, 31 January 2023 )