PythonでGUIのボタンを押したときにmultiprocessingを動作させたい


Pythonでのmultiprocessingの仕様について調べているのですが、エラーになってうまくいきません。  
以下の例の実行前に3つをライブラリをpip installしてください。

[code]

    pip install docutils pygments pypiwin32 kivy.deps.sdl2 kivy.deps.glew
    pip install kivy.deps.gstreamer --extra-index-url https://kivy.org/downloads/packages/simple/
    pip install kivy
    
[/code]

以下のように、KivyというUIライブラリを使って、出てきたボタンを押すとプログラムが実行されますがエラーになります。

[code]

    #-*- 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 p_test(self):
            print("Hello")
            print("Sleeping... 3s")
            time.sleep(3)
            print("Good morning")
    
        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.process_test.join()
                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()
    
    if __name__ == '__main__':
        TestApp().run()
    
[/code]

[![画像の説明をここに入力](https://i.stack.imgur.com/9OqZi.png)](https://i.stack.imgur.com/9OqZi.png)

エラー内容は、次のようなものです。

[code]

     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
    
[/code]

[次のようなコード](https://teratail.com/questions/322060)であれば動くようです。

[code]

    import time
    from multiprocessing import Process
    
    # 呼び出したい関数
    def f1(name):
        print("Hello", name)
        print("Sleeping... 3s")
        time.sleep(3)
        print("Good morning", name)
    
    if __name__ == "__main__":
        # サブプロセスを作成します
        p = Process(target=f1, args=("Bob",))
        # 開始します
        p.start()
        print("Process started.")
        # サブプロセス終了まで待ちます
        p.join()
        print("Process joined.")
    
[/code]

リンク元によると、multiprocessingは、`if __name__ ==
"__main__":`から呼び出さないと動かないと書かれていましたが、GUIからmultiprocessingを呼び出したい場合にはどうすれば良いのでしょうか？

threading.threadだと、パフォーマンスがカクカクしてしまうので別のプロセスから呼び出したいと考えています。GUIから別プロセスを呼び出すことは可能なのでしょうか？

使用環境は、Windows11です。




実際には、エラーは既に別の物が発生していて、質問に書かれているのは二次的なエラーだと思われます。

実行したら先に以下のようなエラー表示が行われていました。  
これの前後にkivyの動作状況ログのメッセージがあり、最後に質問に書かれたエラーメッセージが表示され、中断待ち状態になります。

[code]

     Traceback (most recent call last):
       File "C:\Develop\Python\vKV\kivysample.py", line 56, in <module>
         TestApp().run()
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\app.py", line 956, in run
         runTouchApp()
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\base.py", line 574, in runTouchApp
         EventLoop.mainloop()
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\base.py", line 339, in mainloop
         self.idle()
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\base.py", line 383, in idle
         self.dispatch_input()
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\base.py", line 334, in dispatch_input
         post_dispatch_input(*pop(0))
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\base.py", line 263, in post_dispatch_input
         listener.dispatch('on_motion', etype, me)
       File "kivy\_event.pyx", line 731, in kivy._event.EventDispatcher.dispatch
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\core\window\__init__.py", line 1691, in on_motion
         self.dispatch('on_touch_down', me)
       File "kivy\_event.pyx", line 731, in kivy._event.EventDispatcher.dispatch
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\core\window\__init__.py", line 1708, in on_touch_down
         if w.dispatch('on_touch_down', touch):
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       File "kivy\_event.pyx", line 731, in kivy._event.EventDispatcher.dispatch
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\uix\widget.py", line 589, in on_touch_down
         if child.dispatch('on_touch_down', touch):
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       File "kivy\_event.pyx", line 731, in kivy._event.EventDispatcher.dispatch
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\uix\widget.py", line 589, in on_touch_down
         if child.dispatch('on_touch_down', touch):
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       File "kivy\_event.pyx", line 731, in kivy._event.EventDispatcher.dispatch
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\uix\behaviors\button.py", line 151, in on_touch_down
         self.dispatch('on_press')
       File "kivy\_event.pyx", line 727, in kivy._event.EventDispatcher.dispatch
       File "kivy\_event.pyx", line 1307, in kivy._event.EventObservers.dispatch
       File "kivy\_event.pyx", line 1191, in kivy._event.EventObservers._dispatch
       File "C:\Develop\Python\vKV\Lib\site-packages\kivy\lang\builder.py", line 55, in custom_callback
         exec(__kvlang__.co_value, idmap)
       File "<string>", line 11, in <module>
       File "C:\Develop\Python\vKV\kivysample.py", line 40, in buttonClicked
         self.process_test.start()
       File "C:\Users\{username}\AppData\Local\Programs\Python\Python311\Lib\multiprocessing\process.py", line 121, in start
         self._popen = self._Popen(self)
                       ^^^^^^^^^^^^^^^^^
       File "C:\Users\{username}\AppData\Local\Programs\Python\Python311\Lib\multiprocessing\context.py", line 224, in _Popen
         return _default_context.get_context().Process._Popen(process_obj)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       File "C:\Users\{username}\AppData\Local\Programs\Python\Python311\Lib\multiprocessing\context.py", line 336, in _Popen
         return Popen(process_obj)
                ^^^^^^^^^^^^^^^^^^
       File "C:\Users\{username}\AppData\Local\Programs\Python\Python311\Lib\multiprocessing\popen_spawn_win32.py", line 95, in __init__
         reduction.dump(process_obj, to_child)
       File "C:\Users\{username}\AppData\Local\Programs\Python\Python311\Lib\multiprocessing\reduction.py", line 60, in dump
         ForkingPickler(file, protocol).dump(obj)
       File "stringsource", line 2, in kivy._event.EventDispatcher.__reduce_cython__
     TypeError: no default __reduce__ due to non-trivial __cinit__
    
[/code]

* * *

原因としてはこちらの記事の回答が当てはまるのでしょう。oririさんの回答もそれと同じことでしょう。  
[Can't pickle <type 'instancemethod'> when using multiprocessing
Pool.map()](https://stackoverflow.com/q/1816958/9014308)

> **The problem is that multiprocessing must pickle things to sling them among
> processes, and bound methods are not picklable.** The workaround (whether
> you consider it "easy" or not;-) is to add the infrastructure to your
> program to allow such methods to be pickled, registering it with the
> copy_reg standard library method.

> **問題は、マルチプロセッシングではプロセス間で処理するために物事をピクル化する必要があり、バインドされたメソッドはピクル化できないことです。**
> 回避策は (「簡単」だと思うかどうかは関係ありませんが)、そのようなメソッドをピクル化できるインフラストラクチャをプログラムに追加し、それを
> copy_reg 標準ライブラリ メソッドに登録することです。

太字は回答者による引用の強調。  
上記文章や他の回答を見ると「回避策」はあるようですので、その方法にしたいのなら記事を読み解いてみてください。

単純な回避策は oriri さんの回答に示されたように、`def p_test(self):`メソッドを`class
TextWidget(Widget):`の外に追い出して普通の関数にすることでしょう。  
その場合、`Process(target=self.p_test,
args=())`の部分は`self.`の無い`Process(target=p_test, args=())`になるでしょう。  
また今の`def
p_test(self):`は`self`パラメータを使っていないので有っても無くても問題はないですが、普通の関数化した場合にはインスタンスである`self`に関する何かは(単純には)使えないでしょう。




> GUIから別プロセスを呼び出すことは可能なのでしょうか？

並列処理 (parallel processing: パラレルプロセッシング) を行う場合, マルチスレッド or マルチプロセス (multi-
process) が選択肢としてあり,  
マルチスレッドは同一プロセス空間を共有, マルチプロセスはそれぞれ別のプロセス空間で走ります

GUIライブラリー (or GUIフレームワーク)は一つのプロセスとして走るので  
例えば, ボタンだけ別プロセスで という訳にはいきません  
(ボタンだけ表示する(単独動作可能な) GUIアプリを作成し, 複数の GUIアプリを同時にという意味なら可能)

質問のコードは, 別プロセスとして動作する部分が **GUIにかかわらない** ため, それならば呼び出し可能なはず

ただし, `Widget`クラスを継承した (GUI関連の)クラスのメソッドを呼び出すのではなく  
**普通の関数を** 用意し, それを別プロセスとして呼び出すほうがよいでしょう

`self.p_test()` (`TextWidget()`のインスタンス) では, メンバーの内容は別空間であるため,  
片方で内容を設定しても他方には反映されず, トラブルのもとになる可能性が高いからです

