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

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

ただいまの
回答率

89.13%

Pythonのthreadingにおける、終了時の処理

解決済

回答 2

投稿

  • 評価
  • クリップ 2
  • VIEW 19K+
退会済みユーザー

退会済みユーザー

前提・実現したいこと

Pythonを使って、動画や画像を表示するGUIを作っています。その中で、みなさまのご支援を頂ながらカメラの画像を取り込んで再生できるところまで来ました。

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

GUIのウインドウを閉じると、GUIを閉じることはできるのですが、threadがまだ残っているのかコマンドプロンプトのコントロールが帰ってきません。

該当のソースコード

import wx
import os
import cv2
import numpy as np
import threading
import sys

GUI_IMAGE_X = 240

GUI_IMAGE_Y = 180
CAMMODE = False

########################################################################
class DnDPanel(wx.Panel):

    #----------------------------------------------------------------------
    def __init__(self, parent):
        wx.Panel.__init__(self, parent=parent)

        # Image box
        img = wx.Image(GUI_IMAGE_X,GUI_IMAGE_Y)

        # Statc bitmap
        self.imageCtrl = wx.StaticBitmap(self, wx.ID_ANY,wx.Bitmap(img))

        # Text box
        self.fileTextCtrl = wx.TextCtrl(self,style=wx.TE_MULTILINE|wx.HSCROLL|wx.TE_READONLY)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.imageCtrl, 1, wx.ALL, 5)
        sizer.Add(self.fileTextCtrl, 1, wx.EXPAND|wx.ALL, 5)
        self.SetSizer(sizer)
        self.Fit()

    #----------------------------------------------------------------------
    def updateText(self, text):
        #Overwtite a text
        self.fileTextCtrl.SetValue(text)

        #Add a text
        #self.fileTextCtrl.WriteText(text)

    #----------------------------------------------------------------------
    def updateImage_Entity(self, cv2image):

        # Calculate aspect rat
        mag_X = GUI_IMAGE_X / cv2image.shape[1]
        mag_Y = GUI_IMAGE_Y / cv2image.shape[0]

        if mag_X < mag_Y:
            img = cv2.resize(cv2image, None, fx = mag_X, fy = mag_X)
        else:
            img = cv2.resize(cv2image, None, fx = mag_Y, fy = mag_Y)

        # Calculate aspect ratio diff
        # X-axis
        diff_X = GUI_IMAGE_X - img.shape[1]
        diff_X_start = round(diff_X/2)
        diff_X_end = diff_X_start + img.shape[1]

        # Y-axis
        diff_Y = GUI_IMAGE_Y - img.shape[0]
        diff_Y_start = round(diff_Y/2)
        diff_Y_end = diff_Y_start + img.shape[0]
        #print("[(%s,%s) : (%s,%s)]"%(diff_X_start,diff_Y_start,diff_X_end,diff_Y_end))

        # Create modified picture
        bg_image = np.zeros((GUI_IMAGE_Y, GUI_IMAGE_X, 3), np.uint8)

        bg_image[diff_Y_start:diff_Y_end,diff_X_start:diff_X_end] = img
        # BGR --> RGB
        img = cv2.cvtColor(bg_image, cv2.COLOR_BGR2RGB)

        # Convert color array to bitmap image
        img = wx.Bitmap.FromBuffer(img.shape[1], img.shape[0], img)

        #Static bitmap
        self.imageCtrl.SetBitmap(img)


########################################################################
class CameraControl(threading.Thread):

    def __init__(self,parent):
        threading.Thread.__init__(self)

        # Prepare camera mode
        self.panelText = DnDPanel(parent).updateText
        self.panelImage =  DnDPanel(parent).updateImage_Entity

    #----------------------------------------------------------------------
    def run(self):

        global CAMMODE
        if CAMMODE == False:
            return False

        # End other daemon thread at first
        while threading.activeCount() > 2:
             CAMMODE = False

        # Start only one daemon thread
        CAMMODE = True

        while CAMMODE == True :
            # Get camera information : "0" is device numer
            cap = cv2.VideoCapture(0)
            ret, frame = cap.read()
            while ret == False:
                # Get camera information : "0" is device numer
                cap = cv2.VideoCapture(0)
                ret, frame = cap.read()
                self.panelText("ERROR: Camera is not working.")
                return False

            while ret == True:
                cv2.waitKey(1)
                ret, frame = cap.read()
                self.panelText("Camera mode")
                # Load an image to entity from file
                self.panelImage(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))

                if CAMMODE == False:
                    ret = False

########################################################################
class DnDFrame(wx.Frame):
    #----------------------------------------------------------------------
    def __init__(self):
        self.window = wx.Frame.__init__(self, parent=None, title="GUI test", style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER)

        self.InitMenu()
        self.panel = DnDPanel(self)
        self.Fit()

        self.cc = CameraControl(self)
        self.cc.setDaemon(True)
        self.cc.start()

        self.Show()

    #----------------------------------------------------------------------
    # Initialize
    def InitMenu(self):
        # Menubar
        menubar = wx.MenuBar()
        fileMenu = wx.Menu()
        f_item2 = fileMenu.Append(wx.ID_ANY, '&Camera(C)', 'Load image from USB camera')
        fileMenu.AppendSeparator()
        f_item3 = fileMenu.Append(wx.ID_ANY, '&Quit(Q)', 'Quit application')
        menubar.Append(fileMenu, '&File(F)')

        self.SetMenuBar(menubar)

        # Event call definition
        self.Bind(wx.EVT_MENU, self.OnCamera, f_item2)
        self.Bind(wx.EVT_MENU, self.OnQuit, f_item3)

        # Frame definition
        self.Centre()

        # Status bar
        self.CreateStatusBar()
        self.setstatusbarTXT("")

        self.Show(True)

    #----------------------------------------------------------------------
    # Event hander: set status text
    def setstatusbarTXT(self, msg):
        self.SetStatusText(msg)

    #----------------------------------------------------------------------
    # Event: Quit
    def OnQuit(self,event):
        global CAMMODE
        CAMMODE = False

        sys.exit()

    #----------------------------------------------------------------------
    # Event: Camera
    def OnCamera(self, event):
        global CAMMODE
        CAMMODE = True
        self.panel.updateText("Camera mode")
        self.setstatusbarTXT("Camera mode")

        self.cc = CameraControl(self)
        self.cc.setDaemon(True)
        self.cc.start()

#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    frame = DnDFrame()
    app.MainLoop()

試したこと

class CameraControlのrunにあるwhileloopの中にprint("foobar")のようなでバグ文字列を吐かせましたが、GUI終了時にはデバグ文字列は出てきませんでしたので、whileloopで躓いているわけではなさそうです。

補足情報

Win10(64bit) 
Python 3.5.3(64bit)     
wxPython 4.0.0a1 (Phoenix)

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 2

checkベストアンサー

+2

昔に書いたコードなので詳細は覚えてないですが、どちらの方法でもmain終了時にthread側を終了できました。参考まで。

方法1

  • thread側でdaemon = Trueとして、main側の終了時にthread側も終了するようにする。
# -*- coding: utf-8 -*-
import sys, time, datetime
import threading

# 排他制御付print
def print_lock( ctx, str):
    with ctx["lock"]:
        print( str)

# スレッド処理クラス
class TestThread( threading.Thread):

    def __init__(self, ctx):
        super( TestThread, self).__init__()
        self.daemon = True  # メイン終了時にもスレッド終了する
        self.ctx = ctx

    def run( self):
        print_lock( self.ctx, "sub loop start---")
        while True: # 無限ループ
            time.sleep(0.5)
            print_lock( self.ctx, "sub  : " + str(datetime.datetime.today()))

        print_lock( self.ctx, "sub loop end---")

if __name__ == '__main__':
    # mainとスレッドで共有するデータ
    ctx = {"lock":threading.Lock()}

    th = TestThread( ctx)
    th.start()

    print_lock( ctx, "main loop start---")
    for i in range(5):
        time.sleep(1)
        print_lock( ctx, "main : " + str(datetime.datetime.today()))

    print_lock( ctx, "main loop end---")

方法2

  • main側から終了フラグctx["stop"]にてthread側の処理を終了できるようにする。
  • main側にて.join()にてthread側の処理を待つ。
# -*- coding: utf-8 -*-
import sys, time, datetime
import threading

# 排他制御付print
def print_lock( ctx, str):
    with ctx["lock"]:
        print( str)

# スレッド処理クラス
class TestThread( threading.Thread):

    def __init__(self, ctx):
        super( TestThread, self).__init__()
        self.ctx = ctx

    def run( self):
        print_lock( self.ctx, "sub loop start---")
        while True: # 無限ループ
            if self.ctx["stop"]: # main側から終了を指示されたら終了
                break
            time.sleep(0.5)
            print_lock( self.ctx, "sub  : " + str(datetime.datetime.today()))

        print_lock( self.ctx, "sub loop end---")

if __name__ == '__main__':
    # mainとスレッドで共有するデータ
    ctx = {"lock":threading.Lock(), "stop":False}

    th = TestThread( ctx)
    th.start()

    print_lock( ctx, "main loop start---")
    for i in range(5):
        time.sleep(1)
        print_lock( ctx, "main : " + str(datetime.datetime.today()))

    print_lock( ctx, "main loop end---")
    ctx["stop"] = True # スレッド側に終了を指示
    th.join()          # スレッドの終了を待つ

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2017/05/20 12:30 編集

    can110様
    実際に動くコードを出して説明していただきありがとうございました。
    私の書いたコードとcan110様のコードを混ぜ合わせながら動作確認をしたところ、無事動かすことが出来ました。

    原因は、カメラの画像を無限ループで拾い続ける中で、sleepが挟まらないと処理が正常に終了できない、というところにありました。

    ---(処理のイメージ)---
    メインループ:wxPythonでGUI制御
    子分スレッド:CameraControlでカメラ画像を無限ループで取り込み
    ※1 この無限ループの中でtime.sleep(0.5)がないと処理が終了できない
    ※2 私の環境ではsleep時間が、0.07では半々でクラッシュ、0.08なら時々クラッシュ、0.09でまれにクラッシュ、0.1でOKでした。

    正常に動作させるために「わざとsleepを挟む」のが不思議な気がします。imshowのあとのwaitkey(1)も同じ挙動をしますよね…

    キャンセル

  • 2017/05/20 13:16

    pythonでのスレッドの詳細仕様は確認していないのですが、
    一般的にはsleep(0.01)なりでもよいので呼び出すことによって、他のスレッドにも処理する余裕を与えます。
    また、クラッシュする原因についてはソースを確認しておらず推測なのですが
    同時にアクセスすべきではない(キャプチャ画像?)データに複数スレッドで同時アクセスしているためかもしれません。そのようなデータに対しては、一般的に排他(同期)アクセスする必要があります。
    私のコードではthreading.Lock()オブジェクトで標準出力(print)へ排他アクセスをしています。

    キャンセル

  • 2017/05/20 16:51

    can110様
    詳しいご解説をありがとうございます。
    問題がどこにあるかが見えてきましたので、排他アクセスとキャプチャ周りの挙動について、もう少し何が起きているかを調べたいと思います。

    キャンセル

+1

Threadの終了処理の基本は以下の2パターンで、今回は後者を選択すると良いと思います。

from threading import Thread
import time
import sys


class MyThread(Thread):
    des run(self):
        time.sleep(9999)


t = MyThread()
t.setDaemon(True)  # メインスレッドが終了すればこのスレッドも有無をいわさずに落とす
t.start()
sys.exit(0)
from threading import Event
from threading import Thread
import time
import sys


class MyThread(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.stopping = Event()

    def run(self):
        while not self.stopping.is_set():
            time.sleep(1)


t = MyThread()
t.start()
t.stopping.set()  # 終了中フラグを立てる
t.join()   # スレッドが終了するまで待つ
sys.exit(0)

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2017/05/21 07:00

    Youhei様
    Event()を使ったフラグ管理のご回答をありがとうございます。
    sleepの時間を1秒にすると1秒、5秒に変更すると5秒後に終了するのを確認しました。
    後者のやり方はとてもシンプルで分かりやすい方法でした。

    キャンセル

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

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