Updating a tk ProgressBar from a multiprocess.proc

2019-05-05 06:37发布

问题:

I have successfully created a threading example of a thread which can update a Progressbar as it goes. However doing the same thing with multiprocessing has so far eluded me. I'm beginning to wonder if it is possible to use tkinter in this way. Has anyone done this?

I am running on OS X 10.7. I know from looking around that different OS's may behave very differently, especially with multiprocessing and tkinter.

I have tried a producer which talks directly to the widget, through both namespaces and event.wait, and event.set. I have done the same thing with a producer talking to a consumer which is either a method or function which talks to the widget. All of these things successfully run, but do not update the widget visually. Although I have done a get() on the IntVar the widget is bound to and seen it change, both when using widget.step() and/or widget.set(). I have even tried running a separate tk() instance inside the sub process. Nothing updates the Progressbar.

Here is one of the simpler versions. The sub process is a method on an object that is a wrapper for the Progressbar widget. The tk GUI runs as the main process. I also find it a little odd that the widget does not get destroyed at the end of the loop, which is probably a clue I'm not understanding the implications of.

import multiprocessing
from tkinter import *
from tkinter import ttk
import time

root = Tk()

class main_window:

    def __init__(self):
        self.dialog_count = 0

        self.parent = root
        self.parent.title('multiprocessing progess bar')

        frame = ttk.Labelframe(self.parent)
        frame.pack(pady=10, padx=10)

        btn = ttk.Button(frame, text="Cancel")
        btn.bind("<Button-1>", self.cancel)
        btn.grid(row=0, column=1, pady=10)

        btn = ttk.Button(frame, text="progress_bar")
        btn.bind("<Button-1>", self.pbar)
        btn.grid(row=0, column=2, pady=10)

        self.parent.mainloop()

    def pbar(self, event):

        name="producer %d" % self.dialog_count
        self.dialog_count += 1

        pbar = pbar_dialog(self.parent, title=name)

        event = multiprocessing.Event()
        p = multiprocessing.Process(target=pbar.consumer, args=(None, event))

        p.start()



    def cancel(self, event):
       self.parent.destroy()



class pbar_dialog:

    toplevel=None
    pbar_count = 0

    def __init__(self, parent, ns=None, event=None, title=None, max=100):
        self.ns = ns
        self.pbar_value = IntVar()
        self.max = max

        pbar_dialog.pbar_count += 1
        self.pbar_value.set(0)


        if not pbar_dialog.toplevel:
            pbar_dialog.toplevel= Toplevel(parent)

        self.frame = ttk.Labelframe(pbar_dialog.toplevel, text=title)
        #self.frame.pack()
        self.pbar = ttk.Progressbar(self.frame, length=300, variable=self.pbar_value)
        self.pbar.grid(row=0, column=1, columnspan=2, padx=5, pady=5)

        btn = ttk.Button(self.frame, text="Cancel")
        btn.bind("<Button-1>", self.cancel)
        btn.grid(row=0, column=3, pady=10)
        self.frame.pack()


    def set(self,value):
        self.pbar_value.set(value)

    def step(self,increment=1):
        self.pbar.step(increment)
        print ("Current", self.pbar_value.get())

    def cancel(self, event):
       self.destroy()

    def destroy(self):
        self.frame.destroy()
        pbar_dialog.pbar_count -= 1
        if pbar_dialog.pbar_count == 0:
            pbar_dialog.toplevel.destroy()

    def consumer(self, ns, event): 
        for  i in range(21):
            #event.wait(2)
            self.step(5)
            #self.set(i)
            print("Consumer", i)
        self.destroy()



if __name__ == '__main__':
    main_window()

For contrast, here is the threading version which works perfectly.

import threading
from tkinter import *
from tkinter import ttk
import time

root = Tk()

class main_window:

    def __init__(self):
        self.dialog_count = 0

        self.parent = root
        self.parent.title('multiprocessing progess bar')

        frame = ttk.Labelframe(self.parent)
        frame.pack(pady=10, padx=10)

        btn = ttk.Button(frame, text="Cancel")
        btn.bind("<Button-1>", self.cancel)
        btn.grid(row=0, column=1, pady=10)

        btn = ttk.Button(frame, text="progress_bar")
        btn.bind("<Button-1>", self.pbar)
        btn.grid(row=0, column=2, pady=10)

        self.parent.mainloop()


    def producer(self, pbar):
        i=0
        while i < 101:
            time.sleep(1)
            pbar.step(1)
            i += 1
        pbar.destroy()


    def pbar(self, event):

        name="producer %d" % self.dialog_count
        self.dialog_count += 1

        pbar = pbar_dialog(self.parent, title=name)

        p = threading.Thread(name=name, target=self.producer, args=(pbar,))

        p.start()

        #p.join()


    def cancel(self, event):
       self.parent.destroy()



class pbar_dialog:

    toplevel=None
    pbar_count = 0

    def __init__(self, parent, ns=None, event=None, title=None, max=100):
        self.ns = ns
        self.pbar_value = IntVar()
        self.title = title
        self.max = max

        pbar_dialog.pbar_count += 1

        if not pbar_dialog.toplevel:
            pbar_dialog.toplevel= Toplevel(parent)

        self.frame = ttk.Labelframe(pbar_dialog.toplevel, text=title)
        #self.frame.pack()
        self.pbar = ttk.Progressbar(self.frame, length=300, variable=self.pbar_value)
        self.pbar.grid(row=0, column=1, columnspan=2, padx=5, pady=5)

        btn = ttk.Button(self.frame, text="Cancel")
        btn.bind("<Button-1>", self.cancel)
        btn.grid(row=0, column=3, pady=10)
        self.frame.pack()

        self.set(0)

    def set(self,value):
        self.pbar_value.set(value)

    def step(self,increment=1):
        self.pbar.step(increment)

    def cancel(self, event):
       self.destroy()

    def destroy(self):
        self.frame.destroy()
        pbar_dialog.pbar_count -= 1
        if pbar_dialog.pbar_count == 0:
            pbar_dialog.toplevel.destroy()
            pbar_dialog.toplevel = None

    def automatic(self, ns, event): 
        for i in range(1,100):
            self.step()

if __name__ == '__main__':
    main_window()

回答1:

Doing something similar, I ended up having to use a combination of threads and processes - the GUI front end had two threads: one for tkinter, and one reading from a multiprocessing.Queue and calling gui.update() - then the back-end processes would write updates into that Queue



回答2:

This might be a strange approach, but it works for me. Copy and paste this code to a file and run it to see the result. It's ready to run.

I don't have the patience to explain my code right now, I might edit it another day.

Oh, and this is in Python 2.7 I started programming two months ago, so I have not idea if the difference is relevant.

# -*- coding: utf-8 -*-
# threadsandprocesses.py

# Importing modules
import time
import threading
import multiprocessing
import Tkinter as tki
import ttk


class Master(object):
    def __init__(self):
        self.mainw = tki.Tk()
        self.mainw.protocol("WM_DELETE_WINDOW", self.myclose)
        self.mainw.title("Progressbar")
        self.mainw.geometry('300x100+300+300')
        self.main = tki.Frame(self.mainw)
        self.RunButton = ttk.Button(self.main, text='Run',
                                    command=self.dostuff)
        self.EntryBox = ttk.Entry(self.main)
        self.EntryBox.insert(0, "Enter a number")
        self.progress = ttk.Progressbar(self.main,
                                        mode='determinate', value=0)
        self.main.pack(fill=tki.BOTH, expand=tki.YES)
        self.progress.pack(expand=tki.YES)
        self.EntryBox.pack(expand=tki.YES)
        self.RunButton.pack()
        print "The Master was created"

    def dostuff(self):
        print "The Master does no work himself"
        data = range(int(self.EntryBox.get()))
        S = Slave(self, data)
        print "The Master created a Slave to do his stuff"
        print "The Slave gets told to start his work"
        S.start()

    def myclose(self):
        self.mainw.destroy()
        return

    def nextstep(self):
        print "Good job, Slave, I see the result is"
        print Master.results.get()


class Slave(threading.Thread):
    def __init__(self, guest, data):
        print "This is the Slave."
        print "Nowdays, Work is outsourced!"
        self.data = data
        self.guest = guest
        threading.Thread.__init__(self)

    def run(self):
        print "The Slave is outsourcing his work to Calcualte inc."
        time.sleep(1)
        Outsourcing = Calculate()
        Results = Outsourcing.run(self.guest, self.data)
        return Results


# unwrapping outside a class
def calc(arg, **kwarg):
    return Calculate.calculate(*arg, **kwarg)


class Calculate(object):

    def run(self, guest, data):
        print"This is Calculate inc. ... how can I help you?"
        time.sleep(1)
        maximum = int(guest.EntryBox.get())
        guest.progress.configure(maximum=maximum, value=0)
        manager = multiprocessing.Manager()
        queue = manager.Queue()
        lock = manager.Lock()
        print "Things are setup and good to go"
        # Counting the number of available CPUs in System
        pool_size = multiprocessing.cpu_count()
        print "Your system has %d CPUs" % (pool_size)
        # Creating a pool of processes with the maximal number of CPUs possible
        pool = multiprocessing.Pool(processes=pool_size)
        Master.results = pool.map_async(calc, (zip([self]*len(data), [lock]*len(data),
                                        [queue]*len(data), data)))
        for job in range(1, maximum+1):
            queue.get()  # this is an abuse I think, but works for me
            guest.progress.configure(value=job)
        # Properly close and end all processes, once we're done
        pool.close()
        pool.join()
        print "All done"
        guest.nextstep()
        return

    def calculate(self, lock, queue, indata):
        lock.acquire()
        print 'Reading values and starting work'
        lock.release()
        time.sleep(3)  # some work
        results = indata  # The works results
        lock.acquire()
        print 'Done'
        lock.release()
        queue.put("Finished!")
        return results

if __name__ == '__main__':
    TheMaster = Master()
    TheMaster.mainw.mainloop()