многопроцессорная обработка в python с использованием tkinter

#python #python-3.x #tkinter #multiprocessing

Вопрос:

Я пытаюсь понять многопоточность и пытаюсь выполнить следующий код, но получаю ошибку. Пожалуйста, помогите решить эту проблему.

 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()
 

Следующая ошибка, которую я получаю:

 Exception in Tkinter callback
Traceback (most recent call last):
  File "C:Program FilesPython37libtkinter__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 FilesPython37libmultiprocessingprocess.py", line 112, in start
    self._popen = self._Popen(self)
  File "C:Program FilesPython37libmultiprocessingcontext.py", line 223, in _Popen
    return _default_context.get_context().Process._Popen(process_obj)
  File "C:Program FilesPython37libmultiprocessingcontext.py", line 322, in _Popen
    return Popen(process_obj)
  File "C:Program FilesPython37libmultiprocessingpopen_spawn_win32.py", line 89, in __init__
    reduction.dump(process_obj, to_child)
  File "C:Program FilesPython37libmultiprocessingreduction.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 FilesPython37libmultiprocessingspawn.py", line 105, in spawn_main
    exitcode = _main(fd)
  File "C:Program FilesPython37libmultiprocessingspawn.py", line 115, in _main
    self = reduction.pickle.load(from_parent)
EOFError: Ran out of input
 

Пожалуйста, помогите мне понять, что я делаю не так. Моя цель-запустить индикатор выполнения в окне tkinter с фоновым процессом. Индикатор выполнения должен работать плавно.

Комментарии:

1. Вы не можете передать свое приложение порожденным процессам. Вам придется найти решение, которое обрабатывает приложение в основном процессе и только рабочую нагрузку в новых процессах.

2. Почему вы используете новый процесс вместо нового потока? Это действительно имеет значение, потому что вы должны использовать только 1 поток для всех вызовов tkinter. О многопроцессорной обработке не может быть и речи при работе с tkinter

3. Вы можете перенести вычисления в отдельный поток или процесс, но все действия с графическим интерфейсом должны выполняться в основном потоке основного процесса.

4. Многопроцессорная обработка может работать только тогда, когда в другом процессе абсолютно нет кода tkinter. Объекты Tkinter не могут охватывать границы процесса.

5. @TheLizzard во-первых, о многопроцессорной обработке на самом деле не может быть и речи, вы все еще можете взаимодействовать с процессом, не вызывая tkinter материал из этого процесса (то же самое с потоками), но есть по крайней мере один случай, когда потоковая обработка не будет работать, по крайней мере, в некотором роде. Потоки используют те же ресурсы, что и весь процесс, поэтому, если у вас в основном процессе есть tkinter и поток или несколько потоков, которые потребляют одни и те же ресурсы и делают это много, это может привести к тому, что у tkinter будет меньше этих ресурсов, и это может стать очень медленным, поэтому вы можете распространить это на несколько процессов, у которых есть свои собственные ресурсы

Ответ №1:

Может быть, я неправильно понял что-Л., Но я уверен, что вы спрашивали о многопроцессорных или, по крайней мере, это было в коде, так вот как сделать что в сочетании с tkinter (объяснение в коде комментариев) (и «по совместительству» я имею в виду, что tkinter всегда должно быть в главном процессе, так и в одну нить, чтобы другой вещи, как «расчеты» в этом случае будет перенесен на другие потоки или процессы):

 # 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()
 

Очень важно (предложение, но вам действительно нужно следовать, особенно в этом случае, я видел, как по крайней мере два человека уже допустили эту ошибку при импорте всего из обоих tkinter и tkinter.ttk ):
Я настоятельно не рекомендуем использовать подстановочный знак ( * ) при импорте что-то, вы должны либо импортировать то, что вам нужно, например, from module import Class1, func_1, var_2 и так далее, либо импортировать весь модуль: import module вы также можете использовать псевдоним: import module as md или sth, как это, дело в том, что не все импортное, если вы на самом деле знаете, что Вы делаете; наименование столкновения эту проблему.

ПРАВКА: действительно незначительная вещь, но добавленная daemon=True в Process например, чтобы процесс завершался при выходе из основного процесса (насколько я знаю, на самом деле не лучший способ выхода из процесса (потоки, кстати, имеют то же самое), но не знаю, действительно ли это так плохо, если вообще (я думаю, также зависит от того, что делает процесс, но, например, он может неправильно закрывать файлы или что-то в этом роде, но если вы не записываете в файлы или что-то в этом роде, то в худшем случае, вероятно, будет так же просто, как просто запустить процесс снова, чтобы восстановить потерянный прогресс (в текущем примере процесс может завершиться только резко, если либо окно закрыто, либо вся программа отключается с помощью диспетчера задач или sth)))

EDIT2 (тот же код, просто удалил все комментарии, чтобы сделать его менее неуклюжим):

 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()
 

Если у вас есть еще какие-либо вопросы, задавайте их!