このページでは Python tkinter でのマルチスレッドの使用例について解説していきたいと思います。
特にプログラミング習いたての時はそうだと思うよ!
でも処理を並列に動かすことで高速化したり、
別々の処理を同時に実行したりしたい場合には重要になってくる
特に tkinter ではユーザー操作時のアプリの反応を
良くしたりすることもできるから重要!
僕の作ったアプリ良く止まるんだよね…
興味出てきたよ!
マルチスレッドはプログラムを高速化したり同時処理を行ったりする上で重要な仕組みになります。より高度なアプリも作れるようになるので是非覚えておきましょう!
このページの内容は mainloop
に密接に関わるので mainloop
について理解しているとより理解しやすくなると思います。ですので mainloop
がどんなものかを知らない方は是非下記ページを事前に読んでいただければと思います。
また、最初はマルチスレッドの基本的なことを解説しますので、マルチスレッドを既にご存知の方はtkinter におけるマルチスレッドまでスキップしていただければと思います。
Contents
マルチスレッドとは
マルチスレッドがどのようなものであるかは下記ページで解説しているので参考にしていただければと思います(C言語向けの解説ですが…)。
入門者向け!C言語でのマルチスレッドをわかりやすく解説要はプログラムの処理を並列で実行するための仕組みです。
スレッドというのは CPU が実行する処理(仕事)のことを言います。考え方によっては処理をする人と考えても良いと思います。
CPU はこのスレッド単位で処理を行います。CPU が同時に動作できる場合(複数のコアから構成される場合)、CPU コアは複数のスレッドを同時に実行することが可能です。
ですが、あくまでも CPU に割り当てられるのはスレッド単位です。
なので、プログラムが1つのスレッドから構成される場合、そのプログラムを処理してくれる CPU コアは1つのみになります。1つのスレッドを複数の CPU コアが同時に実行してくれるようなことはありません。
マルチスレッドなどの並列処理の仕組みを利用しない場合、私たちが作成するプログラムは1つのスレッドから構成されますので、基本的には CPU のコアは1つしか動作していない状態になります。
一方で、マルチスレッドの仕組みを利用してプログラムの中で新たなスレッドを生成した場合、そのスレッドを別の CPU コアに実行させることが可能です。
ただスレッドを作れば必ず複数のコアが同時に処理してくれるというわけではないです。
例えば CPU の全てのコアが他のアプリのスレッドを処理しているような場合は、スレッドを生成しても、順番が回ってくるまで処理が待たされることになります。
なるほど…
マルチスレッド使ってなかったから
僕の作ったアプリは CPU コア1つしか使われてないってことだね…
まあ厳密にいうと tkinter 本体もスレッドを持っていると思うからおそらく複数コアが動いているんだけどね
でもおそらく君が書いたスクリプト部分は CPU コア1つからしか同時に処理されていないと思うよ
Python におけるマルチスレッドの使い方
ではそのマルチスレッドの Python での使い方を簡単に(本当に簡単に)紹介ておきたいと思います。
スポンサーリンク
モジュールのインポート
Python ではマルチスレッドを threading
を import
することで利用することができます。
threading
は Python の標準モジュールなので Python をインストールしているだけで利用可能だと思います。
import threading
スレッドの生成
さらにスレッドは threading.Thread
クラスのコンストラクタを実行することより生成することができます。
thread1 = threading.Thread(target=func)
返却値はスレッドのインスタンスになります。
引数 target
には「関数」や「メソッド」を指定します。ここで指定する「関数」や「メソッド」が新たなスレッドで具体的に行う処理となります。
ここでは詳しい説明は行いませんが、threading.Thread
クラスのコンストラクタの引数としては他にもいろいろ指定することができます。
例えば引数 arg
では、target
で指定した関数に渡したい引数を指定することができます。
質問!
生成できるスレッドは1つだけなの?
いや、そんなことはないよ!
このページで紹介するスクリプトでは生成するスレッドは1つだけだけど、複数生成することもできる!
最大何個まで生成できるかまでは分からん…
また、スレッドはスクリプト内で複数生成することもできます。
スレッドの開始
さらにスレッドのインスタンスに start
メソッドを実行させることにより、生成したスレッドを開始することができます。
スレッドは開始後、CPU によって処理が行われます。
thread1.start()
ちなみに start
メソッドはスレッドの開始をしたらすぐに終了する関数で、スレッドの処理として指定した関数やメソッドの処理が完了するのを待つようなことはしません。
また、スレッドは指定された処理(引数 target
で指定した関数やメソッド)が終了すると自動的に消滅します。
スポンサーリンク
マルチスレッドの使用例
簡単な例でマルチスレッドによる並列処理を説明したいと思います。
シングルスレッドの例
下記はシングルスレッドで動作する Python プログラムになります。
# -*- coding:utf-8 -*-
import threading
def func():
for j in range(100):
print("sub : " + str(j))
func()
# メインスレッドでprintのループ
for i in range(100):
print("main : " + str(i))
単に関数 func
を呼んでいるだけですので、まず func
関数のループで print
が 100
回実行されて sub : yy
が表示され(yy
は 0
から 99
の値)、その後に 100
回 main : xx
が表示されることになります(xx
は 0
から 99
の値)。
シングルスレッドですので、この i
のループと j
のループが同時に実行されることはありません。
ですので、必ず最初に 100
回 sub : yy
が表示され、その後に 100
回 main : xx
が表示されます。sub : yy
の途中で main : xx
が表示されることは絶対にありません。
そもそもプログラムってそういうものだと思ってたよ…
処理が単純に逐次的に実行されていくものだと思ってた
マルチスレッドのことを知らないとそう勘違いする人もいると思うよ
スレッドを使いこなせばプログラミングの世界が一気に広がるよ
次は実際にそのマルチスレッドを用いたプログラム例を見てみよう!
マルチスレッド の例
次に紹介するのはマルチスレッドを利用した例になります。
# -*- coding:utf-8 -*-
import threading
# 別スレッドで処理される関数
def func():
# 別スレッドでprintのループ
for j in range(100):
print("sub : " + str(j))
# スレッドの生成とスタート
thread1 = threading.Thread(target=func)
thread1.start()
# メインスレッドでprintのループ
for i in range(100):
print("main : " + str(i))
今度は threading.Thread
により新たなスレッドの生成をしており、そのスレッドの処理として関数 func
を指定しています。
さらに生成したスレッドを start
メソッドで開始することで、func
関数を別のスレッドとして CPU コアに処理させることができるようにしています。
これにより、i
に対するループと j
に対するループは複数の CPU コアにより同時に実行可能になります。
ですので、シングルスレッドの例で示した単なる関数呼び出しとは異なり、main : xx
の表示と sub : yy
の表示が同時に実行されることになります。
したがって、このマルチスレッドの例では main : xx
と sub : yy
とが入り乱れた状態で表示されます(main : xx
の表示の途中で sub : yy
の表示が割り込んだりする)。
こんな感じでマルチスレッドを利用することでプログラム内の処理を同時に並列に実行することができます。
そうだね!
そもそも CPU は僕たちが作ったプログラムだけじゃなくて、他に動作しているアプリや OS のスレッドの処理も行っているから、自分が作ったスレッドが処理されるタイミングもまちまちなんだ
だから、同期っていう処理が重要になるよ
これは最後にちょっとだけ解説する!
tkinter におけるマルチスレッド
ここまではマルチスレッドの基本の解説をしてきましたが、ここからは本題の「tkinter におけるマルチスレッド」についてここから解説していきます。
tkinter を用いたプログラミングにおいて、マルチスレッドは非常に便利です。
tkinter ではマルチスレッドが便利
これはなぜかというと、マルチスレッドにすることで、mainloop
で待機する必要のないスレッドが使えるようになるからです。
mainloop
が何か知らない方は是非下記ページを参考にしていただければと思います。
上記ページでも解説していますが、tkinter ではボタンクリックなどのイベントが発生した際に、mainloop
実行中ないとそのイベントを処理することができません。
なので、mainloop
実行中でない時にボタンクリックなどのユーザー操作が行われても即座にアプリが反応せず、ユーザーに「反応が悪い」「途中で固まる」などの印象を与えることになります。
そのため、tkinter では出来るだけ mainloop
実行中になるようにプログラミングする必要があります。
ただし、mainloop
実行中である必要のあるスレッドは1つのみです。
マルチスレッドの場合、複数のスレッドを用意することができるので、1つのスレッドのみ mainloop
で待機するようにすれば、残りのスレッドは mainloop
を意識することなく使用することができます。
例えば時間がかかるような処理や、sleep
などで待ちを行うような処理を行うような場合にアプリの反応が悪くなる例を下記ページで紹介しました。
こういった処理も、新たに生成するスレッド(threading.Thread
で生成するスレッド)で行わせるようにすれば、ボタンクリックなどのユーザー操作が行われた際にも、すぐにその操作に対する処理を実行することが可能です。
なるほど!
1つのスレッドは mainloop
で待機専用にするってわけだね!
mainloop
で待機してたらイベント処理も即座に実行できるからアプリの反応が良くなるのか!
そういうことだね!
使ってみるとすぐマルチスレッドの効果がすぐ実感できるので、マルチスレッドを学ぶには tkinter は良い題材だと思うよ!
確かに!
アプリの反応が一気に改善されるから楽しくマルチスレッド学べそう!
スポンサーリンク
tkinter でのマルチスレッドの使い方
特にプログラミング入門したての方にとって、tkinter でのマルチスレッドの基本的な使い方は下記のようになると思います(メインスレッドはプログラム起動時に生成されるスレッドです)。
- メインスレッド:
- 基本的に
mainloop
で待機 - 待機中にイベントなどが発生した場合にそのイベント処理も行う
- 基本的に
threading.Thread
で生成するスレッド:- メインスレッドで処理するとアプリの反応が悪くなるような処理
tkinter を使ってアプリ開発を行うような際には、最初は(マルチスレッドを使わずに)普通にシングルスレッドでプログラミングを行えば良いと思います。
そして、アプリの反応が悪くなるような処理を行いたくなった時にスレッドを生成し、生成したスレッドでその処理を行わせるようにします。
これにより、アプリの反応の良さを保ったまま様々な処理を行うことができるようになります。
tkinter でのマルチスレッドの使用例
最後に tkinter でのマルチスレッドの具体的な使用例を紹介したいと思います。
下記ページのスリープ処理を after メソッドに置き換えるで紹介した「sleepを使用したタイマーアプリ」をマルチスレッドを用いてアプリの動きを改善していきたいと思います。
【Python】tkinterのmainloopについて解説シングルスレッドの例
下記がスリープ処理を after メソッドに置き換えるで紹介した「sleepを使用したタイマーアプリ」のスクリプトを引用したものになります。
# -*- coding:utf-8 -*-
import tkinter
start_flag = False
# タイマー
def timer(count):
global label
global start_flag
if start_flag:
import time
time.sleep(1)
label.config(text=count)
# スタートボタンが押された時の処理
def start_button_click(event):
global start_flag
start_flag = True
for i in range(10):
timer(i + 1)
# ストップボタンが押された時の処理
def stop_button_click(event):
global start_flag
start_flag = False
# メインウィンドウを作成
app = tkinter.Tk()
app.geometry("200x100")
# ボタンの作成と配置
start_button = tkinter.Button(
app,
text="スタート",
)
start_button.pack()
stop_button = tkinter.Button(
app,
text="ストップ",
)
stop_button.pack()
# ラベルの作成と配置
label = tkinter.Label(
app,
width=5,
height=1,
text=0,
font=("", 20)
)
label.pack()
# イベント処理の設定
start_button.bind("<ButtonPress>", start_button_click)
stop_button.bind("<ButtonPress>", stop_button_click)
# メインループ
app.mainloop()
実行してみるとわかると思いますが、「スタート」ボタンを押すと0が表示されるだけで、その後表示されている数字は更新されませんし、「ストップ」ボタンを押しても反応しません(10秒後にアプリが反応します)。
これは、start_button_click
関数の中で10秒間待っているからです。この待っている間は処理が mainloop
に戻らないので、その間にイベントが発生してもそのイベントの処理が行われないためです。
これ結構やりがちだよね…
確か sleep
じゃなくて after
メソッド使えば改善できたはず
after
メソッドを使えば mainloop
にすぐに処理が戻せるのでボタンを押しても反応しない現象は改善できるね!
だけど今回は after
メソッドを使わずに、マルチスレッドで改善する例を紹介していくよ!
スポンサーリンク
マルチスレッドの例
この「数字が更新されない」「ボタンを押しても反応しない」ところをマルチスレッドを利用して改善したいと思います。
下記がそのマルチスレッドを利用したスクリプトになります。
# -*- coding:utf-8 -*-
import tkinter
import threading
start_flag = False
quitting_flag = False
count = 0
# タイマー
def timer():
global label
global start_flag
global quitting_flag
global count
while not quitting_flag:
if start_flag:
label.config(text=count)
count += 1
if count > 10:
start_flag = False
import time
time.sleep(1)
# スタートボタンが押された時の処理
def start_button_click(event):
global start_flag
global count
count = 0
start_flag = True
# ストップボタンが押された時の処理
def stop_button_click(event):
global start_flag
start_flag = False
# 終了ボタンが押された時の処理
def quit_app():
global quitting_flag
global app
global thread1
quitting_flag = True
# thread1終了まで待つ
thread1.join()
# thread1終了後にアプリ終了
app.destroy()
# メインウィンドウを作成
app = tkinter.Tk()
app.geometry("200x100")
# ボタンの作成と配置
start_button = tkinter.Button(
app,
text="スタート",
)
start_button.pack()
stop_button = tkinter.Button(
app,
text="ストップ",
)
stop_button.pack()
# ラベルの作成と配置
label = tkinter.Label(
app,
width=5,
height=1,
text=0,
font=("", 20)
)
label.pack()
# イベント処理の設定
start_button.bind("<ButtonPress>", start_button_click)
stop_button.bind("<ButtonPress>", stop_button_click)
app.protocol("WM_DELETE_WINDOW", quit_app)
# スレッドの生成と開始
thread1 = threading.Thread(target=timer)
thread1.start()
# メインループ
app.mainloop()
今度はシングルスレッドの時とは異なり、「スタート」ボタンを押せば数字がどんどんカウントアップされ、「ストップ」ボタンを押せばカウントアップが停止します。
つまりシングルスレッドの時に比較してアプリの反応が改善されています。
なぜこのように動作が異なるのか、その理由を解説していきます。
まず、下記でスレッドの生成と開始を行っています。
# スレッドの生成と開始
thread1 = threading.Thread(target=timer)
thread1.start()
スレッドで処理する関数として timer
を指定していますので、生成されるスレッドでは timer
関数が実行されることになります。
ちなみに下記で示す timer
関数以外の処理は全てメインスレッドで実行されます
start_button_click
関数stop_button_click
関数- ウィジェットの作成
- イベント処理の受付
mainloop
の実行
2つのスクリプトの動きの違い
ではここで質問です。
シングルスレッドの例で示したスクリプトと、今回紹介したマルチスレッドの例のスクリプトとでアプリの反応が異なる決定的な違いはなんでしょう?
うーん、「待ち」を行っている関数が違うよね
シングルスレッドの場合は start_button_click
関数で待ってるし、
マルチスレッド の場合は timer
関数で待ってる!
まあ他にも多少違う点はあるのですが、待ちを行っている関数が違います。で、スレッドの観点で考えると「待ちを行っているスレッド」が異なります。
この「待ちを行っているスレッド」の違いがシングルスレッドのスクリプトとマルチスレッドのスクリプトとで動き(アプリの反応)が異なる1番の理由になります。
スクリプトの動作を考えると、スクリプトが起動するとまずメインスレッドがメインウィンドウやウィジェットを作成し、さらにイベントを設定した後に mainloop
を実行します(マルチスレッドの場合はスレッドの生成と開始も行います)。
さらに「スタート」ボタンがクリックされると、mainloop
実行中のスレッド(つまりメインスレッド)が start_button_click
関数を実行します。
ここまではシングルスレッドの場合でもマルチスレッドの場合でもほぼ同じ動きになります。
ただし、シングルスレッドの場合、start_button_click
関数の中で10秒待つ処理が行われていますので、10秒間関数が終了しません。
したがって、その間は mainloop
に戻りませんので、この間は次のイベントが発生してもそのイベントに対する処理が実行できないことになります。
なので、シングルスレッドの場合は「スタート」ボタンが押したあとはしばらく他のイベント処理が行われずアプリが反応しなくなったように見えてしまいます。
一方、マルチスレッドの場合、start_button_click
関数は count = 0
と start_flag = True
のみを実行して即座に終了します(後述で説明しますが、この start_flag = True
は thread1
に「スタート」ボタンがクリックされたことを通知する処理になります)。
そして関数終了後はまた mainloop
に戻ります。なので、「ストップ」ボタンクリックなどの次のイベントが発生すると、すぐにそのイベントに対する処理を実行することができます。
これを実現できているのは、待つ処理をメインスレッドでないスレッド(つまり thread1
)で行っているためです。
今回は「待つ処理」をメインスレッドでないスレッドで行わせるようにしたけど、
「時間のかかる処理」をメインスレッドでないスレッドで行わせるようにしても良さそうだね!
そうだね!
「時間のかかる処理」をメインスレッドで実行しちゃうとアプリの反応が悪くなるので、生成したスレッドで処理させることでその反応を改善することができるよ!
マルチスレッドにおける同期の必要性
こんな感じでマルチスレッドを利用することでアプリの反応の良さを保ったまま様々な処理を実行できるようになります。
ただし、ただマルチスレッドは便利なだけではなく、マルチスレッドならではの難しい点もあります。
それが同期です。スレッド間でのタイミングの制御ですね。
スレッド間での同期の例
例えば thread1
で実行される timer
関数では「スタート」ボタンが押された時に数字のカウントアップを開始し、「ストップ」ボタンが押された時にカウントアップを停止するようにしたいので、thread1
はボタンが押されたタイミングを知る必要があります。
ただしボタンが押されたことはメインスレッドしか知らない(mainloop
を実行しているスレッドしか知らない)ので、メインスレッドから thread1
にボタンが押されたタイミングを教えてあげる必要があります。
なので、start_flag
を用意し、「スタート」ボタンが押された時に実行される start_button_click
関数や「ストップ」ボタンが押された時に実行される stopt_button_click
関数で start_flag
の値をそれぞれ True
と False
を設定するようにしています。
そして、timer
関数では start_flag
が True
になった時にカウントアップを開始し、False
になった時にカウントアップを停止するようにしています。
つまり start_flag
を利用した情報の通知により、メインスレッドと thread1
とで同期を取っているわけです。
こんな感じで、マルチスレッドプログラミングにおいては他のスレッドと情報の通知を行いながら同期を取る必要があります。
今回の例では単純に情報の通知に global
変数を利用しましたが、同期を取るための手段として Queue
などが存在します。
アプリ終了時の同期
さらに tkinter でマルチスレッドを利用する場合、アプリ終了時の同期についても考慮する必要があります。
tkinter で開発したアプリにおいても、終了ボタンが押された時にアプリが終了します。そして、このアプリの終了の時には、ウィジェットの削除なども行われます。
なので、スレッドでウィジェットへの操作を行っている場合、アプリ終了時の同期が取れていないと削除したウィジェットに操作を行ってしまいエラーが発生することになります。
例えばマルチスレッドの例で示したスクリプトでは、thread1
で実行される timer
関数では、ラベルウィジェットの label
に config
メソッドを実行させるようにしています。
ですので、「スタート」ボタンが押されて数字のカウントアップを行っている最中にアプリが終了されると、既に削除されたラベルウィジェットの label
に config
メソッドを実行させることになるのでエラーが発生します。
tkinter でのマルチスレッドの利用時は、こういったアプリ終了時のエラーを防ぐための同期も必要になります。
例えば終了ボタンが押された時に、下記のような順序で処理が行われるように同期をすれば、アプリ終了時のエラーを防ぐことができます。
- メインスレッドが
thread1
にアプリが終了されることを通知する - 通知を受けて
thread1
は関数を終了する - メインスレッドは
thread1
が終了するのを待つ thread1
終了後、メインスレッドは実際にアプリの終了を行う
thread1
終了後にアプリを終了するので、削除ずみのウィジェットへの操作が行われるようなことがなくなります。
これを実現するために、マルチスレッドの例で示したスクリプトでは下記のようなことを行っています。
終了ボタンが実行された時の処理を設定
上記を実現するためには、終了ボタンが押された時に、そのことを thread1
に教えてやる必要があります。
このため、終了ボタンが押された時に自身で定義した関数が実行されるように下記を行っています。
app.protocol("WM_DELETE_WINDOW", quit_app)
これにより、終了ボタンが押された時にアプリが終了するのではなく、関数 quit_app
が実行されるようになります。この quit_app
はメインスレッドで実行されます。
thread1
終了待ち
そして、終了ボタンが押された時に実行される quit_app
関数では、global
変数の quitting_flag
を用いて thread1
へのアプリが終了することの通知と thread1
の終了待ちを行っています。
具体的には、quit_app
関数の中で quitting_flag
を True
に設定することで、アプリが終了することを thread1
に通知します。
さらに、下記を実行することで thread1
の終了の待ち合わせを行います。join
メソッドは実行したインスタンスのスレッドの終了を待つメソッドです。
これを実行することで、スレッドが終了するまでメソッドの中で待つことができます。
# thread1終了まで待つ
thread1.join()
thread1
で実行される timer
関数では quitting_flag
が True
になるとループを終了するようにしているので、メインスレッドから quitting_flag
に True
が設定されると関数が終了してスレッドが終了することになります。
thread1
が終了すると、thread1.join
メソッドも終了しますのでメインスレッドは次の処理を実行することができます。
thread1.join
メソッドは thread1
が終了しないと return
しないため、thread1.join
の後の処理が実行されるタイミングは “確実に” thread1
が終了した後になります。
thread1
終了後のアプリの終了
そして、その次の処理ではアプリの終了を行います。
具体的には下記を実行することでアプリを終了します。
# thread1終了後にアプリ終了
app.destroy()
このアプリの終了は、thread1
が終了した後に必ず実行されるため、thread1
がアプリの終了により削除されたウィジェットへの操作を行うようなことはあり得ません。
したがって前に述べたアプリ終了時の同期ができていなくて発生したエラーを防ぐことができます。
同期は難しいよ…
“実行される処理の順番を確実にするためにはどうすれば良いか?” をしっかり考えながらプログラミングする必要がある
まあでも失敗すれば失敗するほど同期について考える力はつくと思うから、どんどん挑戦していけばいいと思うよ!
まとめ
このページでは tkitnter におけるマルチスレッドの利用について解説しました。
tkinter でマルチスレッドを利用すれば、重い処理などもアプリの反応性を落とすことなく実行するができます。
tkinter を利用したアプリ開発におけるマルチスレッドのメリットは大きいので、アプリ開発しながらマルチスレッドを学ぶのはオススメです。効果が実感しやすいので楽しく学べます!
ただし、スレッド間での同期を行う必要もあります。スレッド間でどのようにタイミングを制御するかをしっかり考えながらプログラミングするようにしましょう!
[…] こちらも詳しくは(参考)【Python/tkinter】tkinterでマルチスレッドを利用する – だえう …が分かりやすかったです。 […]