このページでは、Tkinter と通信を併用する場合の注意点について解説していきます。
通信には様々なものが存在しますが、このページでは「通信 = ソケット通信」として解説を行っていきます。ただ、他の通信を利用する場合でも同様の問題が発生しますし、このページで紹介する解決策で同様に解決可能です。
ちなみに、Python でのソケット通信については下記ページで解説していますので、ソケット通信について詳しく知りたい方は別途下記ページを参照していただければと思います。
Pythonでのソケット通信(ポート番号・プロトコル・サーバー / クライアント)Contents
Tkinter と通信の併用時の注意点
このページで解説しようとしている注意点とは、「受信待ちを行っている間、アプリが反応しなくなる」という点になります。
これは、結局は下記ページで解説している mainloop
に関するもので、単純に受信待ちを実行してしまうと mainloop
に処理が戻らなくなり、イベントが発生してもイベントハンドラーが実行されなくなるよーという注意点になります。
mainloop
が実行されないとアプリが反応しない
もう少し詳細を解説していきます。
Tkinter において、mainloop
は非常に重要な役割を持つメソッドです。この mainloop
が実行されている間、特定のイベントが発生した際にイベントハンドラーが実行されるようになっています。このイベントやイベントハンドラーに関しては下記ページで解説していますので、ご存じない方は下記ページを参照していただければと思います。
例えば、ボタンのクリックというイベントに対して click_button
という関数をイベントハンドラーとして登録しておけば、ボタンクリック時に click_button
という関数が自動的に実行されるようになります。この仕組みは単純で、mainloop
メソッドの中でイベント発生を監視するようになっており、イベントの発生を検知した際に mainloop
からイベントハンドラーを呼び出すようになっているだけです。
また、開発者が登録したイベント・イベントハンドラーだけでなく、デフォルトで登録されているイベント・イベントハンドラーも存在しており、例えばウィンドウの最大化ボタンが押された時にウィンドウを最大化するような処理も、この mainloop
やイベントの仕組みで実現されています。
で、この仕組みからも分かるように、mainloop
が実行されていない間はイベントが発生してもイベントハンドラーは実行されないことになります。なので、例えば mainloop
が実行されていない間に Tkinter で開発したアプリの最大化ボタンをクリックしてもアプリは反応しません。他のイベントに関しても同様です。要は、操作しても何も反応しないアプリとなってしまいます。
このような仕組みで Tkinter で開発したアプリは動作するので、反応性の高いアプリを開発するためには、できるだけ常に mainloop
が実行されている状態にしておくことが重要となります。
スポンサーリンク
「受信待ち」によってアプリが反応しなくなる可能性がある
では、ここまで説明してきた mainloop
やイベントハンドラーが実行される仕組みと通信処理とにどういう関係があるのでしょうか?
次は、この点について説明していきます。
実は、mainloop
と通信処理に直接関係性があるというわけではありません。ですが、通信処理を行うと mainloop
が実行されない状態が長くなる可能性があるという点に注意が必要です。特に通信ではデータの受信待ちを行うような場合があり、この受信待ちを行っている間 mainloop
が実行されなくなる可能性があります。
もう少し具体的に説明をしていきましょう!
例えば、アプリにボタンを用意し、そのボタンが実行された際には「通信相手のプログラムにソケット通信でデータを送信し、さらに、その通信相手から返事となるデータの受信を行う」という処理が実行されるようにアプリを開発したとしましょう。このようなアプリは、ボタンクリック時のイベントハンドラーとして上記の括弧内の処理を実行する関数を登録しておくことで実現できます。
ただ、ここでポイントになるのが、そのイベントハンドラーが実行されている間は mainloop
が実行されていない状態になるという点になります。イベントハンドラーは mainloop
から呼び出される形で実行され、通常の関数同様に、return
したり最後まで処理が終了しないと呼び出し元である mainloop
に処理が戻りません。つまり、イベントハンドラー実行中は一時的にアプリが反応しない状態となります。
で、このイベントハンドラーがすぐに処理を終了すれば良いのですが、通信でデータの受信を行う際には「受信待ち」状態になる可能性があります。上記の例であれば、通信相手が他の処理で忙しくて返事となるデータを送信する処理が遅れる可能性があり、その遅れている間は受信待ちとなります。
ソケット通信の場合はソケットを受信待ちしないように設定することも可能です(ノンブロック)
ただ、このページではそのような設定は行わず、”データが受信できるまでの間は受信待ち状態” となることを前提として解説を行います
そして、この受信待ちはイベントハンドラー内で実行されることになるため、この受信待ちを行っている間は他のイベントが発生してもイベントハンドラーが実行されません。これは、前述の通り、イベントハンドラーが実行されている間は mainloop
が実行されていない状態となるためです。
受信待ちを行っている間はユーザーがアプリを操作しようとしても反応しないため、ユーザーからは「アプリが停止した」「パソコンが壊れた?!」「アプリの反応が悪くてイライラする」などの印象を持たれてしまうことになります…。
ここまでの説明からも分かるように、アプリが反応しなくなるのは通信処理を行うこと自体が原因なのではなく、イベントハンドラー内で「待ち」が発生してしまうことが原因です。他の処理を行って「待ち」が発生することも当然ありますが、通信処理では通信相手の状況によっては待たされる可能性があり、上記のような問題が発生することが多いため、特に注意が必要だと思います。もちろん、同様の理由でイベントハンドラー内で重い処理・時間のかかる処理を実行させることも避ける必要があります。
アプリの反応が無くなるアプリのサンプルスクリプト
ここまで文章や図のみを用いた説明を行ってきましたので、ここでイベントハンドラーで受信処理を行うことで上記のような問題が発生するサンプルスクリプトを紹介しておきたいと思います。
ここで紹介するスクリプトはクライアントサーバーモデルのもので、クライアントを Tkinter を利用して開発します。このクライアントではボタンクリック時にサーバーに対して b'World!'
を送信し、さらにサーバーから返事となるデータの受信を行う作りとしています。
サーバーは Tkinter を利用せず、単にクライアントからデータを受信し、さらに b'World!'
をクライアントに返却するプログラムとしています。
サーバー
まず、サーバー側のサンプルスクリプトを示します。
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!'
というデータを送信するだけのプログラムになっています。ただ、クライアントへのデータの送信が遅くなるように、つまりクライアントでの受信待ちが長くなるように 、データの送信前に sleep
で 10
秒間スリープするようにしています。
UDP 通信についてや、UDP 通信を行うサーバーの作り方等について知りたい方は下記ページを参照していただければと思います。
Pythonでのソケット通信(ポート番号・プロトコル・サーバー / クライアント)クライアント
サーバーの通信相手となるクライアントのサンプルスクリプトは下記のようになります。
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
における click_button
関数が実行され、その関数内の sendto
メソッドの実行によって server.py
にデータが送信されます。そして、server.py
からの返事を待つために recv
メソッドが実行されることになります。この recv
メソッドは server.py
からデータが送信されてくるまで受信待ちの状態になります。そして、server.py
ではデータの送信前に sleep
によって 10
秒待つようになっているため、10
秒間受信待ちの状態になることになります。click_button
関数が実行されている間は mainloop
が実行されていない状態となるため、この間アプリを操作しても反応しないことになります。
ということで、送信
ボタンをクリックした後にウィンドウの最大化ボタンをクリックしてみてください。おそらく最大化ボタンはクリックしても反応しないはずです。そして、送信
ボタンクリック後の約 10
秒後にようやくウィンドウが最大化されることになるのではないかと思います。この時、ラベルに World!
という文字列が表示されることも確認できるはずです。
まず、最大化ボタンを押しても反応しなかったのは、送信
ボタンクリック後の約 10
秒間は click_button
関数実行中のため mainloop
が実行されていない状態となっていたためです。また、送信
ボタンをクリックしてから約 10
秒後に click_button
関数が終了して mainloop
に処理が戻るため、ここで既に発生していた「最大化ボタンのクリック」に対するイベントハンドラーが実行されることになります。なので、ウィンドウの最大化が遅れて実行されたというわけです。
このように、イベントハンドラー内で通信を行うと受信待ちが発生する可能性があり、それによりアプリが反応しなくなる現象が発生することになります。server.py
で 10
秒間という長い時間スリープさせているので、実際にはここまで長い間アプリが無反応になることはないかもしれませんが、通信の場合、通信相手の状況によっては受信等が待たされる可能性は常に存在するため、そもそもイベントハンドラー内で受信待ちが発生するような処理は避けるべきです。
解決策
次は、ここまで説明してきたような「アプリが反応しない」という現象を回避するための解決策について解説していきます。
スポンサーリンク
マルチスレッドを利用して解決
もし可能であれば、イベントハンドラー内で「待ち」が発生するような処理を行わないというのが一番シンプルで直接的な解決策になります。
ただ、どうしてもボタンクリック等のイベント発生時に通信を行いたいような場合もあると思います。その場合は、この章のタイトルにもある「マルチスレッド」を利用することで解決することが可能です。
同様に、マルチプロセッシングを利用することで解決可能です
このページではマルチスレッドを利用することを前提に解説を進めます
マルチスレッドとは処理の並行化を行うための仕組みになります。つまり、1つのプログラム内で複数の処理を並行して実行することができます。したがって、マルチスレッドを導入することで mainloop
関数と他の処理を並行して実行することが可能となり、mainloop
関数と、ここまで問題視してきた受信待ちとを並行して実行することも可能となります。
受信待ちの間も mainloop
が実行されることになるため、受信待ちの間にアプリの反応がなくなるような問題を解決することができます。
もちろん、受信待ちだけでなく、イベントハンドラーで実行するとアプリの反応を悪化せてしまうような「時間がかかる処理」に関しても同様に、マルチスレッドの導入による処理に並列化によってアプリの反応の改善につなげることが可能となります。
マルチスレッドの導入手順
じゃあ、具体的にどうやってマルチスレッドを導入すればよいのか?
次は、この点について解説していきます。
まず、マルチスレッドは threading
というモジュールを import
することで導入可能です。threading
は Python の標準モジュールですので、Python が利用可能な環境であればインストール作業など無しに 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.Thread
の target
引数に、先ほど分離した関数(関数オブジェクト)を指定します。さらに、threading.Thread
の返却値に start
メソッドを実行させます。これにより、target
引数に指定した関数を実行するスレッドがスタートすることになります。
例えば、先ほど示したコードの例で考えると、時間のかかる処理を分離して作成した関数は 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
でのイベントの監視と並行してデータの受信待ちが行われることになります。もちろん、通信相手からデータ送信されてきたときにはデータの受信も行われることになります。つまり、アプリの反応を低下させることなく、他の処理を同時に実行させることができるようになります。
スポンサーリンク
マルチスレッドを導入したアプリのサンプルスクリプト
ここまで説明してきたマルチスレッドを導入したアプリのサンプルスクリプトを示していきたいと思います。
ここで示すのは、アプリの反応が無くなるアプリのサンプルスクリプト で紹介したクライアントをベースにマルチスレッドを導入したスクリプトになります。サーバーに関しては、アプリの反応が無くなるアプリのサンプルスクリプト で紹介したサーバーをそのまま変更せずに使って動作確認を行っていきます。
マルチスレッド導入後のクライアント
マルチスレッドを導入したクライアントのサンプルスクリプトは下記のようになります。
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
を実行すれば下の図のようなウィンドウが表示されるはずです。ここまではマルチスレッド導入前と同じですね!
ここで、表示されたウィンドウの 送信
ボタンをクリックしてみてください。そして、その直後にウィンドウの最大化ボタンをクリックしてみてください。マルチスレッド導入前は、最大化ボタンをクリックしてもアプリが反応しませんでしたが、今回は即座にアプリが反応してウィンドウが最大化される様子が確認できるはずです。そして、送信
ボタンをクリックして約 10
秒経過するとラベルに World!
が表示されることも確認できるはずです。
このような動作になっているのは、送信
ボタンクリック後に mainloop
と recv
(データの受信) が並行して実行されているからになります。そのため、イベントが発生した場合は mainloop
からイベントハンドラーが実行され、それによりウィンドウが移動したりウィンドウが最大化されたりしますし、データを受信した際には受信したデータを文字列に変換した World!
がラベルに表示されることができます。
このような処理の並行化はマルチスレッドの導入によって実現されたものであり、マルチスレッドの導入により、アプリの反応を悪くすることなく通信を実現できていることが確認できたと思います。
今回の問題点はマルチスレッドの導入で非常に簡単に解決することができましたし、マルチスレッド自体も簡単に導入できることを感じていただけたのではないかと思います。ただし、マルチスレッドを多用すると処理の同期等が必要になり、処理が複雑になったりバグが発生してプログラムが停止してしまう可能性も高くなりますので、この点は注意してください。
まとめ
このページでは、Tkinter と通信を併用したときの注意点について解説しました!
具体的には、単に通信でデータの受信待ちを行うと、その待っている間 Tkinter の mainloop
が実行されなくなり、それによってアプリが反応しなくなる問題が発生します。これが注意点です。
そして、この問題はマルチスレッドの導入により解決可能になります。具体的には通信を行う処理を他のスレッドで実行させてやれば、通信時に待ちが発生したとしても、その待ちと mainloop
とを並行して実行することができます。つまり、待ちの間も mainloop
が実行されているためアプリの反応が無くなるようなことはありません。特に Tkinter では常に mainloop
を実行させておくことが重要となるため、イベントハンドラーで待ちが発生したり時間がかかる処理を実行させたりするような場合はマルチスレッドの導入を検討してみても良いと思います!
ただ、マルチスレッドを多用すると処理が複雑になりますし、場合によっては同期処理や排他制御など難しい制御も必要となります。まずは、マルチスレッド等の並行・並列処理なしに mainloop
をできるだけ常に実行させることができないかを検討し、無理そうな場合のみマルチスレッド等の導入を試みる流れで考えるのが良いと思います!