3

I am currently having issues to do with my understanding of threading or possibly my understanding of how variables are passed/assigned thru threads in python. I have this simple program that takes in a list of current stocks that are displayed on a screen and grabs the stock information related to those. I am using threads so that I can constantly update the screen and constantly collect data. I am having two issues:

  1. Inside dataCollector_thread() i understand that if i append to the stocksOnScreenListInfo that the variable (stocksOnScreenListInfo) inside main is updated.

However I don't want to append to the list but rather just reassign the list like the following but this does not work?.

def dataCollector_thread(stocksOnScreenListInfo, stocksOnScreen):
    while(True):
        placeholder = []
        for stock in stocksOnScreen:
            placeholer.append(RetrieveQuote(stock))
        stocksOnScreenListInfo = placeholder
        time.sleep(5)
  1. Inside screenUpdate_thread i am wanting to update stocksOnScreen to the variable 'TSLA' defined by the function UpdateScreen. This does not seem to update its corresponding stocksOnScreen in main as when I print to check it continues to say 'AAPL'?

    def main(args): 
    
     stocksOnScreen = ["AAPL"] # List of the stocks currently displayed on LED screen
    
     stocksOnScreenListInfo = [] # The quote information list for each stock on screen 
    
     thread_data_collector = threading.Thread(target=dataCollector_thread, args=(stocksOnScreenListInfo,stocksOnScreen))
     thread_data_collector.daemon = True
     thread_data_collector.start()
    
     thread_screen = threading.Thread(target=screenUpdate_thread, args=(stocksSearchArray,stocksOnScreen))
     thread_screen.daemon = True
     thread_screen.start()
    
    
    
     def dataCollector_thread(stocksOnScreenListInfo, stocksOnScreen):
         while(True):
             for stock in stocksOnScreen:
                 stocksOnScreenListInfo.append(RetrieveQuote(stock))
             time.sleep(5)
    
     def screenUpdate_thread(stocksSearchArray, stocksOnScreen):
         while(True):
             stocksOnScreen = UpdateScreen(stocksSearchArray)
    
    
     def UpdateScreen(stocksSearchArray):
         pass
    
     return ["TSLA"]
    

3 Answers 3

3
+25

There are a couple of issues with this function:

def dataCollector_thread(stocksOnScreenListInfo, stocksOnScreen):
    while(True):
        placeholder = []
        for stock in stocksOnScreen:
            placeholer.append(RetrieveQuote(stock))
        stocksOnScreenListInfo = placeholder
        time.sleep(5)
  • you're assigning stocksOnScreenListInfo within this function to a new list placeholder. What you want to do is modify the contents in-place so that stocksOnScreenListInfo in main is updated. You can do that like this: stocksOnScreenListInfo[:] = placeholder (which means change contents from beginning to end with the new list).

  • stocksOnScreen could change while you're iterating it in the for loop since you're updating it in another thread. You should do this atomically. A lock (that you pass as a parameter to the function) will help here: it's a synchronisation primitive that is designed to prevent data races when multiple threads share data and at least one of them modifies it.

I can't see stocksOnScreenListInfo being used anywhere else in your code. Is it used in another function? If so, you should think about having a lock around that.

I would modify the function like this:

def dataCollector_thread(stocksOnScreenListInfo, stocksOnScreen, lock):
    while True:
        placeholder = []
        with lock: # use lock to ensure you atomically access stocksOnScreen
            for stock in stocksOnScreen:
                placeholder.append(RetrieveQuote(stock))
        stocksOnScreenListInfo[:] = placeholder  # modify contents of stocksOnScreenListInfo
        time.sleep(5)

In your other thread function:

def screenUpdate_thread(stocksSearchArray, stocksOnScreen):
     while(True):
         stocksOnScreen = UpdateScreen(stocksSearchArray)

you're assigning stocksOnScreen to a new list within this function; it won't affect stocksOnScreen in main. Again you can do that using the notation stocksOnScreen[:] = new_list. I would lock before before updating stocksOnScreen to ensure your other thread function dataCollector_thread accesses stocksOnScreen atomically like so:

def screenUpdate_thread(stocksSearchArray, stocksOnScreen, lock):
    while True:
        updated_list = UpdateScreen() # build new list - doesn't have to be atomic

        with lock:
            stocksOnScreen[:] = updated_list  # update contents of stocksOnScreen

        time.sleep(0.001)

As you can see I put in a small sleep, otherwise the function will loop constantly and be too CPU-intensive. Plus it will give Python a chance to context switch between your thread functions.

Finally, in main create a lock:

lock = threading.Lock()

and pass lock to both functions as a parameter.

Sign up to request clarification or add additional context in comments.

3 Comments

I wonder if the lock is even necessary anymore with the list[:] = approach (in other words, will it not be blocked by the iteration in the other thread?)
@TomYan There is no such guarantee. Even though Python has a global interpreter lock, there is still a potential race condition when one thread is iterating a list and another thread mutates the same list. It's better to protect against it using a lock.
I just wonder what's the exact definition for python list being thread-safe...
0

stocksOnScreen = ... changes the reference itself. Since the reference is passed to the function/thread as a parameter, the change is done to the copy of the original reference within the function/thread. (Both function/thread have their own copy.)

So instead you should manipulate the list object it refers to (e.g. list.clear() and list.extend()).

However, as you can see, it's now no longer an atomic action. So there are chances that dataCollector_thread would be working on an empty list (i.e. do nothing) and sleep 5 seconds. I provided a possible workaround/solution below as well. Not sure if it is supposed to work (perfectly) though:

def dataCollector_thread(stocksOnScreen):
    while(True):
        sos_copy = stocksOnScreen.copy() # *might* avoid the race?
        for stock in sos_copy:
            print(stock)
        if (len(sos_copy) > 0): # *might* avoid the race?
            time.sleep(5)

def UpdateScreen():
    return ["TSLA"]

def screenUpdate_thread(stocksOnScreen):
    while(True):
        # manipulate the list object instead of changing the reference (copy) in the function
        stocksOnScreen.clear()
        # race condition: dataCollector_thread might work on an empty list and sleep 5 seconds
        stocksOnScreen.extend(UpdateScreen())

def main():
    stocksOnScreen = ["AAPL"] # List of the stocks currently displayed on LED screen

    thread_data_collector = threading.Thread(target=dataCollector_thread, args=(stocksOnScreen,)) # note the comma
    thread_data_collector.daemon = True
    thread_data_collector.start()

    thread_screen = threading.Thread(target=screenUpdate_thread, args=(stocksOnScreen,)) # note the comma
    thread_screen.daemon = True
    thread_screen.start()

Note: according to this answer, python lists are thread-safe, so the copy workaround should work.


You can probably make use of global instead of passing stocksOnScreen as parameter as well:

def dataCollector_thread():
    global stocksOnScreen # superfluous if no re-assignment
    while(True):
        for stock in stocksOnScreen:
            print(stock)
        time.sleep(5)

def UpdateScreen():
    return ["TSLA"]

def screenUpdate_thread():
    global stocksOnScreen # needed for re-assignment
    while(True):
        stocksOnScreen = UpdateScreen()

def main():
    global stocksOnScreen # Or create stocksOnScreen outside main(), which is a function itself
    stocksOnScreen = ["AAPL"] # List of the stocks currently displayed on LED screen

    thread_data_collector = threading.Thread(target=dataCollector_thread)
    thread_data_collector.daemon = True
    thread_data_collector.start()

    thread_screen = threading.Thread(target=screenUpdate_thread)
    thread_screen.daemon = True
    thread_screen.start()

Ref.: https://docs.python.org/3/faq/programming.html#what-are-the-rules-for-local-and-global-variables-in-python

Comments

0

You have three options here since, python like java passes parameters by value & not reference.

First, use a global parameter.

def threadFunction():
    globalParam = "I've ran"

global globalParam
threading.Thread(target=threadFunction)

Second, an Updater Function

def threadFunction(update):
    update("I've ran")

threading.Thread(target=threadFunction, args=((lambda x: print(x)),))

Third, Expose global parameter holder

def threadFunction(param1, param2):
    globalParams[0]= param1 + " Just Got access"

global globalParams
globalParams = ["Param1","Param2"]
threading.Thread(target=threadFunction, args=(*globalParams))

I hope this answered your question ;)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.