• トップ
  • ブログ一覧
  • 【Python】非同期プログラムでマルチスレッドが動かない場合と対処方法
  • 【Python】非同期プログラムでマルチスレッドが動かない場合と対処方法

    広告メディア事業部広告メディア事業部
    2020.03.09

    IT技術

    非同期プログラムでマルチスレッドが思ったように動かない場合は?

    Python(パイソン)では、非同期プログラムを作成する場合、一般的に、Threadクラスを使って「マルチスレッド」にします。

    しかし、これが思うように動かないことが、稀にあります。

    今回は、「非同期プログラムで、マルチスレッドが思ったように動かない場合」と、「その対処方法」について、ご紹介したいと思います!

    通常のマルチスレッドの実現方法

    言語によっては面倒なマルチスレッドも、Python では非常に簡単に実現できます。

    例えば、「メインスレッドで a ~ g」、「サブスレッドで 1 ~ 7」を出力する場合、以下のようなコードになります。

    コード

    1import threading
    2
    3# サブスレッドで実行する関数
    4def sub_thread():
    5    for i in range(7):
    6        print(i)
    7
    8# メイン処理
    9if __name__ == "__main__":
    10    # サブスレッドを起動
    11    sub = threading.Thread(target=sub_thread)
    12    sub.start()
    13
    14    # メインスレッドでループ処理
    15    for s in range(ord('a'), ord('g')):
    16        print(chr(s))

    コードの解説

    コードを、簡単に解説します。

    threading

    threading は、スレッド操作するためのモジュールです。

    まず、これを import します。

    sub_thread()

    sub_thread() は、サブスレッドで動かすための関数です。

    この中で、「1 ~ 7」をループで出力しています。

    サブスレッドの起動

    メイン処理では、まずサブスレッドを起動しています。

    サブスレッドにセット

    Threadクラスの、コンストラクタの引数に、先程の関数 sub_thread を渡します。

    これで、この関数は、サブスレッドにセットされました。

    サブスレッドを開始

    start()  メソッドで、サブスレッドを開始します。

    ループ出力

    メインスレッド内で「a ~ f」をループで出力しています。

    実行結果

    このプログラムの実行結果は、以下のようになります。

    0
    1
    a
    b
    c
    d
    e
    f
    2
    3
    4
    5
    6

    非同期で実行しているので、「a ~ f」と「1 ~ 7」が、適当に混ざって出力されています

    これが、非同期のマルチスレッド処理の例です。

    マルチスレッドで画像を動画として再生してみる

    さて、それではマルチスレッドを使って、画像を動画として表示してみましょう!

    動画として表示するコード

    1import wx
    2import cv2
    3import threading
    4import time
    5
    6class VideoBitmap(wx.StaticBitmap):
    7    def __init__(self, parent, file_name):
    8        self.video = cv2.VideoCapture(file_name)
    9        ret, frame = self.video.read()
    10        bmp = self.create_wx_bitmap_from_cv2_image(frame)
    11        super().__init__(parent, wx.ID_ANY, bmp, (0, 0), (1024, 768), style=0, name='')
    12
    13    def create_wx_bitmap_from_cv2_image(self, cv2_image):
    14        height, width = cv2_image.shape[:2]
    15        cv2_image_rgb = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2RGB)
    16        return wx.BitmapFromBuffer(width, height, cv2_image_rgb)
    17
    18    def run(self):
    19        frame_rate  = int(self.video.get(cv2.CAP_PROP_FPS))
    20        while True:
    21            is_read, frame = self.video.read()
    22            if not is_read:
    23                break
    24            bmp = self.create_wx_bitmap_from_cv2_image(frame)
    25            self.SetBitmap(bmp)
    26            time.sleep(frame_rate/1000)
    27
    28if __name__ == "__main__":
    29    app = wx.App(False)
    30    file_name = "video/video.mp4"
    31    frame = wx.Frame(None, wx.ID_ANY, "", size=(1024,768))
    32    panel = wx.Panel(frame, size=(1024,768))
    33    layout = wx.FlexGridSizer(rows=1, cols=1, gap=(0,0))
    34    panel.SetSizer(layout)
    35
    36    video = VideoBitmap(panel, file_name)
    37    layout.Add(video)
    38    frame.Show(True)
    39
    40    thread = threading.Thread(target=video.run)
    41    thread.start()
    42
    43    app.MainLoop()

    このコードを動かすには、ソースをおいたフォルダ直下に「videoフォルダ」を作成。

    そこに、動画ファイル「video.mp4」を置いてください。(動画の内容は問いません)

    コードの解説

    コードの解説をします。

    動画取得とGUI操作

    動画の画像取得に「cv2 モジュール(OpenCV)」、GUI操作には「wx モジュール(wxPython)」を使っています。

    動画再生

    動画再生用に、簡単なクラス「VideoBitmap」を作成し、それの実行メソッド run() を、サブスレッドとして実行しています。

    VideoBitmap

    VideoBitmap は、「wxPython」の「StaticBitmap」を継承しています。

    つまり、「動画も再生できる Bitmap クラス」というわけです。

    動画からの画像取得

    動画からの画像取得は、create_wx_bitmap_from_cv2_image() メソッドで行っています。

    run() メソッド

    run() メソッドでは、

    1、OpenCV でフレームを読み込み
    2、それをこのメソッドで画像に変換
    3、自身にセットして描画する

    という処理を、フレームが読み込める間、ループで繰り返します

    sleep()

    sleep()  は再生速度の調整 (frame_rate)  に使っています。

    実行後の状態

    さて、このコードを実行すると、以下のどちらかの状態になるかと思います。

    1. 1枚めの画像は表示されるが、あとは真っ白
    2. 動画が表示されるが、ちらつきが激しい

    いずれにしても、このままだと使い物になりません…。

    マルチスレッドで動画が正常に表示されない理由

    理屈的には、先ほど紹介したコードで正しいはずです。

    しかし「思ったように動かない!」

    こういう時は、その「クラスがマルチスレッドに対応しているか」どうかを疑ってみましょう。

    wxPython の StaticBitmap クラスがマルチスレッドに対応しているかは不明

    今回は、wxPython の StaticBitmap を継承して、動画再生クラスを作りました。

    しかし、そもそも wxPython の StaticBitmap クラスが、マルチスレッドに対応しているかどうか分かりません。

    (wxPython のドキュメントを見ても、対応しているかどうか明記されておりません。)

    マルチスレッド対応しているかチェックする必要性がある

    今回の事象は、使用しているクラスが、「マルチスレッドに対応していないことが原因」である可能性が高いと考えられます。

    Python の場合、マルチプラットフォーム対応なので、更に話が複雑になります。

    つまり、例えば「Windows10 では動作しても、Linux や Mac では動作しない」といった事象も、あり得るということです。

    マルチスレッドでクラスを使う場合は、それが(対象プラットフォームで)マルチスレッド対応しているかどうか、をチェックする必要があります。

    マルチスレッドで思ったように動かないときの対処方法

    マルチスレッドが正しく動かないのなら、「シングルスレッド」で処理します。つまり、メインスレッドだけで処理を行うしかありません。

    マルチスレッドは、複数のスレッドが並行して走ることを指します。

    ですが、これは見方を変えれば、スレッドを切り替えながら実行しているということです。

    ということは、シングルスレッドでも、処理の流れを切り替えながら並行して実行できれば、擬似的にマルチスレッドを実現できるわけです。

    timerイベントを使う

    今回、このために「timer イベント」を使ってみたいと思います!

    timer  は、指定した間隔でタイマー割り込みをかけ、現在処理中の箇所から、timer イベントに処理を切り替えるものです。

    それでは、先のコードを、timer  を使って書き換えてみましょう。

    timerを使ったコード

    1import wx
    2import cv2
    3import threading
    4import time
    5
    6class VideoBitmap(wx.StaticBitmap):
    7    def __init__(self, parent, file_name):
    8        self.video = cv2.VideoCapture(file_name)
    9        ret, frame = self.video.read()
    10        bmp = self.create_wx_bitmap_from_cv2_image(frame)
    11        super().__init__(parent, wx.ID_ANY, bmp, (0, 0), (1024, 768), style=0, name='')
    12
    13        # timer処理用
    14        self.interval = 1000/int(self.video.get(cv2.CAP_PROP_FPS))
    15        self.timer = wx.Timer(self)
    16        self.Bind(wx.EVT_TIMER, self.on_timer)
    17
    18    def create_wx_bitmap_from_cv2_image(self, cv2_image):
    19        height, width = cv2_image.shape[:2]
    20        cv2_image_rgb = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2RGB)
    21        return wx.BitmapFromBuffer(width, height, cv2_image_rgb)
    22
    23    # 再生開始
    24    def start(self):
    25        self.timer.Start(self.interval)
    26    
    27   # タイマーイベント(動画を1フレーム再生)
    28    def on_timer(self, event):
    29        self.timer.Stop()
    30        is_read, frame = self.video.read()
    31        if not is_read:
    32            return
    33        bmp = self.create_wx_bitmap_from_cv2_image(frame)
    34        self.SetBitmap(bmp)
    35        self.timer.Start(self.interval)
    36
    37if __name__ == "__main__":
    38    app = wx.App(False)
    39    file_name = "video/video.mp4"
    40    frame = wx.Frame(None, wx.ID_ANY, "", size=(1024,768))
    41    panel = wx.Panel(frame, size=(1024,768))
    42    layout = wx.FlexGridSizer(rows=1, cols=1, gap=(0,0))
    43    panel.SetSizer(layout)
    44
    45    video = VideoBitmap(panel, file_name)
    46    layout.Add(video)
    47    frame.Show(True)
    48
    49    # 動画再生スタート
    50    video.start()
    51
    52    app.MainLoop()

    コードの変更点

    コードに下記の変更を行いました!

    初期処理内にて

    初期処理内で timer イベント用の処理を、3行追加しています。

    interval

    interval  は、timer イベントの発生間隔を示すもので、フレームの再生レートをセットしています。

    timer インスタンスが、timer イベントを制御する本体で、これを使うと、timer イベント(EVT_TIMER)を発生させることができます。

    Bind() メソッド

    最後の、Bind() メソッドは、イベントとメソッドを紐付ける(Bind する)ために、実行しています。

    on_timer() メソッド

    timer イベントで呼ばれる、on_timer() メソッド内では、動画を1フレーム読み込み、それを bitmap に変換して自身にセットしています。

    この timer イベントを使うと、擬似的にマルチスレッド的な処理を行うことができます

    処理後の画像

    動画だったので、画像を入れても伝わらないので入れるかどうか迷いましたが…。分かりにくい画像で申し訳ありません(笑)

    さいごに

    今回は、Pythonにおいて「マルチスレッドが思ったように動作しない時の原因」と、「その回避方法」をご紹介しました!

    通常は、3つ以上のスレッドが行き交うような複雑なスレッド処理でなく、2つのスレッドを並行処理する場合が大半です。

    そうした場合に、思ったようなスレッディングができない場合は、利用しているクラスが「マルチスレッド非対応」である可能性を疑いましょう。

    そして、それを回避するためには timer イベントを利用した、疑似並行処理が有効です。

    困った時には、ぜひお試しください!

    こちらの記事もオススメ!

    featureImg2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    広告メディア事業部

    広告メディア事業部

    おすすめ記事