1

Right now I'm using Tkinter as a front-end framework to display data coming in through a queue. This data is then drawn to the screen using my draw() function, etc.

The looping part is using Tkinters after function that calls a specific function after n milliseconds. I have a feeling that when calling destroy or just closing the window, this loop halts? Causing some back-end process to not be satisfied.

I've posted the code below, as you'll be missing the abstract class. Removing the (Updater) and the super within init will satisfy it, as the abstract class is more so like an interface.

Replicating the issue: During run-time of the script, if the Tkinter window is closed by any means available before plotting all the data from the queue onto the plot. The script never returns to command line and is forever stuck. This situation is unfavorable as being able to exit the window and expect the process to be destroyed is what I'm seeking.

Further tracking: Upon failing to pass oscy.run(), the program promptly exits but doesn't return to the command line; as it's seen by running and swiftly exiting the program before completion. This begins to hint towards something happening in init?

from __future__ import division

import logging

import atexit
import matplotlib
import numpy as np
matplotlib.use('TkAgg')

from functools import wraps
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from Tkinter import Scale, Button, Tk, TOP, BOTTOM, BOTH, HORIZONTAL


class Oscilloscope():
    """ Displays the Oscilloscope provided the data streaming in. """
    def __init__(
        self, 
        data_queue,
        closed_callback,
        title="DRP",
        xlabel="Range Cells",
        ylabel="Magnitude"
    ):
        """
        Initialization function for the Osc oscilloscope.
        :param data_queue: List data representative of magnitudes per update.
        :type data_queue: Queue
        :param closed_callback: When the figure is closed, callback should be used to remove the figure.
        :type closed_callback: Function
        :param title: Title of the plot being drawn for the Oscilloscope.
        :type title: String
        :param xlabel: X-axis of the plot being drawn, should either be Range or Doppler.
        :type xlabel: String
        :param ylabel: Y-axis of the plot being drawn, should always be Magnitude.
        :type ylabel: String
        """

        self.data_queue = data_queue
        self.closed_callback = closed_callback

        self.window = Tk()
        atexit.register(self.closed_callback)

        self.title = title
        self.xlabel = xlabel
        self.ylabel = ylabel

        self.y_limits = np.array([0, np.finfo(np.float).eps])      

    def _adjust_ylim_if_req(self, magnitude):
        """
        Changes the limits based on magnitudes. 
        :param magnitude: Size of the magnitude being plotted.
        :type magnitude: Float
        """
        if magnitude < self.y_limits[0]:
            self.y_limits[0] = magnitude
        elif magnitude > self.y_limits[1]:
            self.y_limits[1] = magnitude
        self.ax.set_ylim(self.y_limits[0], self.y_limits[1])

    def draw(self):
        """
        Draws the main line plot.
        """ 
        try:
            magnitudes = self.data_queue.get_nowait()
        except:
            pass
        else:
            # Adjust y limits
            for magnitude in magnitudes:
                self._adjust_ylim_if_req(magnitude)
            # Plot the graph
            self.ax.cla()
            self.ax.set_title(self.title, fontdict={'fontsize': 16, 'fontweight': 'medium'})
            self.ax.set_xlabel(self.xlabel, fontdict={'fontsize': 12, 'fontweight': 'medium'})
            self.ax.set_ylabel(self.ylabel, fontdict={'fontsize': 12, 'fontweight': 'medium'})
            self.ax.plot([n for n in range(len(magnitudes))], magnitudes, '-bo')

    def run(self):
        """
        Sets up and runs the main logic of the Window
        """
        self.plot()
        self.updateplot()
        self.window.mainloop()

    def plot(self):
        """
        Creates the initial base plot
        """
        figure = matplotlib.figure.Figure()
        self.ax = figure.add_subplot(1,1,1)
        self.ax.set_title(self.title, fontdict={'fontsize': 16, 'fontweight': 'medium'})
        self.ax.set_xlabel(self.xlabel, fontdict={'fontsize': 12, 'fontweight': 'medium'})
        self.ax.set_ylabel(self.ylabel, fontdict={'fontsize': 12, 'fontweight': 'medium'})
        self.canvas = FigureCanvasTkAgg(figure, master=self.window)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
        self.canvas._tkcanvas.pack(side=TOP, fill=BOTH, expand=1)

        self.draw()

        def close_fig():
            self.window.destroy()
            self.closed_callback
        button = Button(self.window, text='Close', command=close_fig)
        button.pack()

    def updateplot(self):
        """
        Updates the plot by gathering more data from the Queue.
        """
        print('Start')
        self.draw()
        self.canvas.draw()
        self.window.after(1, self.updateplot)
        print('End')

if __name__ == '__main__':
    from threading import Thread
    from multiprocessing import Queue
    q = Queue()
    for i in xrange(100):
        l = []
        for i in xrange(1000):
            l.append(np.random.randint(0, 100))
        q.put(l)
    def cb():
        print('Closed')
    oscy = Oscilloscope(q, cb)
    oscy.run()

enter image description here

Kyle
  • 793
  • 6
  • 24
  • Can you reduce your example to a [Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example)? – Mike - SMT Oct 22 '19 at 18:27
  • The entire example can actually be ran. I'll provide an updated version without the interface requirement. – Kyle Oct 22 '19 at 18:47
  • Whether or not something can be ran is not the point when it comes to MRE. Writing an MRE can lead you to your own solution and it reduce the problem to its very basic parts so the question and answer can apply more broadly to others who find this post. All the extra stuff out side of the problem only serves to complicate the issue unnecessarily. – Mike - SMT Oct 22 '19 at 18:49
  • All that said do you have an error occurring. Is there a process still running in the background? What exactly is the issue. When I convert your code to python 3 for my use it works as expected and closes fine. – Mike - SMT Oct 22 '19 at 18:54
  • @Mike-SMT I've added an edit that should help clarify the issue. I had forgotten that the issue doesn't occur after all the data has been plotted. – Kyle Oct 22 '19 at 18:59
  • ***I have a feeling that when calling destroy or just closing the window, this loop halts? Causing some back-end process to not be satisfied.*** If you question is if something is not being take care of when you close the application then in this case no. Check out this post: [What is the difference between root.destroy() and root.quit()?](https://stackoverflow.com/questions/2307464/what-is-the-difference-between-root-destroy-and-root-quit) for further context. – Mike - SMT Oct 22 '19 at 19:00
  • So how do we reproduce this problem? How are you able to close prior to plating data? In your code the data is piloted before the window is displayed. We need a "reproducible" example so we can test the issue. I cannot reproduce your issues. My code never gets stuck when I `destroy()` or `quit()` in various section of the code. or when I use the close button. – Mike - SMT Oct 22 '19 at 19:03
  • @Mike-SMT I've actually played around with both but the second answer noting **Calling root.destroy() will destroy all the widgets and exit mainloop. Any code after the call to root.mainloop() will run** seems valid but doesn't answer as to why it's stuck. – Kyle Oct 22 '19 at 19:03
  • Replicating the issue is noted above. Upon running the script, the queue is filled and passed to the Oscilloscope. This then creates the window and begins processing by calling updateplot using window.after. During this time, the user may click 'close' or exit the program, thus causing Tkinter to close but the command prompt can't execute any further commands as it appears Python is still running. – Kyle Oct 22 '19 at 19:06

1 Answers1

0

This was a very strange problem and never would've occurred to me.

Solution

Using multiprocessing.Queue was the problem and to fix this, use regular Queue.

Whats happening

Multiprocessing Queue seems to feed in it's data via a pipe. When the program terminates and the scope of main is lost, this data is still being held, awaiting to be removed. This then causes the "hang" that nothing is happening but something is going on in the background.

How to clear a multiprocessing queue in python

Calling close() before program execution solves the problem. As the multiprocessing module is, in it's self, a Process-based "threading" interface.

Kyle
  • 793
  • 6
  • 24