Yu_Poke
@Yu_Poke

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

Linuxにてpynputとthreadingを組み合わせるとフリーズします

解決したいこと

Pythonでpynputのキー入力をON/OFFする機構を
検討しているのですが、Linux上でthreadingを使用すると
処理がフリーズします。

解決方法をご教授頂けますでしょうか?

プログラムの説明と発生している問題

tkinterのウィンドウにて
ボタンでpynputの入力受付のON/OFFを行い、
ラベルにて正常にプログラムが動くことの確認として
1秒毎にカウントアップするプログラムです。

コード1の場合はthreadingにてカウントアップを行い、
コード2の場合はtkinterのafter()メソッドにてカウントアップを行っています。

Windows上ではコード1、コード2ともに
正常にpynputの入力受付のON/OFFが行えるのですが、
Linux上ではコード1だとkey_disable()内の
self.key_listener.stop()のタイミングで
プログラムがフリーズします。

コード2はLinux上でも正常に動作するのですが、最終的にはthreadingを用いて描画とは別で動かしたい処理があるため、解決策を模索しております。

コード1

import tkinter as tk
from pynput.keyboard import Key, Listener
import time
from threading import Thread

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.title("ボタンの作成")     # ウィンドウタイトル
        self.master.geometry("200x100")       # ウィンドウサイズ(幅x高さ)
        self.flag = False
        self.button_text = tk.StringVar()
        self.label_text = tk.StringVar()
        self.button_text.set("OFF")
        self.counter = 0
        self.label_text.set(str(self.counter))
        self.key_listener = None
        self.thread = Thread(target=self.counter_thread, args=())
        self.thread_flag = True
        self.thread.start()
        self.master.protocol("WM_DELETE_WINDOW", self.exit)

        #--------------------------------------------------------
        # ボタンの作成
        self.button = tk.Button(self.master,
                                textvariable=self.button_text,
                                command = self.button_click
                                )

        self.button.grid(column='0', row='0', padx='5', pady='5', sticky='ew')
        self.label = tk.Label(self.master, textvariable=self.label_text)
        self.label.grid(column='1', row='0', padx='5', pady='5', sticky='ew')

        #--------------------------------------------------------

    def button_click(self):
        if self.flag == True:
            print("Key入力OFF")
            self.key_disable()
        else:
            print("Key入力ON")
            self.key_enable()

    def counter_inc(self):
        self.counter += 1
        self.label_text.set(str(self.counter))

    def counter_thread(self):
        while self.thread_flag:
            self.counter_inc()
            time.sleep(1)

    def key_enable(self):
        self.flag = True
        if self.key_listener == None:
            self.button_text.set("ON")
            self.key_listener = Listener(on_press=self.on_press,
                                         on_release=self.on_release)
            self.key_listener.start()

    def key_disable(self):
        self.flag = False
        if self.key_listener != None:
            self.button_text.set("OFF")
            self.key_listener.stop()
            self.key_listener = None

    def on_press(self, key):
        print('{0} pressed'.format(key))

    def on_release(self, key):
        print('{0} released'.format(key))

    def exit(self):
        self.thread_flag = False
        self.thread.join()
        self.key_disable()
        self.master.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.key_enable()
    app.mainloop()

コード2

import tkinter as tk
from pynput.keyboard import Key, Listener
import time
from threading import Thread

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.title("ボタンの作成")     # ウィンドウタイトル
        self.master.geometry("200x100")       # ウィンドウサイズ(幅x高さ)
        self.flag = False
        self.button_text = tk.StringVar()
        self.label_text = tk.StringVar()
        self.button_text.set("OFF")
        self.counter = 0
        self.label_text.set(str(self.counter))
        self.key_listener = None
        self.master.protocol("WM_DELETE_WINDOW", self.exit)

        #--------------------------------------------------------
        # ボタンの作成
        self.button = tk.Button(self.master,
                                textvariable=self.button_text,
                                command = self.button_click
                                )

        self.button.grid(column='0', row='0', padx='5', pady='5', sticky='ew')
        self.label = tk.Label(self.master, textvariable=self.label_text)
        self.label.grid(column='1', row='0', padx='5', pady='5', sticky='ew')

        self.master.after(1000, self.counter_after)
        #--------------------------------------------------------

    def button_click(self):
        if self.flag == True:
            print("Key入力OFF")
            self.key_disable()
        else:
            print("Key入力ON")
            self.key_enable()

    def counter_inc(self):
        self.counter += 1
        self.label_text.set(str(self.counter))

    def counter_thread(self):
        while self.thread_flag:
            self.counter_inc()
            time.sleep(1)

    def counter_after(self):
        self.counter_inc()
        self.master.after(1000, self.counter_after)

    def key_enable(self):
        self.flag = True
        if self.key_listener == None:
            self.button_text.set("ON")
            self.key_listener = Listener(on_press=self.on_press,
                                         on_release=self.on_release)
            self.key_listener.start()

    def key_disable(self):
        self.flag = False
        if self.key_listener != None:
            self.button_text.set("OFF")
            self.key_listener.stop()
            self.key_listener = None

    def on_press(self, key):
        print('{0} pressed'.format(key))

    def on_release(self, key):
        print('{0} released'.format(key))

    def exit(self):
        #self.thread_flag = False
        #self.thread.join()
        self.key_disable()
        self.master.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.key_enable()
    app.mainloop()
0

2Answer

私はLinuxユーザーではないし、実行環境がわからないのでこの回答が有効かわからないのですが……。

Pythonインタプリタでは、プログラムが動かせるスレッドの最大値が決まっています。このために、少なくとも私の環境ではthreadingは全く動きません。(設定を調整したり、インタプリタではなく実行ファイル化したりすれば確か動く)

その代わりに、マルチプロセスはインタプリタでも動くはずです。
マルチプロセスに変えてみるか実行環境や設定を見直してみると、もしかしたら解決するかもしれません。

0Like

Comments

  1. @Yu_Poke

    Questioner

    考察、及び助言ありがとうございます。

    質問の際にきちんと動作環境を載せるべきでした。大変申し訳ございません。
    一応WindowsとLinuxの2環境で検証しており、Pythonインタプリタ下でも
    threading、及びpynputはそれぞれ単体では正常に動作しております。
    Windows:
    →Windows 10 Pro on Macbook Pro(Intel(R) Core(TM) i5-6267U CPU,16GB)

    Linux:
    →Debian 1:6.1.63-1+rpt1 on Raspberry Pi 4B 8GB

    しかし、Linuxの場合だと、
    1.pynputとthreadingと同時に起動し、
    (ここまではpynputとthreadingが正常に動作しております)

    2.pynputの入力受付を停止(stopメソッドを実行)したタイミングで
    プログラム(メイン処理とスレッドの処理両方)がフリーズする

    3.その後キー入力を行うと、フリーズが解除される。

    という状況です。
    (フリーズする問題は依然として解決しておりませんが、フリーズの復帰が分かったの記載させて頂きました)

    大変申し訳ないのですが、スレッドが既に動いている状況下にて、プログラムがフリーズするという事象なので最大スレッド数が関係するのかは自分がピンとこない状況です。
    一応検証してみたいので、可能であれば、"設定を調整したり"、"インタプリタではなく実行ファイル化"する方法をご教授頂くことは可能でしょうか?

    また、マルチプロセスというご提案をしてせっかく頂いたのですが、マルチプロセスは共有変数のアクセス周りが複雑になってしまうのと、流用ベースがthreadingであることから、申し訳ございませんが、今のところマルチプロセス化することは検討しておりません。

  2.  正直、Threadingは自分の環境でまともに動かないため本当にうろ覚えで自信がないのですが、
    ・設定というのは、環境設定などではなく、スレッドの最大値を指定するタイプのメソッドやオプション引数の類で設定する、という意味です。申し訳ないですが、実際にあるかどうかわからないです。
    ・実行ファイル化というのは単純に、pyinstallerやnuitkaなどをpipで導入し、コマンドラインからUNIX実行ファイルに変換するという話。これならば実行元がpythonからOSに変わるのでスレッドの最大値云々の話が変わってきた、気がするという曖昧な記憶だよりの話です。

     回答しておいて適当すぎないか、と言われたら反論のしようがないのですが。

     知識がなく推論なことを前提として話すと、
     OSごとで違うとなると、おそらくインタプリタ側というよりはOSやハードウェア側のスレッド制約的なものや、もっと別のバグが原因なのではないかと思います。状況的にも、動いているフェーズと動いていないフェーズがあるようですし。Linuxを動かしているのがラズパイということも関係があるかもしれません。ミドルウェアとハードウェアの相性とかもあると思いますし。

     というか、スレッドの最大値なんて曖昧な表現で書き込んでいるのもちょっと不誠実でしたね。私が言っていたのはグローバルインタプリタロック(GIL)というやつらしいです。

     実際のところ、質問者様と私ではPythonの扱い方や環境が違いすぎるので、これ以上の助言は厳しいですね……。
     私の見当違いの回答で混乱させてしまっていたらすみません。

  3.  長文・連投申し訳ありません。

    「 concurrent.futures.ThreadPoolExecutor offers a higher level interface to push tasks to a background thread without blocking execution of the calling thread, while still being able to retrieve their results when needed.
    queue provides a thread-safe interface for exchanging data between running threads.
    asyncio offers an alternative approach to achieving task level concurrency without requiring the use of multiple operating system threads.」

     代替ライブラリもいくつかあるようですし、そちらを検討するのも手かもしれません。

以下、気になったのでAIに聞いてみた結果です。
参考になれば良いですが……。

原因 1: スレッドの停止タイミングの問題
• pynputはスレッドを内部で使用しているため、スレッドの終了タイミングが競合している可能性があります。
• listener.stop() はリスナーのスレッドを停止する際にブロッキング (待機) 処理が発生し、他のスレッドと競合するとフリーズすることがあります。
対策
• listener.stop() の代わりに listener.running = False を使うと、非同期的に停止処理を行うことが可能です。

原因 2: スレッドの終了待機 (join) に関する問題
• threading.Thread でバックグラウンドスレッドを操作している場合、listener.join() が呼び出されている箇所でフリーズすることがあります。
• 特に threading.Thread を使用しつつ listener を停止しようとすると、スレッド間でデッドロックが発生する場合があります。
対策
• スレッドの終了処理を非ブロッキングにしてみてください。

原因 3: GUI環境やXサーバーとの関連性
• pynput は X サーバー (Linux GUI 環境) を使用することが多く、GUI が正しく設定されていない場合、停止時にデッドロックを起こす可能性があります。
対策
• 実行中の環境がGUIモードであることを確認してください。CLIモードのみで動作している場合は、必要に応じて以下を試します:
sudo apt-get install xserver-xorg xinit

原因 4: ラズパイ上のPythonバージョンと依存関係の不一致
• pynput が使用するバックエンドライブラリと、ラズパイにインストールされている環境が一致していない可能性があります。
対策
• pynputを最新バージョンに更新し、依存関係が適切であることを確認します:
pip install --upgrade pynput

原因 5: イベントループが正しく停止していない
• listener.stop() は内部でイベントループを停止する処理を行っていますが、ラズパイ環境ではイベントループが他のスレッドにブロックされることがあります。
対策
• イベントループを明示的に停止させ、リスナーを終了させるようにします。

0Like

Your answer might help someone💌