項目案内
技術内容だけ知りたい方は「スレッドの要点」へ飛んでください。
こんなものを作っています
「帰宅時快適! 室温快適化システム!」というシステムをゼミで作っています。
季節によって家が暑くなったり、寒くなったりして家に帰ってきた時不愉快という問題点を解決するためのシステムです。
Slackから特定のメッセージを送ると、赤外線発振回路が赤外線を送信し、エアコンを操作できる仕組みになっています。
言ってしまえばスマートリモコンですね。
ただ、これだけだと他のものと同じになってしまうので、室内温度を素早く最適温度にすることを売りにするつもりでいます。
というわけで、室内の温度を常に監視し、その監視結果に合わせて設定温度の変更を行うようにしようとしていたのですが、ここで、ある問題に気づきました。
室内温度自動制御を素直に書くと困ること
素直に、というのはwhile文で無限ループさせると、という意味です。
無限ループによる室内温度自動制御を行うと、処理がループ範囲から出ません。
これでは困ります。
なにに困るかというと、処理がループから出ないせいで、ユーザの指示を受け付けることができなくなってしまいます。
今後の展望として、そのほかの機器も赤外線信号で動作させたいので、これだと困ります。
というわけで、やりたいこと
並列処理を実現するスレッドを用いることによって、室内温度制御を行いつつ、ユーザの命令にも対応できるようにしたいです。
今回使っているのはPythonですので、そのモジュールであるthreading.Threadを利用します。
なお、関数をスレッドに設定する方法と、スレッドクラスを継承する方法がありますが、今回は継承する方法で実装しました。
スレッドの要点
まずはスレッドの要点を押さえます。
スレッドを利用(継承の場合について)
スレッドを継承する際に重要になるのは、以下の二点です。
-
run()をオーバーライドして、実行したい処理を定義する -
start()でスレッド処理を開始する
これで、スレッドを利用できます。
サンプルソースを以下に示します。
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()
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クラスについては以上です。
サンプルソース
以下にスレッドの停止と再開のサンプルソースを載せます。
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()
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()の中断処理を抜けます。
室内温度を自動制御するソースコード
というわけで、室内温度を自動制御するスレッドクラスを作ったものを以下に載せます。
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クラスのメソッドを利用するときは、メンバに内部フラグを所持していることを意識する
以上です!
室内温度もいい感じに保てるようになりました(急激にやるのでうるさい笑)。
よかったです。
