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

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

ただいまの
回答率

89.06%

threadingで呼び出した以前のループメソッドを停止したい

解決済

回答 3

投稿

  • 評価
  • クリップ 0
  • VIEW 1,026

tomo1998

score 34

前提・実現したいこと

ボタンを押すとループで1から3までカウントし始めて、ボタンを押しなおすとカウントを中止し、また最初から1から3までカウントする といったプログラムを作っています

ボタンを押すとthreadingでループするメソッドを呼び出し、ボタンをもう一度押すとループを停止するフラグを成立させてまたループするメソッドを呼び出そうとしていますがうまく行きません

該当のソースコード

import threading
import tkinter as tk
import time

LoopFlg=True

def Count():
    global LoopFlg
    LoopFlg=True
    while LoopFlg==True:
        if LoopFlg!=True:
            break
        time.sleep(1)
        if LoopFlg!=True:
            break
        print(1)

        time.sleep(1)
        if LoopFlg!=True:
            break
        print(2)

        if LoopFlg!=True:
            break        
        time.sleep(1)
        print(3)

def Call_Count():
    global LoopFlg
    LoopFlg=False
    threading.Thread(target=Count).start()

root=tk.Tk()
button=tk.Button(root,text="Count",command=Call_Count)
button.pack()
root.mainloop()

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

ボタンを押しなおすと以前のループは停止せず、そのままカウントし始めるのでその結果複数のループが発生してしまいます

試したこと

https://qiita.com/xeno1991/items/b207d55a413664513e5f
のサイトをちょっと読んで真似てみたのですがうまく行きませんでした。
多分、time.sleep()によって処理が止まっている間にボタン押してもループはフラグ成立したことが分からないからなのかなぁ・・・?

import threading
import tkinter as tk
import time

class App():
    def __init__(self):
        self.stop_event = threading.Event() #停止させるかのフラグ

        root=tk.Tk()
        button=tk.Button(root,text="Count",command=self.call_count)
        button.pack()
        root.mainloop()

    def call_count(self):
        self.stop_event.set()
        self.stop_event = threading.Event() 
        threading.Thread(target = self.target).start()

    def target(self):
        while not self.stop_event.is_set():
            time.sleep(1)
            if self.stop_event.is_set():
                break
            print(1)
            if self.stop_event.is_set():
                break
            time.sleep(1)
            if self.stop_event.is_set():
                break
            print(2)
            time.sleep(1)
            if self.stop_event.is_set():
                break
            print(3)            

app=App()
  • 気になる質問をクリップする

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 3

checkベストアンサー

+2

    LoopFlg=False
    threading.Thread(target=Count).start()


が実行されたあと、Count関数の2行目でLoopFlg=Trueされてしまいます。この間に判定が走れば上手くいくかもしれませんが、極めて稀なケースです。

一番直感的な解決は、LoopFlg=Falseのあとに1秒以上待ってみることです。

def Count():
    global LoopFlg
    time.sleep(1.05)  # こっちに書かないとメインスレッドを止めます

    LoopFlg=True

これはほぼ期待通り動きます。とはいえ、余計に待たされてダサいですね。

追記:can110さんのjoinする方法もシンプルで良いと思います。以下の方法とは若干動作が異なります(1秒を待ってから次のスレッドが起動するか、待たないでそのまま切り替わるか)。

押した瞬間に切り替わるようにしたければ、イベントを使うと良いでしょう。ちなみにtime.sleepしている間は何もできませんが、Event.waitであればタイムアウト時間を設定して待たせることも可能です。

全体的に書き換えたコード。命名規則はPEP8に合わせています。

import threading
import tkinter as tk

def count():
    event.clear()
    x = 0
    while True:
        print(x % 3 + 1)
        if event.wait(timeout=1.0):
            break
        x += 1

def call_count():
    event.set()
    threading.Thread(target=count, daemon=True).start()

root=tk.Tk()
button=tk.Button(root,text="Count",command=call_count)
button.pack()

event = threading.Event()
root.mainloop()

単一のスレッドで回し続けると、カウンタ変数を書き換えるだけで簡単に実現できます。

import time
import threading
import tkinter as tk

def count():
    global i
    i = 0
    while True:
        print(i % 3 + 1)
        i += 1
        time.sleep(1)

def call_count():
    global i
    i = 0
    if not th.isAlive():
        th.start()

root=tk.Tk()
button=tk.Button(root,text="Count",command=call_count)
button.pack()

th = threading.Thread(target=count, daemon=True)
root.mainloop()

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/05/01 15:52

    なるほど・・・色々代案ありがとうございます。
    print(x % 3 + 1)と書くことで1から3までのカウントも出来るんですね、ビックリです
    eventの使い方についても良くわかりました
    ありがとうございましたm(_ _)m

    キャンセル

+2

以下のようにスレッド動作していたら.joinで終了待ち後に再生成することで目的の動作ができると思います。

LoopFlg=True
def Count():
    # 略

def Call_Count():
    global LoopFlg,t
    LoopFlg=False
    if t.isAlive():
        t.join()
        t = threading.Thread(target=Count) # スレッド生成
    t.start()

t = threading.Thread(target=Count) # スレッド生成
root=tk.Tk()
button=tk.Button(root,text="Count",command=Call_Count)
button.pack()
root.mainloop()

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

+2

期待通りにならない原因はhayataka2049さんが回答されているので省略します。

このようなプログラムの設計の仕方は色々考えられます。
(A)「ボタンを押す度にスレッドを起こす」方法
(B)「単一のスレッドでカウントアップをし続ける」方法
(C)「afterメソッドを用いてスレッドを起こさない」方法
GUIアプリケーションの定期的処理は(C)が一番自然な気がしますが、ボタンを押してから1秒後に1を表示する前提で制御を考えると(A)も悪い方法ではないかも知れません。

対処:

とりあえず元のコードのとおり(A)を前提とするなら「一つのフラグをあちこちのスレッドからアクセスするのではなく、スレッドごとに専用のフラグを持つ」のがわかりやすい対処であるような気がします。同期待ちのようなことを一切せずに済みますので。

import threading
import tkinter as tk
import time


class App(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.pack()
        button = tk.Button(self, text="Count", command=self.on_count)
        button.pack()
        self.thread = None

    def on_count(self):
        if self.thread is not None:
            self.thread.stop_requested = True
        self.thread = Counter()


class Counter(threading.Thread):
    def __init__(self):
        super().__init__(daemon=True)
        self.stop_requested = False
        self.start()

    def run(self):
        while True:
            for i in range(3):
                time.sleep(1)
                if self.stop_requested:
                    break
                print(i + 1)


root = tk.Tk()
app = App(root)
root.mainloop()

最初can110さんのようにjoinにより既スレッドが止まるのを待ってから次のスレッドを起動・・・と考えたのですが、

・GUIスレッドでThread.joinやtime.sleepなどスレッドをブロックする処理は避ける
・ボタンを押す度に1秒経過してから最初の数値「1」を表示する
・極力単純に・・・

あたりを意識して上のような方法を提案してみました。

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/05/01 15:58

    とっても興味深い回答ありがとう!
    KSwordOfHasteさんの書いたself.threadのような使い方は想像できなかったのでビックリです
    かなりスマートな書き方だと思います!
    ありがと!

    キャンセル

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

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

関連した質問

同じタグがついた質問を見る