【Python】別スレッドを用いた処理がなかなかうまくいかなくて苦労した件。

Python:スレッド処理が難しい プログラミング

PythonでGUI画面を作り、その中に数字を1から2000まで順番に表示していくだけのプログラムを組んでいました。

当初は以下のような感じで組みました。

import tkinter

def TwoTh():    #行いたい処理 
    for i in range(2000):
        TextField.insert(tkinter.END, str(i + 1) + "\n")
        TextField.see("end")

#▼▼▼▼▼▼▼▼▼GUI項目▼▼▼▼▼▼▼▼▼
GUI = tkinter.Tk()
GUI.geometry("640x400")
GUI.title("1から2000まで順番に表示")
# メインフレーム
MainF = tkinter.Frame(GUI, width = 500, height = 400, bg = "white")
MainF.pack()
ScrollBar = tkinter.Scrollbar(MainF)
ScrollBar.pack(side = tkinter.RIGHT, fill = "y")
TextField = tkinter.Text(MainF, width = 50, height = 20, bd = 5)
TextField.pack(side = tkinter.LEFT)
TextField["yscrollcommand"] = ScrollBar.set

#処理開始ボタン
StartBtn = tkinter.Button(GUI, text = "開始", command = TwoTh)
StartBtn.pack()

GUI.mainloop()

このプログラムの実行結果は次のような感じです(画像ではStartBtnの部分が全てGetBtnになっています。別のプログラムファイルからコピペしたコードを使いまわしていたので申し訳ない)。

Python:スレッド処理の組み立て-1

しっかりと表示されました。

しかし、僕が予想していた動作ではありませんでした。

スポンサーリンク

処理実行中にGUI画面が固まったまま更新されていなかった

開始ボタンを押下すると自動でGUI画面内に1~2000までリアルタイムに表示されていくプログラムを組んでみたかったのですが、実際にはプログラム動作中にGUIの更新が一切されず、処理が終わると同時に数字が表示されました。

ちなみに処理中は以下のような感じで固まっていました(開始ボタンが凹んだまま固まっているのが確認できると思います)。

Python:スレッド処理の組み立て-2

この問題を解決するにはどうすればいいか色々ネットでググってみた所、GUIのテキストボックスにプログラムの進行度合いをリアルタイムで表示していくためには、別スレッドを用いるしかないとのこと。

今のような書き方だとプログラム全体がシングルスレッドで実行されることになり、自分が行いたい処理とGUI画面の更新とを同時に行うことができないっぽい。

スポンサーリンク

右往左往し、何とかGUI画面にリアルタイムで数字を表示していくことには成功

スレッド処理について全く知識が無い状態から色々とネットでググり、最初に書いていたプログラムコードを以下のように書き直してみました。

import tkinter
import threading

#▼▼▼▼▼▼▼▼▼別スレッドで行いたい処理▼▼▼▼▼▼▼▼▼

def TwoTh():
    for i in range(2000):
        TextField.insert(tkinter.END, str(i + 1) + "\n")
        TextField.see("end")

#上記の関数を別スレッドに投げるコード
Th_TwoTh = threading.Thread(target = TwoTh)

#▼▼▼▼▼▼▼▼▼GUI項目▼▼▼▼▼▼▼▼▼
GUI = tkinter.Tk()
GUI.geometry("640x400")
GUI.title("1から2000まで順番に表示")
# メインフレーム
MainF = tkinter.Frame(GUI, width = 500, height = 400, bg = "white")
MainF.pack()
ScrollBar = tkinter.Scrollbar(MainF)
ScrollBar.pack(side = tkinter.RIGHT, fill = "y")
TextField = tkinter.Text(MainF, width = 50, height = 20, bd = 5)
TextField.pack(side = tkinter.LEFT)
TextField["yscrollcommand"] = ScrollBar.set

#処理開始ボタン
StartBtn = tkinter.Button(GUI, text = "開始", command = Th_TwoTh.start)
StartBtn.pack()

GUI.mainloop()

スレッド処理を行うためにはthreadingモジュールをインポートしなければいけないので、最初にimport threadingと記述しています。

そして、別スレッドで行いたい処理を以下のコードで指定します。

Th_TwoTh = threading.Thread(target = TwoTh)

変数 = threading.Thread(target = 別スレッドで行いたい処理)
といった感じで指定します。

今回行いたい処理は、TwoTh()という関数で定義しているので、
カッコ内には(target = TwoTh)と記述します。

この状態で、変数.start() を実行することで、
別スレッドで処理が行われるようになります。

変数名はTh_TwoThとしているので、
Th_TwoTh.start()
を実行することで別スレッドでの処理が開始されます。

しかし今回は、GUIの開始ボタンを押してから処理が実行されるようにしたいので、StartBtn = tkinter.Button()のカッコ内に、command = Th_TwoTh.startと記述すると良いことになります。

実行結果は以下の通りです。

Python:スレッド処理の組み立て-3

既に2000まで表示された後の画像を載せてしまっていますが、ちゃんとリアルタイムで1~2000までの数字が順番に表示されました。

これでやっと成功した!と思い、試しにもう一度開始ボタンを押してみると、次のようなエラーが発生しました。

Python:スレッド処理の組み立て-4

RuntimeError: threads can only be started once
と表示されています。

スレッドは1度しか開始できません・・・・とは一体どういうことだ???

スレッド処理に関する知識が無いせいで、このエラーの意味がよく分かりませんでした。

数時間頭を悩ませた後で、

『とりあえず一度別スレッドに投げて処理したものは二度と実行できなくなるものなのかな?・・・』

と適当に解釈しました(笑)

スポンサーリンク

開始ボタンを押す度にスレッドを再定義することで2回目以降を実行できた

一度別スレッドに投げたプログラムをもう一度実行することができないのであれば、開始ボタンを押す度に毎回別スレッドへ投げるようなコードを組めばいいんじゃないかと考え、以下のようなプログラムに書き直してみました。

import tkinter
import threading

#▼▼▼▼▼▼▼▼▼別スレッドで行いたい処理▼▼▼▼▼▼▼▼▼
def TwoTh():
    for i in range(2000):
        TextField.insert(tkinter.END, str(i + 1) + "\n")
        TextField.see("end")

#最初に必ず別スレッドに投げてから処理を開始する関数
def thread():
    Th_TwoTh = threading.Thread(target = TwoTh)
    Th_TwoTh.start()

#▼▼▼▼▼▼▼▼▼GUI項目▼▼▼▼▼▼▼▼▼
GUI = tkinter.Tk()
GUI.geometry("640x400")
GUI.title("1から2000まで順番に表示")
# メインフレーム
MainF = tkinter.Frame(GUI, width = 500, height = 400, bg = "white")
MainF.pack()
ScrollBar = tkinter.Scrollbar(MainF)
ScrollBar.pack(side = tkinter.RIGHT, fill = "y")
TextField = tkinter.Text(MainF, width = 50, height = 20, bd = 5)
TextField.pack(side = tkinter.LEFT)
TextField["yscrollcommand"] = ScrollBar.set
#処理開始ボタン
StartBtn = tkinter.Button(GUI, text = "開始", command = thread)
StartBtn.pack()

GUI.mainloop()

本来実行したい関数TwoTh()の他に、thread()という関数を定義してみました。

thread()の処理内容としては、
TwoTh()を別スレッドへ投げる→別スレッドで処理開始

といった感じです。

開始ボタンを押す度にthread()を呼び出すようにすれば、最初に必ず別スレッドに投げる処理が行われるはずなので、2度目以降の処理が可能になるんじゃないかと考えました。

そして実行してみた結果が次の通りです。

Python:スレッド処理の組み立て-5

予想通り、ちゃんと2回目の実行を行うことができました。

しかし、ここでまた問題が発覚しました。

このままだと、処理実行中に開始ボタンを押した場合、裏で同じ処理が再度実行されてしまうことが分かりました。

これだと開始ボタンを連打してしまった場合、複数のスレッドが一気に立ち上がり、カオスなことになってしまいます。

GUIのボタン入力を受け付けなくするコードを追加して問題を回避

処理実行中でも開始ボタンを押せてしまう不具合を解決するために、処理実行中はボタン押下を受け付けないようにできないか探ることにしました。

するとその方法が見つかったので、その情報を頼りに次のように書き直しました。

import tkinter
import threading

#▼▼▼▼▼▼▼▼▼別スレッドで行いたい処理▼▼▼▼▼▼▼▼▼
def TwoTh():
    for i in range(2000):
        TextField.insert(tkinter.END, str(i + 1) + "\n")
        TextField.see("end")
        StartBtn.config(state = "normal")

#最初に必ず別スレッドに投げてから処理を開始する関数
def thread():
    StartBtn.config(state = "disabled")
    Th_TwoTh = threading.Thread(target = TwoTh)
    Th_TwoTh.start()

#▼▼▼▼▼▼▼▼▼GUI項目▼▼▼▼▼▼▼▼▼
GUI = tkinter.Tk()
GUI.geometry("640x400")
GUI.title("1から2000まで順番に表示")
# メインフレーム
MainF = tkinter.Frame(GUI, width = 500, height = 400, bg = "white")
MainF.pack()
ScrollBar = tkinter.Scrollbar(MainF)
ScrollBar.pack(side = tkinter.RIGHT, fill = "y")
TextField = tkinter.Text(MainF, width = 50, height = 20, bd = 5)
TextField.pack(side = tkinter.LEFT)
TextField["yscrollcommand"] = ScrollBar.set

#処理開始ボタン
StartBtn = tkinter.Button(GUI, text = "開始", command = thread)
StartBtn.pack()

GUI.mainloop()

thread()関数内のコードの始めに、
StartBtn.config(state = “disabled”)

TwoTh()関数内の最後に、
StartBtn.config(state = “normal”)

を追加しました。

StartBtn.config(state = “disabled”)
では、開始ボタンを無効化しています。

StartBtn.config(state = “normal”)
では、無効化したボタンを再度有効化しています。

これにより、開始ボタンを押した直後から数字が2000まで表示される間、ボタン入力を受け付けなくなりました。

Python:スレッド処理の組み立て-6

数字が表示されている間、開始ボタンがグレーアウトしているのが分かると思います。この間は一切ボタン入力を受け付けません。

数字が2000まで表示された後は次のようにボタンが解禁されます。

Python:スレッド処理の組み立て-7

これでボタン連打による最悪の事態?は回避できました。

ただ、僕自身まだまだ勉強不足であるため、本当にこのような解決方法でいいのかはちょっと分からないですね(笑)

今回は以上です。

コメント