0

As suggested over here, I have created a separate class, exclusively for Tkinter. The main function just gets the scale values from the Tkinter GUI. Though I'm able to get these scale values, I am unable to exit the program after closing the GUI. My program just seems to be stuck after self.root.mainloop() i.e print "mainloop" gets executed. I do not have any problems in terminating the script, if I do something as follows, i.e, if I do not access the output_q values

i = 0
while app.run:
 print i
 i = i+1

The entire code is given below

from Tkinter import *
import threading, time, sys, Queue

class App(threading.Thread):

    def __init__(self, var):
        threading.Thread.__init__(self) 
        self.output_q = Queue.Queue()      
        self.start()

    def callback(self):
        self.run = 0
        self.root.quit()
        self.root.destroy()

    def pub_y(self, val_y):  
        self.x_val = float(self.y_scale.get())
        self.output_q.put((self.x_val, 2, 3)) 

    def run(self):

        self.root = Tk()
        self.root.protocol("WM_DELETE_WINDOW", self.callback)

        self.y_var = DoubleVar()             
        self.y_scale = Scale( self.root, from_=0, to=1, length=300, label="yaw", resolution=0.0000000000001, variable = self.y_var, orient=HORIZONTAL, command=self.pub_y)
        self.y_scale.set(0.5)
        self.y_scale.pack(anchor=CENTER)

        label = Label(self.root, text="Hello World")
        label.pack()

        self.root.mainloop()
        print "mainloop"

var = 1
input = Queue.Queue()
input.put((1,2,3))
app = App(input)
i = 0  
while app.run:
 result = app.output_q.get()
 print result[0]

sys.exit(0)

Could someone point out where I might be going wrong? Thanks!

Surabhi Verma
  • 108
  • 1
  • 2
  • 11

1 Answers1

1

There are multiple things about your code that are confusing but not actually wrong, like the fact that you shadow your run method with an instance attribute with the value 0…

The fact that you’re using tkinter from a background thread actually is wrong, and can lead to a hang on some platforms. But it probably isn’t the cause here.

Your actual problem is the queue logic. Think about how it works:

  • Your main thread checks whether the thread is running.
  • Your main thread calls output_q.get(), which blocks forever until something gets put on the queue.
  • Your background thread gets a destroy event.
  • Your background thread sets run = 0 and exits.
  • Your main thread is still waiting on the queue forever, so it will never see that run is now falsely, so it will never exit.

One way to solve this is to use the same queue that you’re already using to wake up the main thread so it can see that you’ve quit:

def callback(self):
    self.run = 0
    self.root.quit()
    self.root.destroy()
    self.output_q.put(None)

Of course now you can’t just use result[0], because result could be None, so you need to add:

if result is not None:

But there’s still a problem: variables that you change in one thread are guaranteed to eventually be visible to other threads, but not immediately. What happens if the main thread doesn’t see the change until after its gone to block on the queue again? It’s stuck forever.

I’m pretty sure this will work on CPython, but that isn’t guaranteed, it just happens to be true by accident, and it may not work on a different implementation like Jython. The right way to fix this is to synchronize all access to any variables you want to share between threads by using a Lock or Condition or Queue or other sync object.

But notice that you don’t actually need the while app.run: test anymore, because you can just do this:

while True:
    result = app.output_q.get()
    if result is None:
        break
    print result[0]

And then you don’t have to worry about races. Your code is simpler, and guaranteed correct, this way.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • @abarnet Thank you for your detailed reply. I agree, `self.run = 0` is not needed. What do you mean by background thread? I haven't used queues before so I might be wrong, but the `while app.run:` condition is being checked right? So, if this value is false in another thread, it should automatically exit the loop. Can you tell me about the 'sync object'? – Surabhi Verma Sep 06 '18 at 03:36
  • Btw, I made the changes as mentioned and it gave me the following error while terminating, `Exception RuntimeError: 'main thread is not in main loop' in > ignored Tcl_AsyncDelete: async handler deleted by the wrong thread Aborted (core dumped)` – Surabhi Verma Sep 06 '18 at 03:37
  • I followed [this](https://stackoverflow.com/a/1835036/8417107). Doesn't seem to have a problem. – Surabhi Verma Sep 06 '18 at 03:44
  • I also ran the [same](https://stackoverflow.com/questions/459083/how-do-you-run-your-own-code-alongside-tkinters-event-loop/1835036#1835036) code on my system without any errors. So it is not a platform issue but some problem in the code itself. – Surabhi Verma Sep 06 '18 at 04:09
  • What if I use `sys.exit(0)` in the callback function? In that case, the script should exit right in the callback itself right? Irrespective of `result = app.output_q.get()` – Surabhi Verma Sep 06 '18 at 04:16
  • @SurabhiVerma The `while app.run:` condition is being checked at the start of the loop. If you’re stuck forever in the middle of the loop, waiting for a `Queue.get` that will never return, you never get to the start of the loop again, so it never gets checked again. – abarnert Sep 06 '18 at 15:37
  • @SurabhiVerma A “sync object” is something two threads can synchronize on, so you can control things like “thread 2 will not get past here until thread 1 gets there” or “no two threads can be modifying this value at the same time” or “this modification happens before that one”. – abarnert Sep 06 '18 at 15:38
  • @SurabhiVerma The exception on exiting is caused by running Tkinter on the background thread instead of the main thread. As I said, that’s illegal, and it causes all kinds of problems. The fact that on your platform you happen to get away with it except for an exception on shutdown is just a coincidence. I could explain the details if you knew more about threading and about Tcl, but it would be pointless, because the correct answer is the same one you already know: don’t run Tkinter on the background thread. – abarnert Sep 06 '18 at 15:40
  • I also have to use ROS in my main thread. I want to accomplish [this](https://stackoverflow.com/questions/52144361/tcl-asyncdelete-error-unable-to-terminate-tk/52147213#52147213) ultimately. But as suggested in the answer, I need to keep tk and ros in separate threads. – Surabhi Verma Sep 06 '18 at 23:43