ユーザーのお願いを受けとってくださいおねがいします　〜そんな悩みをthreading.Threadで解決〜

# 項目案内

技術内容だけ知りたい方は「スレッドの要点」へ飛んでください。

# こんなものを作っています
「帰宅時快適！　室温快適化システム！」というシステムをゼミで作っています。
季節によって家が暑くなったり、寒くなったりして家に帰ってきた時不愉快という問題点を解決するためのシステムです。

以下はシステムの構成図です。
<img width="607" alt="スクリーンショット 2019-10-29 17.17.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219573/5fe6f99b-d16b-7587-9d28-0a979fad80a3.png">

Slackから特定のメッセージを送ると、赤外線発振回路が赤外線を送信し、エアコンを操作できる仕組みになっています。
言ってしまえばスマートリモコンですね。

ただ、これだけだと他のものと同じになってしまうので、室内温度を**素早く最適温度にする**ことを売りにするつもりでいます。

というわけで、室内の温度を**常に監視し**、その**監視結果に合わせて設定温度の変更を行うようにしようとしていた**のですが、ここで、ある問題に気づきました。

# 室内温度自動制御を素直に書くと困ること
素直に、というのは`while`文で無限ループさせると、という意味です。
無限ループによる室内温度自動制御を行うと、処理がループ範囲から出ません。

これでは困ります。
なにに困るかというと、**処理がループから出ないせいで、ユーザの指示を受け付けることができなくなってしまいます。**

今後の展望として、そのほかの機器も赤外線信号で動作させたいので、これだと困ります。

# というわけで、やりたいこと
並列処理を実現するスレッドを用いることによって、室内温度制御を行いつつ、ユーザの命令にも対応できるようにしたいです。

今回使っているのはPythonですので、そのモジュールである`threading.Thread`を利用します。
なお、関数をスレッドに設定する方法と、スレッドクラスを継承する方法がありますが、今回は継承する方法で実装しました。

# スレッドの要点
まずはスレッドの要点を押さえます。

### スレッドを利用（継承の場合について）
スレッドを継承する際に重要になるのは、以下の二点です。

- `run()`をオーバーライドして、実行したい処理を定義する
- `start()`でスレッド処理を開始する

これで、スレッドを利用できます。
サンプルソースを以下に示します。

```inheritance_thread.py
import threading
from time import sleep


class SampleThread(threading.Thread):
    def __init__(self, num):
        super().__init__()
        self.__num = num
    
    # threading.Threadのrunメソッドをオーバーライドする
    def run(self):
        # スレッドで行わせたい処理を記述する
        # 例）
        sleep(self.__num)
        print("hello thread", self.__num)

if __name__ == "__main__":
    thread1 = SampleThread(1)
    thread2 = SampleThread(2)
    thread3 = SampleThread(3)

    thread3.start()
    print("aaa")
    thread2.start()
    print("bbb")
    thread1.start()
```

```実行結果.txt
aaa
bbb
hello thread 1
hello thread 2
hello thread 3
```

実行結果を見てみると、各スレッドオブジェクトの`start()`で処理が止まらずに、`print("aaa")`、`print("bbb")`が実行されていることがわかります。
また、実行順は`thread3`、`thread2`、`thread1`ですが、それぞれのスレッドが指定秒数待つ処理を実行することにより、`thread1`から処理結果が表示されていることがわかります。
ちゃんと並列処理していますね。

### スレッドで処理の停止と再開を実現
スレッドの停止と再開を実現するにはどうすればいいのでしょうか。

これには`threading`モジュールの、`Event`クラスを利用しましょう。サンプルソースをみる前に、準備として先に`Event`クラスについて説明します。

#### threading.Eventについて
Eventクラスはスレッド間、またはスレッドとその他処理の間で通信を行うためのクラスです。通信を実現するためのメソッドが用意されています。

具体的には、`set()`、`wait()`、`clear()`、`is_set()`があります。

動作は、Eventクラスが内部フラグを持っていることを意識すると簡単に理解できます。

- `set()`は、内部フラグをTrueにします。
- `wait()`は、内部フラグがTrueになるまで処理を中断します。
- `clear()`は内部フラグをFalseにします。
- `is_set()`は内部フラグの状態を返します。

`Event`クラスについては以上です。


#### サンプルソース
以下にスレッドの停止と再開のサンプルソースを載せます。

```threading_interrupt.py
import threading
from time import sleep

class SampleThread(threading.Thread):
    def __init__(self):
        super().__init__()
        self.__event = threading.Event() # Eventオブジェクトの宣言！
        self.__event.set()               # 最初は wait() で停止しないようにする!
    
    def run(self):
        while True:
            print("running...")
            sleep(1)
            self.__event.wait()          # wait
                
        pass
    
    def stop(self):
        self.__event.clear()
        print("stop!")

    def restart(self):
        self.__event.set()

if __name__ == "__main__":
    thread = SampleThread()

    thread.start()

    while True:
        input_line = input()
        if "a" == input_line:
            thread.stop()
        else:
            thread.restart()
```

```実行結果.txt
running...
running...
running...
running...
arunning...

stop!       -> 'a'を入力したので動作がストップ
s           -> 's'を入力したので、動作が再開
running...
running...
running...
running...
running...
arunning...

stop!       -> 'a'を入力したので動作がストップ
```
スレッド処理が標準入力によって停止＆再開されている様子がわかりますね！

`'a'`が入力したとき、`thread.stop()`が実行され、`self.__event.clear()`が呼び出されます。
これにより、`self.__event`の内部フラグがFalseになり、`wait()`で処理が中断されました。

次に、`'s'`が入力されたとき、`thread.restart()`が実行され、`self.__event.set()`が呼び出されます。
これにより、`self.__event`の内部フラグがTrueになり、`wait()`の中断処理を抜けます。

# 室内温度を自動制御するソースコード
というわけで、室内温度を自動制御するスレッドクラスを作ったものを以下に載せます。

```TempAutoController.py

from threading import Thread, Event
import subprocess
import time
import math
import os
from .TemperatureSensor import TemperatureSensor
from .remocon.Remocon import Remocon
from .remocon.RemoconSpecification import RemoconSpecification as Spec

DEFAULT_TEMP = 25

class TempAutoController(Thread):
    def __init__(self):
        super().__init__();
        dead_zone = 2;
        self.cold_thresh = DEFAULT_TEMP - dead_zone
        self.hot_thresh = DEFAULT_TEMP + dead_zone
        self.__aircon_spec = Spec(os.path.dirname(__file__) + "/remocon/aircon_specification.txt")
        self.operate_num = 0 # 設定温度の変更回数（温度下げたならマイナスになる）
        self.sensor = TemperatureSensor()
        self.remocon = Remocon()
        self.isAirconDrived = False
        self.__event = Event()
        pass

    def run(self):
        while True:
            if not self.__event.is_set():
                if self.isAirconDrived:
                    self.CloseProcess()
                print("stop")
                self.__event.wait()
            
            now_temp = int( self.sensor.GetTemperature() )
            diff = DEFAULT_TEMP - now_temp
            print("nowtemp", now_temp)
            print("self.operate_num", self.operate_num)
            

            # 温度をあげる
            if now_temp < self.cold_thresh and now_temp + self.operate_num < DEFAULT_TEMP :
                print("温度%d度あげます" % (diff) )
                self.updateSettingTemp(diff)
                pass

            # 温度を下げる
            elif now_temp > self.hot_thresh and now_temp + self.operate_num > DEFAULT_TEMP:
                print("温度%d度下げます" % ( diff ) )
                self.updateSettingTemp(diff)
                pass

            elif abs(now_temp - DEFAULT_TEMP) <= 1:
                self.CloseProcess()
                print("else")
                pass
        pass

    def ReStart(self):
        self.__event.set()

    def Stop(self):
        self.__event.clear()

    def CloseProcess(self):
        self.updateSettingTemp(-1 * self.operate_num)
        self.remocon.OutputInfraed("aircon", "aircon:off")
        self.isAirconDrived = False

    def updateSettingTemp(self, num=0):
        if not self.isAirconDrived:
            self.remocon.OutputInfraed("aircon", "aircon:on")
            self.isAirconDrived = True
            time.sleep(3)

        up = True
        if num < 0:
            up = False
        num = abs(num)

        while num > 0:
            try:
                if up:
                    if int(self.__aircon_spec["maxtemp"]) > DEFAULT_TEMP + self.operate_num:
                        self.remocon.OutputInfraed("aircon", "aircon:uptemp")
                        self.operate_num += 1
                else:
                    if int(self.__aircon_spec["mintemp"]) < DEFAULT_TEMP + self.operate_num:
                        self.remocon.OutputInfraed("aircon", "aircon:downtemp")
                        self.operate_num -= 1
            except:
                raise subprocess.CalledProcessError
            time.sleep(3)
            num -= 1
```
無駄に長いですが、スレッドに関係しているのは`run()`と、`Stop()`と`ReStart()`です。
肝は`if not self.__event.is_set():`の部分ですね。`__event`が`False`の時だけ文中に入り、待機状態に移行するための処理を必要に応じて行い、`wait()`が処理を中断してくれます。

中断された状態から`ReStart()`が実行されると、`self.__event.set()`が動作し、また自動制御処理が開始します。

# まとめ

- スレッドを利用することで並列処理が実現できる。
- スレッドを継承する時のポイント
    - 親クラスで定義された`run()`をオーバーライドし、そこに並列処理する内容を記述する
    - 親クラスで定義された`start()`を使って`run()`を間接的に利用する
- Eventクラスのメソッドを利用するときは、メンバに内部フラグを所持していることを意識する

以上です！
室内温度もいい感じに保てるようになりました（急激にやるのでうるさい笑）。
よかったです。

# 参考文献
[公式のドキュメント](https://docs.python.org/ja/3/library/threading.html)
[おまいらのthreading.Eventの使い方は間違っている](https://qiita.com/tag1216/items/2dcb112f8018eb19a999)