Pythonのthreading使ったらプログラムが止まらなかったパターンがある

プログラム「俺は止まんねえからよ…」
ぼく「止まってください」

この記事は、[N高等学校 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/n-highschool) 7日目の記事です。
私は現在2年生2回目(転校するときにいろいろあった)で、通学コースに通っています。
N高は必須課題さえ終わらせれば、プログラミングの時間に自分のプロジェクトができて、非常にありがたいです。

この記事では、PythonでDiscordとTwitterのbotを作っていたら直面した問題について書きます。
何か間違いがあったら指摘してください。

:::note info
**追記（2024/11/22）**
Python3.10以降でdeprecatedになった記述を一部修正し、反映しました。詳細は[コメント](https://qiita.com/KjumanEnobikto/items/ae9458d0be283db1887b#comment-b93200072bc06a425ff8)をご覧ください。

本記事は追記時点から5年ほど前に書かれた古い記事であり、内容の全てが更新されているわけではありません。他にも現在は推奨されていない書き方がなされている可能性があることに留意してください。
:::

## 概要
Pythonのthreadingを使ったプログラムをKeyboardInterrupt(Ctrl+C)で止めようとしたら、なぜか一回で止まらなかった。
さらに調べたら`sys.exit()`でも止まらなかった。

## 環境
OS: Windows 10 Home
Runtime: Python 3.8.0

## 実践

とりあえず、検証のためにthreadingを使ったコードを書いてみることにした。

```python
import threading
import time

# 関数定義
def a():
    for i in range(5):
        time.sleep(1)
        print("A" + str(i))

def b():
    time.sleep(0.5)
    for j in range(5):
        time.sleep(1)
        print("B" + str(j))

# スレッドオブジェクト生成
t1 = threading.Thread(target=a)
t2 = threading.Thread(target=b)

# 実行
t1.start()
t2.start()

# 終わるまで待機
t1.join()
t2.join()

print("Finish")
```
出力は期待通り、以下のようになった。
B4が表示されると同時にFinishが表示された。

```
A0
B0
A1
B1
A2
B2
A3
B3
A4
B4
Finish
```

さて、これを無限ループに改造してみる。

```python
import threading
import time

# 関数定義
def a():
    c=1
    while True:
        time.sleep(1)
        print("A" + str(c))
        c+=1

def b():
    k=1
    time.sleep(0.5)
    while True:
        time.sleep(1)
        print("B" + str(k))
        k+=1

# スレッドオブジェクト生成
t1 = threading.Thread(target=a)
t2 = threading.Thread(target=b)

# 実行
t1.start()
t2.start()

# 終わるまで待機
t1.join()
t2.join()
```

出力は以下の通りになった。

```
A1
B1
A2
B2
A3
B3
^CTraceback (most recent call last):
  File "***********.py", line 28, in <module>
    t1.join()
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/threading.py", line 1011, in join
    self._wait_for_tstate_lock()
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/threading.py", line 1027, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
KeyboardInterrupt
A4
B4
A5
B5
A6
B6
^CException ignored in: <module 'threading' from '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/threading.py'>
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/threading.py", line 1388, in _shutdown
    lock.acquire()
KeyboardInterrupt: 
```

なぜか1回目のKeyboardInterruptで止まっていない。
今回は2回やったら止まったからいいが、できれば避けたい。

## 解決策？

「終わるまで待機」させなければ良い。つまり`**.join()`を消せばいい。
実際に`t1.join()`と`t2.join()`を消して実行してみると、こうなる。

```
A1
B1
A2
B2
^CException ignored in: <module 'threading' from '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/threading.py'>
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/threading.py", line 1388, in _shutdown
    lock.acquire()
KeyboardInterrupt:
```

「While Trueを使うような処理ならどうせ途中で終わらせないだろうしCtrl+Cで止まればええやろ」とか思っていた時期が僕にもありました。
でも止めたいシーンって意外とある。botをコマンドでシャットダウンさせたいときとか。
そんなときに発覚したのが以下の内容だ。

## `sys.exit()`も効かない
```python
import threading
import time
import sys

# 関数定義
def a():
    c=1
    while True:
        time.sleep(1)
        print("A" + str(c))
        c+=1

def b():
    k=1
    time.sleep(0.5)
    while True:
        time.sleep(1)
        print("B" + str(k))
        k+=1

# スレッドオブジェクト生成
t1 = threading.Thread(target=a)
t2 = threading.Thread(target=b)

# 実行
t1.start()
t2.start()

# 強制終了
print("Terminate")
sys.exit()
print("Terminated")
```

期待される出力は`Terminate`1行だ。`sys.exit()`以降はプログラムが実行されないはずだからだ(後述するがこれは厳密には間違いである)。
しかしこれを実際に動作させると、以下のような出力が得られる。

```
Terminate
A1
B1
A2
B2
A3
B3
A4
B4
```

`Terminate`の後に`sys.exit()`が走り、`Terminated`は表示されないようになっているが、上にある2つの関数は普通に実行されてしまっている。

## 原因
`sys.exit()`についての認識に間違いがあった。これはプログラム全部を止めてくれるものではなく、スレッドを停止するものだ。
上記のプログラムで止まるスレッドは`print("Terminate")`を実行しているスレッドだけで、無限ループしているスレッド2つには`sys.exit()`が届いていない。
止める方法の一つとして、`sys.exit()`をスレッドオブジェクト内で実行するというものがある。
だが私がやりたいことは「全てのスレッドを一気に止める」ことだ。

## 解決策

メインスレッド以外のスレッドをデーモン化する。

```python
import threading
import time
import sys

# 関数定義
def a():
    c=1
    while True:
        time.sleep(1)
        print("A" + str(c))
        c+=1

def b():
    k=1
    time.sleep(0.5)
    while True:
        time.sleep(1)
        print("B" + str(k))
        k+=1

def c():
    while True:
        n = input()
        if n == "e":
            print("Terminate")
            sys.exit()

# スレッドオブジェクト生成
t1 = threading.Thread(target=a)
t2 = threading.Thread(target=b)

# デーモン化
t1.daemon = True
t2.daemon = True

# 実行
t1.start()
t2.start()
c() # c()のみメインスレッドで実行

```

入力`e`を受け取ったら`sys.exit()`が走るようにした。
出力結果は以下のようになる。

```
A1
B1
A2
e
Terminate
```

eを押してEnter(Return)すれば、そこで実行が止まる。
デーモンスレッドは**デーモンスレッド以外のスレッドが動いていない場合に自動的に消える**という挙動をする。
`sys.exit()`が含まれる処理が1つであるならばメインスレッドで、すなわち`threading.Thread()`をしないで一番外側で動かすといいかもしれない。

## まとめ
正直、原因も調べてみたけれどわからない。シグナルがどうとか、いくつかそれっぽいものはあったが、自分の技量では正しく書けないので載せるのはやめておく。
この記事を書く過程でthreadingについていろいろと調べたが、まだまだ様々な機能があるらしく、上に載せたもの以外にもやりたいことを実現する方法があるのかもしれない。
ただ、とりあえず自分の理解が及ぶ範囲でやろうとすると上記のようになる。これで詰まったときは、また別の方法を探そうと思う。