Home > OS >  Python multiprocessing, how to return variables for a process that can be cancelled
Python multiprocessing, how to return variables for a process that can be cancelled

Time:04-04

I started from a tkinter app with a button starting a specific method from which data could be plotted with matplotlib on the gui.

However I needed to be able to cancel that method, so I changed up the code to use multiprocessing, which worked as expected. The problem is that the plotting is not done anymore.

In the beginning I thought I could just pass the axes as arguments and plot inside the target function, but that didn't give any results, so my next idea was to have the points that i want to plot be returned from the target process and plot them outside. That, I think, would imply not using join(), since I don't want any changes in the process flow. I tried using Queue and Array from multiprocessing but without success, probably i was missing something in the logic.

So my question is, what would be the correct approach between the 2 mentioned, and the correct code extension to what I already have.

The code snippets that are involved:

imports

from tkinter import *
from tkinter import messagebox
from tkinter import ttk
from tkinter import filedialog

from multiprocessing import Process, Value, Manager, Queue
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

main:

if __name__ == "__main__":
    
    root = Tk()
    ParId(root)
    root.mainloop()

the ParId class:

class ParId(Frame):
    def __init__(self, parent, *args, **kwargs):

        Frame.__init__(self, parent, *args, **kwargs)
        self.parent = parent

        frame2 = LabelFrame(self.parent, padx = 5, pady = 1)
        frame2.grid(row=0, column=1, padx = 10, pady = 1)

        frame2.rowconfigure(0, weight = 1)
        frame2.rowconfigure(1, weight = 1)
        frame2.columnconfigure(0,weight = 1)
        frame2.columnconfigure(1,weight = 1)
        
        lf = self.create_left_frame(frame2)
        kct_f = self.create_kct_frame(frame2)

        lf.grid(column=0, row=0, sticky = "W")
        kct_f.grid(row=1, column=0)  

    def create_left_frame(self, container):
        frame = ttk.Frame(container)
        start_button = Button(frame, text = "Continue", state = NORMAL, padx = 50, command = self.start_func, pady = 5)
        start_button.grid(row = 3, column=0, padx=0, pady=10, sticky = "W")
        return frame

    def create_kct_frame(self,container):
        frame = ttk.Frame(container)
        self.figure_kct = plt.Figure(figsize=(4,3), dpi=100)
        self.ax_kct = self.figure_kct.add_subplot(111)
        self.ax_kct.grid()
        kct_plot = FigureCanvasTkAgg(self.figure_kct, frame)
        kct_plot.get_tk_widget().grid(pady = 10, padx = 10, row = 0, column = 0 ,sticky = "N")
        return frame

    # function that starts process
    def start_func(self):
        proc = Process(target=myfoo, args=(various args))
        process_window = ProcessWindow(self, proc)
        process_window.launch()

the ProcessWindow class:

class ProcessWindow(Toplevel):
    def __init__(self, parent, process):
        Toplevel.__init__(self, parent)
        self.parent = parent
        self.process = process

        labl = Label(self, text = "in progress...")        
        terminate_button = ttk.Button(self, text="Cancel", command=self.cancel)

        labl.grid(row=0,column=0)
        terminate_button.grid(row=1, column=0)

    def cancel(self):
        self.process.terminate()
        self.destroy()
        messagebox.showinfo(title="Cancelled", message='Process was terminated')

    def launch(self):
        self.process.start()
        self.after(10, self.isAlive)               
        
    def isAlive(self):
        if self.process.is_alive():                  
            self.after(100, self.isAlive)           
        elif self:
            # Process finished            
            messagebox.showinfo(message="analysis complete", title="Finished")
            self.destroy()

target method:

def myfoo(various arguments):

        all_t = []
        all_v = []

       # rest of the function that populates all_t and all_v, which i want to plot on the main gui

As a summary, I want to plot on the gui(that uses tkinter &matplotlib) some points that are created inside an external function myfoo, but only after the process is finished, without losing the ability to cancel it. I am working on windows, python 3.7. Any insights are helpful.

CodePudding user response:

I'll show the way i solved it in here:

So as Aaron pointed out, i went on and used Queue, passed to both the process and the ProcessWindow class. Whatever was in myfoo was put() in the queue, and only when the process finished i get(), with a timeout, from the Queue, being careful how many times i use the get().

For the actual plotting, i just made a new method in the ParId, that updates the ax and figure, and can call that from the ProcesWindow since ParId is its parent.

I'm not sure if this is the best method, and also if there any any problems if i use the queue like this, memory-wise, if it should be specifically closed/cleared somehow.

from tkinter import *
from tkinter import messagebox
from tkinter import ttk
from tkinter import filedialog

import time
from multiprocessing import Process, Queue
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


class ParId(Frame):
# function that starts process
    
    def __init__(self, parent, *args, **kwargs):
        Frame.__init__(self, parent, *args, **kwargs)
        self.parent = parent

        frame2 = LabelFrame(self.parent, padx = 5, pady = 1)
        frame2.grid(row=0, column=1, padx = 10, pady = 1)

        frame2.rowconfigure(0, weight = 1)
        frame2.rowconfigure(1, weight = 1)
        frame2.columnconfigure(0,weight = 1)
        frame2.columnconfigure(1,weight = 1)
        
        lf = self.create_left_frame(frame2)
        kct_f = self.create_kct_frame(frame2)

        lf.grid(column=0, row=0, sticky = "W")
        kct_f.grid(row=1, column=0)

    def start_func(self):
        self.qu = Queue()
        proc = Process(target=myfoo, args = (self.qu,))
        self.process_window = ProcessWindow(self, proc, self.qu)
        self.process_window.launch()

    def create_left_frame(self, container):
        frame = ttk.Frame(container)
        start_button = Button(frame, text = "Continue", state = NORMAL, padx = 50, command = self.start_func, pady = 5)
        start_button.grid(row = 3, column=0, padx=0, pady=10, sticky = "W")
        return frame

    def create_kct_frame(self,container):
        frame = ttk.Frame(container)
        self.figure_kct = plt.Figure(figsize=(4,3), dpi=100)
        self.ax_kct = self.figure_kct.add_subplot(111)
        self.ax_kct.grid()
        kct_plot = FigureCanvasTkAgg(self.figure_kct, frame)
        kct_plot.get_tk_widget().grid(pady = 10, padx = 10, row = 0, column = 0 ,sticky = "N")
        return frame

    def update_kct_frame(self, all_vel, all_time):
        print("in the update kct")
        self.ax_kct.plot(all_time,all_vel, '*')
        self.figure_kct.canvas.draw()


class ProcessWindow(Toplevel):
    def __init__(self, parent, process, qu):
        Toplevel.__init__(self, parent)
        self.parent = parent
        self.process = process
        self.qu = qu
        
        labl = Label(self, text = "in progress...")        
        terminate_button = ttk.Button(self, text="Cancel", command=self.cancel)
        labl.grid(row=0,column=0)
        terminate_button.grid(row=1, column=0)

    def cancel(self):
        self.process.terminate()
        self.destroy()
        messagebox.showinfo(title="Cancelled", message='Process was terminated')

    def launch(self):
        self.process.start()
        self.after(10, self.isAlive)               
        
    def isAlive(self):
        if self.process.is_alive():                  
            self.after(100, self.isAlive)           
        elif self:
            # Process finished
            self.all_vel = self.qu.get(timeout = 10)
            self.all_time = self.qu.get(timeout = 10)
            print("process finished, v is ", self.all_vel)
            print("process finished, t is", self.all_time)
            self.parent.update_kct_frame(self.all_vel, self.all_time)
            messagebox.showinfo(message="analysis complete", title="Finished")
            self.destroy()


def myfoo(qu):
        all_t = []
        all_v = []

        for i in range(5):
            all_t.append(1000*i)
            all_v.append(10*i)

            time.sleep(1)
            print("i am in the target, appending", all_t[i])

        qu.put(all_v)
        qu.put(all_t)


      
if __name__ == "__main__":
    
    root = Tk()
    ParId(root)
    root.mainloop()
  • Related