Search code examples
pythonkivy

How to get multiprocessing working in Python with Kivy


I do not understand how to combine multiprocessing with Kivy, a Python GUI library, so please help me on this.

Initially, I had created a code using Kivy, a Python GUI library, and threading.Thread, as follows.

#-*- coding: utf-8 -*-

from kivy.lang import Builder
Builder.load_string("""
<TextWidget>:
    BoxLayout:
        orientation: 'vertical'
        size: root.size

        Button:
            id: button1
            text: "start"
            font_size: 48
            on_press: root.buttonClicked()
""")

from kivy.app import App
from kivy.uix.widget import Widget

from kivy.properties import StringProperty 
import threading
import time

class TextWidget(Widget):
    def __init__(self, **kwargs):
        super(TextWidget, self).__init__(**kwargs)
        self.process_test = None
        
    def p_test(self):
        i = 0
        while True:
            print(i)
            i = i + 1
            
            if self.ids.button1.text == "start":
                break

    def buttonClicked(self):
        if self.ids.button1.text == "start":
            self.process_test = threading.Thread(target=self.p_test)
            self.process_test.start()
            self.ids.button1.text = "stop"
        else:
            self.ids.button1.text = "start"
            self.process_test.join()
            

class TestApp(App):
    def __init__(self, **kwargs):
        super(TestApp, self).__init__(**kwargs)
        self.title = 'testApp'

    def build(self):
        return TextWidget()

if __name__ == '__main__':
    TestApp().run()

This code simply displays a single button, and when the button is pressed, it executes a print statement in a while loop.

This code, being a simple example, worked without any issues.

However, as the Kivy GUI definition file became larger, and the CPU processing load of the program running inside the p_test function increased, the program started to become choppy.

According to my machine's task manager, despite having plenty of CPU capacity, it seemed there were limitations to processing everything within a single process.

To circumvent this issue, I decided to use multiprocessing. However, I find the use of multiprocessing complex and hard to understand, so I would like to learn more about how to use it.

First, I replaced the threading.Thread in my earlier code with multiprocessing.Process as follows.

#-*- coding: utf-8 -*-

from kivy.lang import Builder
Builder.load_string("""
<TextWidget>:
    BoxLayout:
        orientation: 'vertical'
        size: root.size

        Button:
            id: button1
            text: "start"
            font_size: 48
            on_press: root.buttonClicked()
""")

from kivy.app import App
from kivy.uix.widget import Widget

from kivy.properties import StringProperty 
import time
from multiprocessing import Process

class TextWidget(Widget):
    def __init__(self, **kwargs):
        super(TextWidget, self).__init__(**kwargs)
        self.process_test = None
        
    def p_test(self):
        i = 0
        while True:
            print(i)
            i = i + 1
            
            if self.ids.button1.text == "start":
                break

    def buttonClicked(self):
        if self.ids.button1.text == "start":
            self.process_test = Process(target=self.p_test, args=())
            self.process_test.start()
            self.ids.button1.text = "stop"
        else:
            self.ids.button1.text = "start"
            self.process_test.join()
            

class TestApp(App):
    def __init__(self, **kwargs):
        super(TestApp, self).__init__(**kwargs)
        self.title = 'testApp'

    def build(self):
        return TextWidget()

if __name__ == '__main__':
    TestApp().run()

Unfortunately, this code did not work correctly and resulted in an error. The error message was as follows.

 Traceback (most recent call last):
   File "<string>", line 1, in <module>
   File "C:\Users\taichi\Documents\Winpython64-3.11.5.0\WPy64-31150\python-3.11.5.amd64\Lib\multiprocessing\spawn.py", line 122, in spawn_main
     exitcode = _main(fd, parent_sentinel)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
   File "C:\Users\taichi\Documents\Winpython64-3.11.5.0\WPy64-31150\python-3.11.5.amd64\Lib\multiprocessing\spawn.py", line 132, in _main
     self = reduction.pickle.load(from_parent)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 EOFError: Ran out of input

I learned that I must use multiprocessing directly under if **name** == '**main** ':. However, this makes it difficult to pass values between Kivy and the multiprocessing code, and I'm not sure how to handle it.

The code I created as a trial is as follows.

#-*- coding: utf-8 -*-

from kivy.lang import Builder
Builder.load_string("""
<TextWidget>:
    BoxLayout:
        orientation: 'vertical'
        size: root.size

        Button:
            id: button1
            text: "start"
            font_size: 48
            on_press: root.buttonClicked()
""")

from kivy.app import App
from kivy.uix.widget import Widget

from kivy.properties import StringProperty 
import threading
import time
from multiprocessing import Process

class TextWidget(Widget):
    def __init__(self, **kwargs):
        super(TextWidget, self).__init__(**kwargs)
        self.process_test = None
                
    def buttonClicked(self):
        if self.ids.button1.text == "start":
            self.ids.button1.text = "stop"
        else:
            self.ids.button1.text = "start"
            

class TestApp(App):
    def __init__(self, **kwargs):
        super(TestApp, self).__init__(**kwargs)
        self.title = 'testApp'

    def build(self):
        return TextWidget()

def p_test(count, array):
    i = 0
    while True:
        print(i)
        i = i + 1

if __name__ == '__main__':
    #shared memory
    count = Value('i', 0)
    array = Array('i', 0)

    process_kivy = Process(target=TestApp().run(), args=[count, array])
    process_kivy.start()
    
    process_test = Process(target=p_test(), args=[count, array])
    process_test.start()
    
    process_kivy.join()
    process_test.join()

I created the above code because I learned that using shared memory allows for data sharing between multiprocessing instances. However, I don't understand how to pass data to a class with shared memory.

I want to set it up so that the while loop starts only when a Kivy's button is pressed, but in reality, the print statement is executed after the Kivy GUI is closed in this code.

Furthermore, since the Kivy program also needs to be launched with multiprocessing, I don't know how to join my own process to itself.

How can I use multiprocessing correctly?

I am using Windows11 and WinPython.


Solution

  • Using the hints in the github response and your latest code, here is a version of your code that does what I think you want:

    from kivy.app import App
    from kivy.uix.widget import Widget
    from multiprocessing import Process, Value
    from kivy.lang import Builder
    
    Builder.load_string("""
    <TextWidget>:
        BoxLayout:
            orientation: 'vertical'
            size: root.size
    
            Button:
                id: button1
                text: "start"
                font_size: 48
                on_press: root.buttonClicked()
            Label:
                id: lab
                text: 'result'
    """)
    
    
    class TextWidget(Widget):
        def __init__(self, **kwargs):
            super(TextWidget, self).__init__(**kwargs)
            self.process_test = None
            self.proc = None
    
            # shared memory
            self.count = Value('i', 0)
    
        def buttonClicked(self):
            if self.ids.button1.text == "start":
                self.proc = start_process(self.count)
                self.ids.button1.text = "stop"
            else:
                if self.proc:
                    self.proc.kill()
                self.ids.button1.text = "start"
                self.ids.lab.text = str(self.count.value)
    
    
    class TestApp(App):
        def __init__(self, **kwargs):
            super(TestApp, self).__init__(**kwargs)
            self.title = 'testApp'
    
        def build(self):
            return TextWidget()
    
    def start_process(count):
        process_test = Process(target=p_test, args=[count], daemon=True)
        process_test.start()
        return process_test
    
    
    def p_test(count):
        i = 0
        while True:
            print(i)
            i = i + 1
            with count.get_lock():
                count.value = i
    
    
    if __name__ == '__main__':
        TestApp().run()
    

    The key point is that the Process creation does not include the self of a kivy widget. I suspect that your code would work if the self was a reference to a simple Python object.