【Tkinterの使い方】”通信処理”実行時の注意点と解決策(アプリが反応しない)

Tkinterと通信併用時の注意点の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Tkinter と通信を併用する場合の注意点について解説していきます。

通信には様々なものが存在しますが、このページでは「通信 = ソケット通信」として解説を行っていきます。ただ、他の通信を利用する場合でも同様の問題が発生しますし、このページで紹介する解決策で同様に解決可能です。

ちなみに、Python でのソケット通信については下記ページで解説していますので、ソケット通信について詳しく知りたい方は別途下記ページを参照していただければと思います。

Pythonでのソケット通信(ポート番号・プロトコル・サーバー / クライアント)

Tkinter と通信の併用時の注意点

このページで解説しようとしている注意点とは、「受信待ちを行っている間、アプリが反応しなくなる」という点になります。

これは、結局は下記ページで解説している mainloop に関するもので、単純に受信待ちを実行してしまうと mainloop に処理が戻らなくなり、イベントが発生してもイベントハンドラーが実行されなくなるよーという注意点になります。

tkinterのmainloopの解説ページのアイキャッチ 【Python】tkinterのmainloopについて解説

mainloop が実行されないとアプリが反応しない

もう少し詳細を解説していきます。

Tkinter において、mainloop は非常に重要な役割を持つメソッドです。この mainloop が実行されている間、特定のイベントが発生した際にイベントハンドラーが実行されるようになっています。このイベントやイベントハンドラーに関しては下記ページで解説していますので、ご存じない方は下記ページを参照していただければと思います。

イベント処理解説ページのアイキャッチ Tkinterの使い方:イベント処理を行う

例えば、ボタンのクリックというイベントに対して click_button という関数をイベントハンドラーとして登録しておけば、ボタンクリック時に click_button という関数が自動的に実行されるようになります。この仕組みは単純で、mainloop メソッドの中でイベント発生を監視するようになっており、イベントの発生を検知した際に mainloop からイベントハンドラーを呼び出すようになっているだけです。

Tkinterでのイベントハンドラーが実行される仕組みを説明する図

また、開発者が登録したイベント・イベントハンドラーだけでなく、デフォルトで登録されているイベント・イベントハンドラーも存在しており、例えばウィンドウの最大化ボタンが押された時にウィンドウを最大化するような処理も、この mainloop やイベントの仕組みで実現されています。

で、この仕組みからも分かるように、mainloop が実行されていない間はイベントが発生してもイベントハンドラーは実行されないことになります。なので、例えば mainloop が実行されていない間に Tkinter で開発したアプリの最大化ボタンをクリックしてもアプリは反応しません。他のイベントに関しても同様です。要は、操作しても何も反応しないアプリとなってしまいます。

mainloopが実行されていないアプリを操作しても反応しない様子

このような仕組みで Tkinter で開発したアプリは動作するので、反応性の高いアプリを開発するためには、できるだけ常に mainloop が実行されている状態にしておくことが重要となります。

スポンサーリンク

「受信待ち」によってアプリが反応しなくなる可能性がある

では、ここまで説明してきた mainloop やイベントハンドラーが実行される仕組みと通信処理とにどういう関係があるのでしょうか?

次は、この点について説明していきます。

実は、mainloop と通信処理に直接関係性があるというわけではありません。ですが、通信処理を行うと mainloop が実行されない状態が長くなる可能性があるという点に注意が必要です。特に通信ではデータの受信待ちを行うような場合があり、この受信待ちを行っている間 mainloop が実行されなくなる可能性があります。

もう少し具体的に説明をしていきましょう!

例えば、アプリにボタンを用意し、そのボタンが実行された際には「通信相手のプログラムにソケット通信でデータを送信し、さらに、その通信相手から返事となるデータの受信を行う」という処理が実行されるようにアプリを開発したとしましょう。このようなアプリは、ボタンクリック時のイベントハンドラーとして上記の括弧内の処理を実行する関数を登録しておくことで実現できます。

実現しようとするアプリのイベント発生時の処理

ただ、ここでポイントになるのが、そのイベントハンドラーが実行されている間は mainloop が実行されていない状態になるという点になります。イベントハンドラーは mainloop から呼び出される形で実行され、通常の関数同様に、return したり最後まで処理が終了しないと呼び出し元である mainloop に処理が戻りません。つまり、イベントハンドラー実行中は一時的にアプリが反応しない状態となります。

イベントハンドラーが実行されている間はmainloopが実行されていない状態となることを説明する図

で、このイベントハンドラーがすぐに処理を終了すれば良いのですが、通信でデータの受信を行う際には「受信待ち」状態になる可能性があります。上記の例であれば、通信相手が他の処理で忙しくて返事となるデータを送信する処理が遅れる可能性があり、その遅れている間は受信待ちとなります。

MEMO

ソケット通信の場合はソケットを受信待ちしないように設定することも可能です(ノンブロック)

ただ、このページではそのような設定は行わず、”データが受信できるまでの間は受信待ち状態” となることを前提として解説を行います

そして、この受信待ちはイベントハンドラー内で実行されることになるため、この受信待ちを行っている間は他のイベントが発生してもイベントハンドラーが実行されません。これは、前述の通り、イベントハンドラーが実行されている間は mainloop が実行されていない状態となるためです。

通信相手のプログラムからのデータの受信待ちの間mainloopが実行されない様子

受信待ちを行っている間はユーザーがアプリを操作しようとしても反応しないため、ユーザーからは「アプリが停止した」「パソコンが壊れた?!」「アプリの反応が悪くてイライラする」などの印象を持たれてしまうことになります…。

受信待ちを行なっているアプリが操作しても反応しないため、ユーザーがイライラしている様子

ここまでの説明からも分かるように、アプリが反応しなくなるのは通信処理を行うこと自体が原因なのではなく、イベントハンドラー内で「待ち」が発生してしまうことが原因です。他の処理を行って「待ち」が発生することも当然ありますが、通信処理では通信相手の状況によっては待たされる可能性があり、上記のような問題が発生することが多いため、特に注意が必要だと思います。もちろん、同様の理由でイベントハンドラー内で重い処理・時間のかかる処理を実行させることも避ける必要があります。

アプリの反応が無くなるアプリのサンプルスクリプト

ここまで文章や図のみを用いた説明を行ってきましたので、ここでイベントハンドラーで受信処理を行うことで上記のような問題が発生するサンプルスクリプトを紹介しておきたいと思います。

ここで紹介するスクリプトはクライアントサーバーモデルのもので、クライアントを Tkinter を利用して開発します。このクライアントではボタンクリック時にサーバーに対して b'World!' を送信し、さらにサーバーから返事となるデータの受信を行う作りとしています。

サーバーは Tkinter を利用せず、単にクライアントからデータを受信し、さらに b'World!' をクライアントに返却するプログラムとしています。

紹介するスクリプトにおけるクライアントとサーバー間の通信の流れを説明する図

サーバー

まず、サーバー側のサンプルスクリプトを示します。

server.py
import socket
import time

# ソケットを生成(UDP通信)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 受信待ちするIPアドレスとポート番号の設定
sock.bind(('0.0.0.0', 40001))

while True:
    # クライアントからのデータ受信
    recv_data, addr = sock.recvfrom(1024)

    # 10秒待つ(サーバーが忙しいことを模擬)
    time.sleep(10)

    # クライアントにデータを送信
    sock.sendto(b'World!', addr)

sock.close()

上記は UDP 通信を行うサーバーのスクリプトであり、基本的には、クライアントからデータを受信し、その後クライアントに対して b'World!' というデータを送信するだけのプログラムになっています。ただ、クライアントへのデータの送信が遅くなるように、つまりクライアントでの受信待ちが長くなるように 、データの送信前に sleep10 秒間スリープするようにしています。

UDP 通信についてや、UDP 通信を行うサーバーの作り方等について知りたい方は下記ページを参照していただければと思います。

Pythonでのソケット通信(ポート番号・プロトコル・サーバー / クライアント)

クライアント

サーバーの通信相手となるクライアントのサンプルスクリプトは下記のようになります。

client.py
import tkinter
import socket

# ソケットを生成(UDP通信)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

def click_button():
    # サーバーにデータを送信
    sock.sendto(b'Hello', ('127.0.0.1', 40001))

    # サーバーからデータを受信
    recv_data = sock.recv(1024)

    # 受信したデータを文字列としてラベルに表示
    label.config(text=recv_data.decode())

# アプリを作成
app = tkinter.Tk()

# ボタンの生成と配置
button = tkinter.Button(
    app,
    text="送信",
    font=('', 40),
    command=click_button # イベントハンドラーの登録
)
button.pack(padx=10, pady=10)

# ラベルの生成と配置
label = tkinter.Label(
    app,
    text='',
    font=('', 40),
)
label.pack(padx=10, pady=10)

# メインループでイベント監視
app.mainloop()

# アプリ終了時にソケットクローズ
sock.close()

このクライアントでは大きく分けて2つのことを行っており、1つは Tkinter を利用した GUI の作成と、もう1つはソケット通信になります。GUI にはボタンとラベルを用意し、ボタンがクリックされた時のイベントハンドラーとして click_button 関数を登録しています。tkinter.Button 実行時に command 引数を指定することで、そのボタンがクリックされた時のイベントハンドラーを登録することができます。

この click_button ではソケット通信でサーバーに対してデータを送信し、さらにデータの受信を行い、さらに受信したデータを文字列としてラベルに表示する処理を行うようにしています。

Tkinter におけるボタンやラベルの作成の仕方、さらにはこれらの各種ウィジェットの配置方法については下記で解説していますので、これらについて詳しく知りたい方は下記ページを参照していただければと思います。

ボタンウィジェット作成解説ページのアイキャッチ Tkinterの使い方:ボタンウィジェット(Button)の使い方 ラベルウィジェット解説ページのアイキャッチ Tkinterの使い方:ラベルウィジェット(Label)の使い方 ウィジェット配置方法解説ページのアイキャッチ Tkinterの使い方:ウィジェットの配置(pack・grid・place)

動作確認

実際にサーバーとクライアントのスクリプトを実行し、クライアント側のアプリの反応が無くなる様子を確認してみましょう!

まず最初に サーバー で紹介したスクリプト server.py を実行し、続いて クライアント で紹介したスクリプト client.py を実行してください。client.py を実行すれば下の図のようなウィンドウが表示されるはずです。

client.pyを実行することで表示されるGUIウィンドウ

ここで、表示されたウィンドウ上の 送信 ボタンをクリックしてみてください。これにより、client.py における click_button 関数が実行され、その関数内の sendto メソッドの実行によって server.py にデータが送信されます。そして、server.py からの返事を待つために recv メソッドが実行されることになります。この recv メソッドは server.py からデータが送信されてくるまで受信待ちの状態になります。そして、server.py ではデータの送信前に sleep によって 10 秒待つようになっているため、10 秒間受信待ちの状態になることになります。click_button 関数が実行されている間は mainloop が実行されていない状態となるため、この間アプリを操作しても反応しないことになります。

ボタンクリック後の約10秒間の間mainloopが実行されないことを示す図

ということで、送信 ボタンをクリックした後にウィンドウの最大化ボタンをクリックしてみてください。おそらく最大化ボタンはクリックしても反応しないはずです。そして、送信 ボタンクリック後の約 10 秒後にようやくウィンドウが最大化されることになるのではないかと思います。この時、ラベルに World! という文字列が表示されることも確認できるはずです。

最大化が遅れて実行される様子

まず、最大化ボタンを押しても反応しなかったのは、送信 ボタンクリック後の約 10 秒間は click_button 関数実行中のため mainloop が実行されていない状態となっていたためです。また、送信 ボタンをクリックしてから約 10 秒後に click_button 関数が終了して mainloop に処理が戻るため、ここで既に発生していた「最大化ボタンのクリック」に対するイベントハンドラーが実行されることになります。なので、ウィンドウの最大化が遅れて実行されたというわけです。

このように、イベントハンドラー内で通信を行うと受信待ちが発生する可能性があり、それによりアプリが反応しなくなる現象が発生することになります。server.py10 秒間という長い時間スリープさせているので、実際にはここまで長い間アプリが無反応になることはないかもしれませんが、通信の場合、通信相手の状況によっては受信等が待たされる可能性は常に存在するため、そもそもイベントハンドラー内で受信待ちが発生するような処理は避けるべきです。

解決策

次は、ここまで説明してきたような「アプリが反応しない」という現象を回避するための解決策について解説していきます。

スポンサーリンク

マルチスレッドを利用して解決

もし可能であれば、イベントハンドラー内で「待ち」が発生するような処理を行わないというのが一番シンプルで直接的な解決策になります。

ただ、どうしてもボタンクリック等のイベント発生時に通信を行いたいような場合もあると思います。その場合は、この章のタイトルにもある「マルチスレッド」を利用することで解決することが可能です。

MEMO

同様に、マルチプロセッシングを利用することで解決可能です

このページではマルチスレッドを利用することを前提に解説を進めます

マルチスレッドとは処理の並行化を行うための仕組みになります。つまり、1つのプログラム内で複数の処理を並行して実行することができます。したがって、マルチスレッドを導入することで mainloop 関数と他の処理を並行して実行することが可能となり、mainloop 関数と、ここまで問題視してきた受信待ちとを並行して実行することも可能となります。

マルチスレッドを導入することで受信待ちの間もmainloopga実行されるようになることを示す図

受信待ちの間も mainloop が実行されることになるため、受信待ちの間にアプリの反応がなくなるような問題を解決することができます。

もちろん、受信待ちだけでなく、イベントハンドラーで実行するとアプリの反応を悪化せてしまうような「時間がかかる処理」に関しても同様に、マルチスレッドの導入による処理に並列化によってアプリの反応の改善につなげることが可能となります。

マルチスレッドの導入手順

じゃあ、具体的にどうやってマルチスレッドを導入すればよいのか?

次は、この点について解説していきます。

まず、マルチスレッドは threading というモジュールを import することで導入可能です。threading は Python の標準モジュールですので、Python が利用可能な環境であればインストール作業など無しに import 可能です。

import
import threading

続いて、イベントハンドラー内で実行している mainloop と並行して実行したい処理の部分を別の関数として分離します。より具体的には、待ちが発生する可能性がある・処理量が多い等の時間のかかる処理を別の関数に分離してやれば良いです。

マルチスレッドの導入手順

具体的な例を示すと、アプリの反応が無くなるアプリのサンプルスクリプトクライアント で紹介したスクリプトの場合であれば、イベントハンドラー click_button から最低限 recv を実行する部分を他の関数に分離してやれば良いことになります。最低限 recv さえ他の関数に分離してやれば良いのですが、別に sendto 実行部分からまとめて他の関数に分離してやるのでも良いです。

例えば下記は、アプリの反応が無くなるアプリのサンプルスクリプトクライアント の click_buttonの処理を分離する一例となります。分離した処理は communicate 関数で実行するようになっています。

関数の分離例
def communicate():
    # サーバーにデータを送信
    sock.sendto(b'Hello', ('127.0.0.1', 40001))

    # サーバーからデータを受信
    recv_data = sock.recv(1024)

    # 受信したデータを文字列としてラベルに表示
    label.config(text=recv_data.decode())

def click_button():
    pass

続いて、分離した処理の部分を置き換える形でイベントハンドラー内で threading.Thread を実行するようにします。これによりスレッドの作成が行われます。そして、この threading.Threadtarget 引数に、先ほど分離した関数(関数オブジェクト)を指定します。さらに、threading.Thread の返却値に start メソッドを実行させます。これにより、target 引数に指定した関数を実行するスレッドがスタートすることになります。

マルチスレッドの導入手順2

例えば、先ほど示したコードの例で考えると、時間のかかる処理を分離して作成した関数は communicate であるため、イベントハンドラーである click_button に下記のような処理を追加してやれば良いことになります。

スレッドの生成とスタート
def click_button():
    # スレッドの生成
    thread = threading.Thread(target=communicate)

    # スレッドスタート
    thread.start()

マルチスレッドを導入する手順は以上になります。

マルチスレッド導入による効果

では、上記のように処理を変更することでどういった効果が得られるのでしょうか?

その点について解説していきます。

まず、マルチスレッド導入前では、mainloop 内でイベントが検知された際に mainloop から関数呼び出しによってイベントハンドラーの全体の処理が実行されるようになっていました。そのため、このイベントハンドラー内の処理が全て終了するまで mainloop に処理が戻らないことになります。なので、イベントハンドラー内の処理に時間がかかるとアプリが反応しなくなります。

マルチスレッド導入前における、各処理の実行タイミング

それに対し、マルチスレッドを導入することで次のように処理が変化することになります。まず、mainloop からイベントハンドラーが実行されるまでの流れは導入前と同じになります。なんですが、イベントハンドラー内では threading.Thread の引数 target に指定した関数は実行されません。start メソッドの実行により、target に指定された関数のスタートを指示するだけになります。そして、スタートの指示をしたら start メソッドは即座に処理を終了します。なので、イベントハンドラー内では target に指定された関数は実行されず、target に指定された関数で時間のかかる処理を実行するようにしていたとしても、イベントハンドラー自体は直ぐに終了し、それに伴い直ぐに mainloop が実行されることになります。なので、アプリの反応が悪くなることはありません。

マルチスレッド導入後における、各処理の実行タイミング

さらに、その mainloop が実行されているのと並行に、target 引数に指定した関数も実行されることになります。したがって、target 引数に指定する関数でデータの受信を行う場合も、mainloop でのイベントの監視と並行してデータの受信待ちが行われることになります。もちろん、通信相手からデータ送信されてきたときにはデータの受信も行われることになります。つまり、アプリの反応を低下させることなく、他の処理を同時に実行させることができるようになります。

スポンサーリンク

マルチスレッドを導入したアプリのサンプルスクリプト

ここまで説明してきたマルチスレッドを導入したアプリのサンプルスクリプトを示していきたいと思います。

ここで示すのは、アプリの反応が無くなるアプリのサンプルスクリプト で紹介したクライアントをベースにマルチスレッドを導入したスクリプトになります。サーバーに関しては、アプリの反応が無くなるアプリのサンプルスクリプト で紹介したサーバーをそのまま変更せずに使って動作確認を行っていきます。

マルチスレッド導入後のクライアント

マルチスレッドを導入したクライアントのサンプルスクリプトは下記のようになります。

client.py
import tkinter
import socket
import threading # マルチスレッド用

# ソケットを生成(UDP通信)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

def communicate():
    # サーバーにデータを送信
    sock.sendto(b'Hello', ('127.0.0.1', 40001))

    # サーバーからデータを受信
    recv_data = sock.recv(1024)

    # 受信したデータを文字列としてラベルに表示
    label.config(text=recv_data.decode())

def click_button():
    # スレッドの生成
    thread = threading.Thread(target=communicate)

    # スレッドスタート
    thread.start()

# アプリを作成
app = tkinter.Tk()

# ボタンの生成と配置
button = tkinter.Button(
    app,
    text="送信",
    font=('', 40),
    command=click_button # イベントハンドラーの登録
)
button.pack(padx=10, pady=10)

# ラベルの生成と配置
label = tkinter.Label(
    app,
    text='',
    font=('', 40),
)
label.pack(padx=10, pady=10)

# メインループでイベント監視
app.mainloop()

# アプリ終了時にソケットクローズ
sock.close()

アプリの反応が無くなるアプリのサンプルスクリプト で示したクライアントでは、ボタンクリック時に実行される click_button の中でサーバーとの通信を行っていましたが、上記では click_button ではスレッドの生成およびスレッドのスタートのみを実行するようにしています。そして、そのスレッドでは communicate が実行されるようにしており、この communicate 関数の中でサーバーとの通信を行うようにしています。そのため、受信待ちが発生したとしてもイベントハンドラーはスレッドスタート後に直ぐに終了し、すぐに mainloop が実行される状態に戻ることになります。 

今回は sendto 実行部分から communicate 関数側に分離していますが、元々アプリが反応しなくなるのはイベントハンドラー内で recv が実行されてデータの受信待ちが行われることが原因です。なので、recv 実行以降の処理のみを communicate 関数側に分離するようにしたとしてもアプリが反応しなくなる問題は解決可能です。

一点補足しておくと、上記のスクリプトは結構作りが甘いです。例えば、ボタンクリック後にアプリを閉じるボタンで終了させると、communicate 関数が実行されている間に並行してスクリプト最後部分の sock.close() が実行されてソケットがクローズされることになります。そして、クローズされたソケットに対して communicate 関数の中でメソッドが実行されることになるため例外が発生することになります。これは言い訳ですが、マルチスレッドによる効果を知ってもらうことを目的としているため、このあたりの細かな制御が抜けてしまっているので注意してください。

動作確認

最後にマルチスレッドを導入したクライアントのスクリプトを実行し、クライアント側のアプリの反応が無くなる現象が解消されていることを確認してみましょう!

まず最初に サーバー で紹介したスクリプト server.py を実行し、続いて マルチスレッド導入後のクライアント で紹介したスクリプト client.py を実行してください。client.py を実行すれば下の図のようなウィンドウが表示されるはずです。ここまではマルチスレッド導入前と同じですね!

client.pyを実行することで表示されるGUIウィンドウ

ここで、表示されたウィンドウの 送信 ボタンをクリックしてみてください。そして、その直後にウィンドウの最大化ボタンをクリックしてみてください。マルチスレッド導入前は、最大化ボタンをクリックしてもアプリが反応しませんでしたが、今回は即座にアプリが反応してウィンドウが最大化される様子が確認できるはずです。そして、送信 ボタンをクリックして約 10 秒経過するとラベルに World! が表示されることも確認できるはずです。

最大化が即座に実行される様子

このような動作になっているのは、送信 ボタンクリック後に mainlooprecv (データの受信) が並行して実行されているからになります。そのため、イベントが発生した場合は mainloop からイベントハンドラーが実行され、それによりウィンドウが移動したりウィンドウが最大化されたりしますし、データを受信した際には受信したデータを文字列に変換した World! がラベルに表示されることができます。

このような処理の並行化はマルチスレッドの導入によって実現されたものであり、マルチスレッドの導入により、アプリの反応を悪くすることなく通信を実現できていることが確認できたと思います。

今回の問題点はマルチスレッドの導入で非常に簡単に解決することができましたし、マルチスレッド自体も簡単に導入できることを感じていただけたのではないかと思います。ただし、マルチスレッドを多用すると処理の同期等が必要になり、処理が複雑になったりバグが発生してプログラムが停止してしまう可能性も高くなりますので、この点は注意してください。

まとめ

このページでは、Tkinter と通信を併用したときの注意点について解説しました!

具体的には、単に通信でデータの受信待ちを行うと、その待っている間 Tkinter の mainloop が実行されなくなり、それによってアプリが反応しなくなる問題が発生します。これが注意点です。

そして、この問題はマルチスレッドの導入により解決可能になります。具体的には通信を行う処理を他のスレッドで実行させてやれば、通信時に待ちが発生したとしても、その待ちと mainloop とを並行して実行することができます。つまり、待ちの間も mainloop が実行されているためアプリの反応が無くなるようなことはありません。特に Tkinter では常に mainloop を実行させておくことが重要となるため、イベントハンドラーで待ちが発生したり時間がかかる処理を実行させたりするような場合はマルチスレッドの導入を検討してみても良いと思います!

ただ、マルチスレッドを多用すると処理が複雑になりますし、場合によっては同期処理や排他制御など難しい制御も必要となります。まずは、マルチスレッド等の並行・並列処理なしに mainloop をできるだけ常に実行させることができないかを検討し、無理そうな場合のみマルチスレッド等の導入を試みる流れで考えるのが良いと思います!

同じカテゴリのページ一覧を表示