0

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.

Vinkesh Shah
  • 127
  • 1
  • 1
  • 11
  • 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. – Klaus D. Aug 14 '21 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 – TheLizzard Aug 14 '21 at 18:03
  • I have tried in thread also but in that my progress bar is not running smoothly. It get start then stuck then start like that. So, I though of implementing multiprocessing which might resolve my issue. – Vinkesh Shah Aug 14 '21 at 18:06
  • I can't think of any reason for why threading wouldn't work but multiprocessing will work. I have never had to use `multiprocessing` for any of my projects. – TheLizzard Aug 14 '21 at 18:08
  • 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. – Tim Roberts Aug 14 '21 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. – Bryan Oakley Aug 14 '21 at 18:12
  • Look at [this](https://stackoverflow.com/a/67004527/11106801). It's a way of making a `tkinter` progress bar in another thread. This will only work if you have no other `tkinter` windows open yet. – TheLizzard Aug 14 '21 at 18:15
  • cmon people, you can get into massive trouble if you do what you are doing right now in terms of debugging. DON'T IMPORT EVERYTHING. more explanation: 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. and this is a prime example of why not – Matiiss Aug 14 '21 at 18:59
  • multiprocessing dosent share the same memory space, threads do. So stick with your question on threads and forget about multiprocessing here. Please take a toturial on threads and keep in mind that tkinter uses a loop while the application is alive. So dont interrupt it. – Thingamabobs Aug 14 '21 at 19:21
  • 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 – Matiiss Aug 14 '21 at 20:09
  • @Matiiss What you are saying depends on the OS. The time given for each process to use the CPU is dictated by the OS. So which OS are you talking about and can you please give me a source. I don't know much about this and want to learn more. – TheLizzard Aug 14 '21 at 20:17
  • Honestly my only source is [this tutorial by Corey Schafer](https://www.youtube.com/watch?v=fKl2JW_qrso) also this video I watched to learn about the differences: [Asynchronous vs Multithreading and Multiprocessing Programming (The Main Difference)](https://www.youtube.com/watch?v=0vFgKr5bjWI) (the part about threading doesn't much affect python maybe because you don't need to manually assign memory). The rest is just searching google to find examples and stuff and how to communicate with processes and stuff. The part about resources is partially somewhere I have read and partially my logic – Matiiss Aug 14 '21 at 20:21
  • @Matiiss Tried looking a bit deeper into it and got a lot of conflicting [answers](https://stackoverflow.com/q/3044580/11106801). Some say new threads are better some say new processes are better. From what I know (which isn't much), I am inclined to believe [this](https://stackoverflow.com/a/44322494/11106801) the most. So basically if you are using `numpy`/`networking`/`io`, using threads are much better. For the rest, I am not sure. – TheLizzard Aug 14 '21 at 20:32
  • @TheLizzard from that specific answer I more of understood that performance increases when using threads with C extensions or sth but that doesn't mean it is better, it may be that the same thing happens with multiprocessing (the perfomance increases), I think it still depends on what you need to do mostly and threads in my experience too are usually sufficient (and easier to run (except that even with Threads I usually use `Queue`s)) – Matiiss Aug 14 '21 at 20:55
  • @TheLizzard the only real case I think I have had so far was when I was trying to make the video player in `tkinter` (from when I have understood that `tkinter` is simply too slow), I think it would have really improved performance if I could have prepared Labels in another process because even with threading it was just too slow (my guess is because threads were using the same resources as the process therefore slowing down the whole `tkinter` update perfomance but idk), the easiest currently is to maybe run some tests or sth but I think this would be better to compare in a low-level language – Matiiss Aug 14 '21 at 20:58
  • @Matiiss I never use `Queue`s. I just use normal python `list`s. Also processes take longer to start, and to communicate to. It is possible for the communication to become a bottle neck. And it also depends on what python you are using. Also why were you using different `Label`s? I used a canvas with `.itemconfigure(image=...)` to change the image. And no the problem is that `tkinter` is just slow. I got `x4` performance when I switched from `tkinter` to `pygame`. – TheLizzard Aug 14 '21 at 21:00
  • @TheLizzard I never thought about using canvas but I did notice that it was faster to place an initialized widget than using `.config(image=...)` (way faster), and I kinda mentioned in my comment that here I realized that `tkinter` is too slow for this (probably because it is old and it has its own interpreter). Don't know about the processes taking longer to start since I have noticed that `.start()` for Process at least finishes faster than Thread (don't know how long it takes behind the scenes) too long of a discussion we have here tho (about stuff we don't even know that well) – Matiiss Aug 14 '21 at 21:10

1 Answers1

1

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!

Matiiss
  • 5,970
  • 2
  • 12
  • 29