1

I am trying to understand multithreading and trying to execute following code but getting the error. Please help resolve this.

from tkinter import *
from tkinter.ttk import *
import tkinter as tk
import datetime
import multiprocessing

process1 = None


class App:
    def __init__(self):
        self.root = Tk()
        self.top_frame = tk.Frame(self.root, height=50, pady=3)
        self.selectFile = tk.Button(self.top_frame, text="Start", activebackground="blue",
                                    command=lambda: self.create_process()).pack()
        self.progressbar_frame = tk.Frame(self.root)
        self.pgbar = Progressbar(self.progressbar_frame, length=125, orient=HORIZONTAL, mode="indeterminate")
        self.pgbar.pack()

        self.top_frame.pack()
        self.root.mainloop()

    def calculate_data(self):
        a = datetime.datetime.now()
        i = 0
        while i < 100000000:
            i+=1
        print(i)
        b = datetime.datetime.now()
        print(b - a)

    def create_process(self):
        #self.pgbar_start()
        global process1
        process1 = multiprocessing.Process(target=self.calculate_data, args=())
        process2 = multiprocessing.Process(target=self.pgbar_start, args=())
        process1.start()
        process2.start()
        self.periodic_call()

    def pgbar_start(self):
        self.progressbar_frame.pack()
        self.pgbar.start(10)

    def pgbar_stop(self):
        self.pgbar.stop()
        self.progressbar_frame.pack_forget()

    def periodic_call(self):
        if process1.is_alive():
            self.pgbar.after(1000, self.periodic_call)
        else:
            self.pgbar_stop()


if __name__ == "__main__":
    app = App()

Following error I am getting:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Program Files\Python37\lib\tkinter\__init__.py", line 1705, in __call__
    return self.func(*args)
  File "C:/Python Automation/Practice/multi_processing.py", line 15, in <lambda>
    command=lambda: self.create_process()).pack()
  File "C:/Python Automation/Practice/multi_processing.py", line 37, in create_process
    process1.start()
  File "C:\Program Files\Python37\lib\multiprocessing\process.py", line 112, in start
    self._popen = self._Popen(self)
  File "C:\Program Files\Python37\lib\multiprocessing\context.py", line 223, in _Popen
    return _default_context.get_context().Process._Popen(process_obj)
  File "C:\Program Files\Python37\lib\multiprocessing\context.py", line 322, in _Popen
    return Popen(process_obj)
  File "C:\Program Files\Python37\lib\multiprocessing\popen_spawn_win32.py", line 89, in __init__
    reduction.dump(process_obj, to_child)
  File "C:\Program Files\Python37\lib\multiprocessing\reduction.py", line 60, in dump
    ForkingPickler(file, protocol).dump(obj)
TypeError: can't pickle _tkinter.tkapp objects
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "C:\Program Files\Python37\lib\multiprocessing\spawn.py", line 105, in spawn_main
    exitcode = _main(fd)
  File "C:\Program Files\Python37\lib\multiprocessing\spawn.py", line 115, in _main
    self = reduction.pickle.load(from_parent)
EOFError: Ran out of input

Please help me to get the understanding what I am doing wrong. My aim is to run progress bar in the tkinter window with background process. Progress bar should be running smooth.

17
  • You can not pass your application to the spawned processes. You will have to find a solution that handles the application in the main process and the workload alone in the new processes. Commented Aug 14, 2021 at 18:02
  • Why are you using a new process instead of a new thread? It does really matter because you should only use 1 thread for all tkinter calls. Multiprocessing is out of the question when dealing with tkinter Commented Aug 14, 2021 at 18:03
  • 1
    You can throw computing into a separate thread or process, but all GUI stuff must be done in the main thread of the main process. Commented Aug 14, 2021 at 18:10
  • 2
    Multiprocessing can only work when there is absolutely no tkinter code in the other process. Tkinter objects cannot span process boundaries. Commented Aug 14, 2021 at 18:12
  • 1
    @TheLizzard first of multiprocessing is not really out of the question, you can still communicate with the process without having to call tkinter stuff from that process (same with threads) but there is at least one case where threading wouldn't work, at least kinda. Threads use the same resources as the whole process, so if you have in the main process tkinter and a thread or multiple that consume the same resources and do it a lot it may leave tkinter with less of these resources and it may become very laggy, so you can span this thing to multiple processes who have their own resources Commented Aug 14, 2021 at 20:09

1 Answer 1

2

Maybe I misunderstood sth but I am pretty sure you were asking about multiprocessing or at least it was in your code so here is how to do that in combination with tkinter (explanation in code comments) (and by "in combination" I mean that tkinter always has to be in the main process and in one thread so the other stuff like the "calculations" in this case are the ones to be moved to other threads or processes):

# import what is needed and don't ask about threading yet that will be explained
# (pretty useless comment anyways)
from tkinter import Tk, Button, Label, DoubleVar
from tkinter.ttk import Progressbar
from multiprocessing import Process, Queue
from threading import Thread
from queue import Empty
from time import perf_counter, sleep


# create main window also inherit from `Tk` to make the whole thing a bit easier
# because it means that `self` is the actual `Tk` instance
class MainWindow(Tk):
    def __init__(self):
        Tk.__init__(self)
        # prepare the window, some labels are initiated but not put on the screen
        self.geometry('400x200')

        self.btn = Button(
            self, text='Calculate', command=self.start_calc
        )
        self.btn.pack()

        self.info_label = Label(self, text='Calculating...')

        # progressbar stuff
        self.progress_var = DoubleVar(master=self, value=0.0)
        self.progressbar = Progressbar(
            self, orient='horizontal', length=300, variable=self.progress_var
        )

        # create a queue for communication
        self.queue = Queue()

    # the method to launch the whole process and start the progressbar
    def start_calc(self):
        self.info_label.pack()
        self.progressbar.pack()
        Process(target=calculation, args=(self.queue, ), daemon=True).start()
        self.update_progress()

    # this function simply updates the `DoubleVar` instance
    # that is assigned to the Progressbar so basically makes
    # the progressbar move
    def update_progress(self):
        try:
            data = self.queue.get(block=False)
        except Empty:
            pass
        else:
            # if the process has finished stop this whole thing (using `return`)
            if data == 'done':
                self.info_label.config(text='Done')
                self.progress_var.set(100)
                return
            self.progress_var.set(data)
        finally:
            self.after(100, self.update_progress)


# interestingly this function couldn't be a method of the class
# because the class was inheriting from `Tk` (at least I think that is the reason)
# and as mentioned `tkinter` and multiprocessing doesn't go well together
def calculation(queue):
    # here is the threading this is important because the below
    # "calculation" is super quick while the above update loop runs only every
    # 100 ms which means that the Queue will be full and this process finished
    # before the progressbar will show that it is finished
    # so this function in a thread will only put stuff in the queue
    # every 300 ms giving time for the progressbar to update
    # if the calculation takes longer per iteration this part is not necessary
    def update_queue():
        while True:
            sleep(0.3)
            queue.put(i / range_ * 100)  # put in percentage as floating point where 100 is 100%
    # here starting the above function again if the calculations per iteration
    # take more time then it is fine to not use this
    Thread(target=update_queue).start()
    # starts the "calculation"
    start = perf_counter()
    range_ = 100_000_000
    for i in range(range_):
        pass
    finish = perf_counter()
    # put in the "sentinel" value to stop the update
    # and notify that the calculation has finished
    queue.put('done')
    # could actually put the below value in the queue to and
    # handle so that this is show on the `tkinter` window
    print((finish - start))


# very crucial when using multiprocessing always use the `if __name__ == "__main__":` to avoid
# recursion or sth because the new processes rerun this whole thing so it can end pretty badly
# there is sth like a fail safe but remember to use this anyways (a good practice in general)
if __name__ == '__main__':
    # as you can see then inheriting from `Tk` means that this can be done too
    root = MainWindow()
    root.mainloop()

Very Important (suggestion but you really need to follow especially in this case, I have seen at least two people make this mistake already when importing everything from both tkinter and tkinter.ttk):
I strongly advise against using wildcard (*) when importing something, You should either import what You need, e.g. from module import Class1, func_1, var_2 and so on or import the whole module: import module then You can also use an alias: import module as md or sth like that, the point is that don't import everything unless You actually know what You are doing; name clashes are the issue.

EDIT: minor thing really but added daemon=True to the Process instance so that the process gets shut down when the main process exits (as far as I know not really the best way to exit a process (threads btw have the same thing) but don't know if it is really that bad if at all (I guess also depends on what the process does but for example it may not close files properly or sth like that but if you don't write to files or anything then in the worst case it will be probably as easy as just running the process again to regain some lost progress (in the current example the process can only exit abruptly if either the window is closed or the whole program is shut down using task manager or sth)))

EDIT2 (same code just removed all the comments to make it less clunkier):

from tkinter import Tk, Button, Label, DoubleVar
from tkinter.ttk import Progressbar
from multiprocessing import Process, Queue
from threading import Thread
from queue import Empty
from time import perf_counter, sleep


class MainWindow(Tk):
    def __init__(self):
        Tk.__init__(self)

        self.geometry('400x200')

        self.btn = Button(
            self, text='Calculate', command=self.start_calc
        )
        self.btn.pack()

        self.info_label = Label(self, text='Calculating...')

        self.progress_var = DoubleVar(master=self, value=0.0)
        self.progressbar = Progressbar(
            self, orient='horizontal', length=300, variable=self.progress_var
        )

        self.queue = Queue()

    def start_calc(self):
        self.info_label.pack()
        self.progressbar.pack()
        Process(target=calculation, args=(self.queue, ), daemon=True).start()
        self.update_progress()

    def update_progress(self):
        try:
            data = self.queue.get(block=False)
        except Empty:
            pass
        else:
            if data == 'done':
                self.info_label.config(text='Done')
                self.progress_var.set(100)
                return
            self.progress_var.set(data)
        finally:
            self.after(100, self.update_progress)


def calculation(queue):
    def update_queue():
        while True:
            sleep(0.3)
            queue.put(i / range_ * 100)
    Thread(target=update_queue).start()
    start = perf_counter()
    range_ = 100_000_000
    for i in range(range_):
        pass
    finish = perf_counter()
    queue.put('done')
    print((finish - start))


if __name__ == '__main__':
    root = MainWindow()
    root.mainloop()

If you have any other questions, ask them!

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.