質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.22%

Python3のtkinterで並行処理?を行いたい

解決済

回答 3

投稿

  • 評価
  • クリップ 2
  • VIEW 3,900

tomo1998

score 27

tkinterで、スタートボタンとストップボタンを生成し、スタートボタンが押されれば数字をカウントし始め、ストップボタンが押されればカウントをストップ といったプログラムを作ろうと思っています。

ースタートボタンを押したらスタートメソッドを呼び出してメソッド内のwhile文でカウントが始まり、ストップフラグが立っていないかif文で確認し、フラグが立っていればwhile文をbreakで抜ける。
ーストップボタンを押したらストップフラグが立つ。

というように作ろうとしたのですが、while文が一度反応すればwhile文による無限ループから離脱できなくなり、ボタンも固まりプログラムがフリーズしてしまいます。

 発生している問題・エラーメッセージ

・無限ループから抜け出せずプログラムがフリーズする

 該当のソースコード

python3

import tkinter as tk
import threading

def start():
    global frame
    global stop

    while True:
        if stop==False:
            break
        else:
            print(frame)
            frame=frame+1

def stop():
    global stop
    stop=False      

######ここからはメソッド外

frame=1
stop=True
root=tk.Tk()
Button001=tk.Button(root,text="Start",command=start)
Button001.pack()
Button002=tk.Button(root,text="End",command=stop)
Button002.pack()
root.mainloop()

 試したこと

ネットで調べたら、threadingを使えば並行処理ができると聞いたので、試した結果プログラムはフリーズしなくなり、ボタンも押せるようになったのですがどういったわけかストップボタンを押してストップフラグを立たせてもループが止まりません・・・

 threadingを使った場合のコード(『ここからはメソッド外』以降から

frame=1
stop=True
thread_1 = threading.Thread(target=start)
thread_2 = threading.Thread(target=stop)

root=tk.Tk()
Button001=tk.Button(root,text="Start",command=thread_1.start())
Button001.pack()
Button002=tk.Button(root,text="End",command=thread_2.start())
Button002.pack()
root.mainloop()

もしかしたら簡単すぎるミスがどこかにあるかもしれないのですが、自分でいろいろ試してもわかりませんでした・・・
どなたかご回答お願いします。m(_ _)m
あとわかりにくかったらすいません

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

checkベストアンサー

+3

まずは動作しない原因を2点ほど

1.
tk.Button() の command引数に渡すのは関数オブジェクトとなりますので、

Button001=tk.Button(root,text="Start",command=thread_1.start())
Button001.pack()
Button002=tk.Button(root,text="End",command=thread_2.start())
Button002.pack()


の部分は

Button001=tk.Button(root,text="Start",command=thread_1.start)
Button001.pack()
Button002=tk.Button(root,text="End",command=thread_2.start)
Button002.pack()


の間違いかと思います。

上の間違った記述だと、thread_*.start() を実行 して、その結果を command に渡すという意味になってしまいます。

2.
stop という名前が 関数名 def stop() とフラグ stop=True の2つの使用している為、stop() が動作しておりませんので、どちらかの名前を変更してください。

とりあえず、フラグ名を stop_flag に変更するなどで良いかと思います。

上記の変更を行うことで、『とりあえず』は動作するかと思います。


ただし・・。上記の処理では
・threadを停止した後に threading.join()による リソースの回収処理部がない
・threadの開始処理(start())は1度だけしかできないので、2度目の開始ができない
・アプリケーションが終了したときのthread停止処理が入ってない

等などの問題があります。

個人的には
・現状のstart()stop()処理をそのままスレッドで実行する

のではなく、

・スレッド処理部は start()stop()の関数以外で行う
start() にてスレッドの生成やstart()処理を行う
stop() にてスレッドの停止やjoin()処理を行う

といった処理にするほうが良いかと思います。

以下は軽くサンプルです。

import tkinter as tk
import threading
import time

def time_count():
    global frame
    global stop_flag

    while not stop_flag:
        print(frame)
        frame=frame+1
        time.sleep(1)

def start():
    global stop_flag
    global thread

    # スレッドが無いなら生成してstart()する
    if not thread:
        thread = threading.Thread(target=time_count)
        stop_flag=False
        thread.start()

def stop():
    global stop_flag
    global thread

    # スレッドがある場合停止してjoin()する
    if thread:
        stop_flag=True
        thread.join()
        thread=None

frame=1
stop_flag=False
thread=None
root=tk.Tk()
Button001=tk.Button(root,text="Start",command=start)
Button001.pack()
Button002=tk.Button(root,text="Stop",command=stop)
Button002.pack()
root.mainloop()
# 終了時にスレッドを停止する処理
stop_flag=True
thread.join()

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/09/19 23:41

    commandのところは完全にうっかりミスでした・・・w
    つい関数宣言と同じ感覚で・・・w
    ()ついたままだと、結果をcommandに渡す、ということになるんですね
    同じ名前を使っちゃいけないことも知りませんでした(;'∀')
    自分が無知すぎたorz
    ちなみに、リソースの回収、threading.join()とは何ですか?
    よくわからないので教えていただけると幸いです。

    キャンセル

  • 2018/09/20 08:47

    thread.join() は要はthreadの停止(threadに割り振った関数が終了したこと)を待つ関数です。
    上記のコードではstopフラグをセットにすることによりループを抜けさせて停止処理を行っているのですが、もっと複雑なコードになってくるとループ内で待ち状態になっていたり、重い処理を行っている等で直ぐにループを抜けるとは限らない訳す。(私のサンプルでもループ内sleep()がありますので、ループを抜けるまで1秒以上かかる可能性があるわけです)
    にも関わらず再度"start"が呼ばれて新しいthreadを生成してstopフラグをクリアする処理をしてしまうと、前のthreadも裏で同時に動き続けるという事態になってしまい、不具合の原因となりかねない上に、CPU資源やメモリー資源を無駄に消費してしまいます。
    そのため、threadの停止処理を行う場合は thread.join() を呼びthreadが確実に終了(つまりCPU資源やメモリー資源の解放)してから次の処理を行うという実装にするほうが良いかと思います。

    キャンセル

+3

主な問題点は

  • 関数stopとフラグ変数stopの名前が同じ
    言語によっては関数的なものと変数的なものを同じ名前で定義できます。例えばJavaは同じ名前のフィールド(変数的なもの)とメソッド(関数的なもの)を同一クラスに定義できます。しかしPythonでは同じスコープ(本質問ではグローバル空間)に同じ名前の関数と変数を同時には定義できません。def stop(): ...で関数を定義しても、そのあとでstop = Trueなどとしてしまうと「stopという名前に格納されていた関数値がFalseで上書きされる」ため、stopはもはや関数ではなくなるのです。またこのような病的なコードを実行してもpythonインタープリタはエラーを報告しません。「他の値で上書きすることを前提として関数定義をする」という手法はまず使わないでしょうから本来は「defにより関数定義したら同じスコープでその関数名を他の目的(変数)では使わない」とすべきでしょう。
    スレッドを使うコードに修正した際にEndボタンを押してもwhileループが完了しない原因はthreading.Thread(target=stop)を呼び出したときのstopの値が「関数ではなくTrueという値になってしまっている」からです。

  • Buttonのcommand引数
    この引数に渡すべきは「ボタンが押された時点で実行すべき関数」です。質問者さんのコードでは
    Button001=tk.Button(root,text="Start",command=thread_1.start())
    となってますが、これではstartが起動されるのはボタンが押されたときではなく、ボタンを生成したときになってしまいます。
    ボタンが押されたときにstartしたい場合は
    Button001 = tk.Button(root, text="Start", command=thread_1.start)
    などと書くべきです。スレッドを用いるようにしたコードではStartボタンを押さなくてもwhileループが始まっていますよね?

  • Endボタンの処理は別スレッドで行う必要なし
    別スレッドで行うと動かなくなるわけではありませんが、そうすることに意味はありません。
    Buttonのコンストラクターのcommand引数に指定するのはあくまで関数であり、それはボタンが押されたときに呼び出されます。またその関数が終了するまではtkinterのイベント処理は動かなくなります。それゆえEndStartボタンのcommandで直接whileループするような関数を指定するとご質問にあるような問題が起きます。これをスレッドを用いて回避すると
    (A) ボタンを押したら別のスレッドを開始し即座に呼び出し元へ戻るようにする
    (B) whileループは別のスレッドで実行する。
    となるわけで「tkinterのイベント処理はメインスレッドで継続しつつ、whileループは別のスレッドで並行して動かす=>tkinterのイベント処理がwhileループにより阻害されないようにする」という考え方になります。一方Endボタンの処理は「単にフラグを変更する」だけなので、それを行うのにわざわざスレッドを動かす必要はありません。

  • startボタンは一度しか動作しません
    同一のスレッドインスタンス(thread_1に代入されている値)を開始できるのは一度きりです。よってStart/Endボタンを複数回動かすならStartボタンを押すたびにスレッドインスタンスを新たに生成しなければならない点に注意してください。(一度だけでよいなら元のままでも構いません)

以上の点に着目し次のように修正してみました。

import tkinter as tk
import threading


def run():  # startという名前のままでもよいがわかりやすさのためrunにした
    global frame, stop

    stop = False  # フラグの名前がstopなのですから停止させるまではFalseにするのが自然です
                  # 動いている間Trueであるようなフラグにするならrunningなどとすべきでしょう
                  # こういう「名前のおかしさ」は第三者のコードの理解を阻害します
    while not stop:  # 論理が冗長なので若干修正
        print(frame)
        frame += 1

def on_start():
    threading.Thread(target=run).start()

def on_stop():
    global stop
    stop = True

# ここからは関数外(on_stopなどはグローバルな関数であって普通メソッドとは呼びません)

frame = 1
root = tk.Tk()
Button001 = tk.Button(root, text="Start", command=on_start)
Button001.pack()
Button002 = tk.Button(root, text="End", command=on_stop)
Button002.pack()
root.mainloop()

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/09/19 10:44

    のんびりコメントしてたらmagichanさんとかぶった回答をしてしまいました。また自分の回答ではmagichanさん指摘のスレッドの後始末に言及できてませんでした~

    キャンセル

  • 2018/09/19 23:22

    なんと!
    Python3では同じ名前を使うと上書きされてしまうんですね・・・知りませんでした(;'∀')
    次回からは分かりやすいように名前を変えてみます~
    Button Widgetのcommand引数はうっかりミスでした(-_-;)
    つい関数宣言と同じ感じで記入してしまい・・w 申し訳ない!
    確かにEndボタンは別スレッドを使う必要なさそう

    試してみたところスレッドインスタンスは確かに一度しか作動できないみたいですね・・・2度実行させようとしたらエラーが発生していました

    おかげさまでとてもよく理解&解決できました!
    ご丁寧な回答ありがとうございました!

    キャンセル

+2

参考URL
簡易ストップウォッチを作ってみた @Python3

tkinter使用するのは初めてなので質問のコードをコピペして少しづつ削りながら動作確認してると
ほとんどが無くなりました。
最低限Endボタンが機能するようになったのでご参考までに。

動くか動かないかの違いはここだけの気がします。
if __name__ == '__main__':
# coding=UTF-8
import tkinter as tk
import threading

def start():
    print('start!')
def stop():
    print('end!')

# main
if __name__ == '__main__':
    root=tk.Tk()
    Button001=tk.Button(root,text="Start",command=start)
    Button001.pack()
    Button002=tk.Button(root,text="End",command=stop)
    Button002.pack()
    root.mainloop()

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/09/19 23:28

    ご回答ありがとうございます・・・っ!
    確かにボタンは作動するのですが、ちょっと希望していたのと違う感じに~(;'∀')
    説明が下手で申し訳ないです・・・
    「if __name__ == '__main__':」は、その実行されたプログラムがモジュールとして作動していなければ実行するコードなので、ちょっと関連性がない気がします・・・すいません><

    キャンセル

  • 2018/09/19 23:31

    この回答した後自分でも作ってみてハマったので似たような質問してます。
    よかったら参考までに御覧ください。

    キャンセル

  • 2018/09/19 23:36

    なんと!早速見てみます!w

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.22%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる