Henry Gridley
Henry Gridley

Reputation: 43

Python multiprocessing asynchronous callback

I'm writing a program with a GUI using TKinter, in which the user can click a button and a new process is started to perform work using multiprocess.Process. This is necessary so the GUI can still be used while the work is being done, which can take several seconds.

The GUI also has a text box where the status of the program is displayed when things happen. This is often straight forward, with each function calling an add_text() function which just prints text in the text box. However, when add_text() is called in the separate process, the text does not end up in the text box.

I've thought about using a Pipe or Queue, but that would require using some sort of loop to check if anything has been returned from the process and that would also cause the main (GUI) process to be unusable. Is there some way to call a function in one process that will do work in another?

Here's an simple example of what I'm trying to do

import time
import multiprocessing as mp
import tkinter as tk

textbox = tk.Text()

def add_text(text):
  # Insert text into textbox
  textbox.insert(tk.END, text)

def worker():
  x = 0
  while x < 10:
    add_text('Sleeping for {0} seconds'.format(x)
    x += 1
    time.sleep(1)

proc = mp.Process(target=worker)

# Usually happens on a button click
proc.start()

# GUI should still be usable here

Upvotes: 0

Views: 438

Answers (2)

Henry Gridley
Henry Gridley

Reputation: 43

I ended up using a multiprocessing.Pipe by using TKinter's after() method to perform the looping. It loops on an interval and checks the pipe to see if there's any messages from the thread, and if so it inserts them into the text box.

import tkinter
import multiprocessing


def do_something(child_conn):
  while True:
    child_conn.send('Status text\n')

class Window:

  def __init__(self):
    self.root = tkinter.Tk()
    self.textbox = tkinter.Text()
    self.parent_conn, child_conn = multiprocessing.Pipe()
    self.process = multiprocessing.Process(target=do_something, args=(child_conn,))

  def start(self):
    self.get_status_updates()
    self.process.start()
    self.root.mainloop()

  def get_status_updates()
    status = self.check_pipe()
    if status:
      self.textbox.add_text(status)
    self.root.after(500, self.get_status_updates) # loop every 500ms

  def check_pipe():
    if self.parent_conn.poll():
      status = self.parent_conn.recv()
      return status

    return None

Upvotes: 0

leotrubach
leotrubach

Reputation: 1597

The asyncronous things actually require loop. You could attach function to the TkInter's loop by using Tk.after() method.

import Tkinter as tk

class App():
    def __init__(self):
        self.root = tk.Tk()
        self.check_processes()
        self.root.mainloop()

    def check_processes(self):
        if process_finished:
            do_something()
        else:
            do_something_else()
        self.after(1000, check_processes)


app=App()

Upvotes: 1

Related Questions