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"]
    
Lohith
  • 866
  • 1
  • 9
  • 25
Jane Doe
  • 187
  • 1
  • 12

3 Answers3

3

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.

jignatius
  • 6,304
  • 2
  • 15
  • 30
  • 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?) – Tom Yan Mar 07 '21 at 14:55
  • @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. – jignatius Mar 07 '21 at 22:29
  • I just wonder what's the exact definition for python list being thread-safe... – Tom Yan Mar 08 '21 at 11:43
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

Tom Yan
  • 201
  • 1
  • 6
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 ;)

Ian Elvister
  • 357
  • 3
  • 9