Streamlitとスレッドを雑に組み合わせると、以下のスクショみたいに何も受け付けなくなることがありがちだけどどうしよう、というメモ。
結論
thread.daemon = Trueしてからthread.start()しよう。
状況
Streamlitでリアルタイムダッシュボードを作るとき、サブスレッドを作成して、
- メインスレッドでは画面を更新する
- サブスレッドでは最新のデータを取得する
ということをやります。
参考記事:センサーデータをリアルタイムに可視化するStreamlitアプリを作る!(with ESP32)
上記の参考記事を真似ると例えばこんな感じ。
import streamlit as st
import threading
import time
# 数を数えるだけのスレッド
class CountThread(threading.Thread):
def __init__(self):
super(CountThread, self).__init__()
self.count = 0
def run(self):
while True:
print(f"サブスレッドが動作中: {self.count}")
# 本当はここで最新のデータを取得するよ
self.count += 1
time.sleep(1.5)
# 現在の数はこれに表示する。空のプレースホルダー(仮の置き場)の中をどんどん書き換える
ph1 = st.empty()
ph2 = st.empty()
# スレッドのインスタンスをセッション状態に保存する
if st.session_state.get("thread") is None:
st.session_state.thread = CountThread()
st.session_state.thread.start()
# たくさんループする。while TrueでもOK
for i in range(100000):
# st.write()とするところを、(プレースホルダー).writeとするとそのプレースホルダーの中に出力できる
ph1.write(f"メインスレッドが動作中: {i}")
ph2.write(f"バックグラウンドスレッドが動作中: {st.session_state.thread.count}")
print(f"メインスレッドが動作中: {i}")
i = i + 1
time.sleep(1.2)
# ループが終わるまでこのボタンは表示されない
st.button("もう1回")
このプログラムをstreamlit runするとリアルタイムに値が更新されるのですが「いったんサーバを落としたいなー」と思ってCtrl+Cで終了しようとすると、冒頭の状況が発生します。
上記のサンプルコードでサブスレッドと呼んでいる部分だけがゾンビのように動き続けて、でもStreamlitのサーバ自体は終了しようとしているから何も処理を受け付けないんですね…
内部で起きていることの推測
Streamlitは内部で以下のような動きをしているようです。
このとき、Streamlitが責任を持って面倒を見るのはapp.pyのような各アプリが動いているメインスレッドまでで、各アプリが勝手にサブスレッドを起動させるケースは想定していないようです。
当然、メインスレッドが終了しようとしているときにそのことをサブスレッドに伝えたり、サブスレッドを終わらせようとしたりはしないんですね。
解決方法と制約
サブスレッドを、バックグラウンド処理でよく用いられるデーモンスレッドとして作成しましょう。デーモンスレッドは他の通常スレッドがすべて終了すると速やかに終了するスレッドです。
スレッドをデーモンスレッドにするにはthread.start()する前にthread.daemon = Trueを指定するだけ。例えば上記のサンプルコードの場合、以下のようになります。
# スレッドのインスタンスをセッション状態に保存する
if st.session_state.get("thread") is None:
st.session_state.thread = CountThread()
st.session_state.thread.daemon = True
st.session_state.thread.start()
一方でデーモンスレッドは他の通常スレッドがすべて終了するといきなり終了してしまうため、デーモンスレッド内で行っていた処理の後処理を実行するタイミングがありません。何かをロックする、ファイルを書き込んでいる…などの処理をするスレッドでは避けた方がいいでしょう。
上記の理由でデーモンスレッドが使えない場合、他の方法としては次のようなものがありそうです。
- そもそもCtrl+Cで終了するべきではないということで諦める1
- ユーザがアプリ上からサブスレッドを終了できるようにしておく
- サブスレッドが時間経過や一定回数のループで終了するように生存期間を実装しておく。メインスレッドから生存期間を適宜延ばすようにする
-
Streamlit Cloudのような実行環境でバージョンアップするときにトラブりそう… ↩


