IoT何をいまさら(79) Python, threadingとasyncioなLチカ

Joseph Halfmoon

前回、古いラズパイ1B+の拡張端子を多少使いやすくする工作をしただけで、がぜんやる気がでました。今回は Python3 使って、threadingなLチカと、asyncioなLチカをやってみました。threadingもasyncioも、GPIOの操作などに使うにはもってこい?かも。勿論、あまりクリティカルなタイミング制御には向きませぬが。

PC上でPythonを使う場合、たまたまですが、用途的にIO依存な処理などまず書かないので、Pythonの threading モジュールの存在は知っていてもあまり使う気になれないでいました。CPython特有の実装だそうですが、有名なGIL(global interpreter lock)の制約があるので複数スレッドでマルチコアCPUを活用しようとしても別に速くならないからです。GILについてご存知なければ以下のドキュメントをご参照ください。

Python 用語集

グローバルインタプリタロック (Global Interpreter Lock) を取り除くことはできないのですか?

Pythonで処理を速くするためにマルチコア使うときはマルチプロセス、という思い込みです。しかし、ラズパイ上でのIO操作を書くときは、宗旨替えをした方が良いようです。threading お役立ちです。たとえ ラズパイ無印(1とは呼ばれていない)やZeroのようなシングルコアの装置であったにせよです。独立性の高い複数の定期処理などを非常に見通しよく実装することが可能じゃないかと思います。まさにIO処理向け。

そして threadingでマルチスレッド化もよいのですが、asyncioを使い、シングルスレッドで処理のノンブロッキング化でも同様なIO待ち時間の有効活用が可能であると認識しました。asyncioは、主にネットワーク上のリソースからデータを取ってくるときに結果が返るのを待って次の処理をブロックすることなく、多数のリクエストを発行する、といったシーンで使われていることが多いようです。コルーチンとかフューチャとか一応なんとなく概念は知っていても(私には)御馴染みでないものどもが沢山。私自身は、このPythonのasyncio、まだよく理解しておりません。手探りで勉強中であります。しかし、ちょっと触っただけでも、その名のとおりIO処理に向いていると感じました。また、実装にもよりますが、MicroPythonにもサブセット的な非同期ioモジュールが存在するようです。きっと組み込み用途にもよろしいんじゃないかと思います。

今回、末尾に掲げたサンプルコードを走らせたマシンは、

Raspberry Pi model B+ (初代ラズパイB+)、OSのコードネーム Jessie

であります。ラズパイ自身も古ければ、Raspbian OSも古いです。実行に使ったPythonのバージョンは 3.6.0。これも古いバージョンですが、別件で、結構苦労してソースからビルドしてJessieのデフォルトだったもっと古い3.4.2を置き換えたものです。

実装したのは以下のようなIO処理のサンプルです。

  • GPIO18に緑のLED接続。True出力で点灯。
  • GPIO27に青のLED接続。True出力で点灯。
  • GPIO22に赤のLED接続。True出力で点灯。
  • GPIO23にタクトスイッチ接続。押すと入力は0となる。
  • 各LEDを異なる周期で点滅させる
  • タクトスイッチを押すと点滅を完了、全消灯し、プログラム終了

各LEDの点滅が、異なる周期処理の見立てであります。ぱっと見、LED3色のLチカに過ぎません。

まずはThreadingバージョン。スレッドの仕様については特に難しいこともないので、末尾のソースを御覧ください。こんなコードで3色LEDで個別の周期でLチカいたします。

これに対して Asyncio の方は不慣れなので結構、試行錯誤しました。いろいろ解説記事もあり参考になったのですが、あまり組み込み向け前提の記事がなかったです。その中で、MicroPythonの非同期I/Oのサンプルが単刀直入、一番分かり易かったです。

uasyncio — 非同期I/Oスケジューラ

ただ、Micropythonのため、実装が異なり、ラズパイ上の普通のPython3では、多分そのままでは走らんと思います(確認してないですが。)

また、Asyncioの仕組みなどについては以下の「くろのて」様の記事が分かり易かったです。ありがとうございます。

なんとなく理解するasyncio

そんなこんなで「とりあえず実機でLチカできた」ソースを末尾に掲げました。

まあ、今回のような長い(1秒とか2秒とか)周期で、軽い仕事をやっているときはどちらもほとんど同じ、期待通りの動作をいたします。けれど、仕事の数が増えたり、周期が短くなったりすると、それぞれ一長一短あらわれてくるような気がいたします。どだいRPi.GPIOの人が指摘されていたとおり、Linuxの上のGCのあるPythonの実装なので実時間性を求めちゃいけない、ということですが。

IoT何をいまさら(78) PIRセンサでPiCameraのシャッタを押す へ戻る

IoT何をいまさら(80) M5Stack、BB風拡張PCA9306搭載 へ進む

Threading な3色Lチカ
import RPi.GPIO as GPIO
import time
import threading

greenLed = 18
blueLed = 27
redLed = 22
swPin = 23

runFlag = True

GPIO.setmode(GPIO.BCM)
GPIO.setup(greenLed, GPIO.OUT)
GPIO.setup(blueLed, GPIO.OUT)
GPIO.setup(redLed, GPIO.OUT)
GPIO.setup(swPin, GPIO.IN)

def blink(pinN, waitS):
    while runFlag:
        GPIO.output(pinN, True)
        time.sleep(waitS)
        GPIO.output(pinN, False)
        time.sleep(waitS)

t1 = threading.Thread(target = blink, args=(greenLed, 1.0) )
t2 = threading.Thread(target = blink, args=(blueLed, 2.0) )
t3 = threading.Thread(target = blink, args=(redLed, 3.0) )

t1.start()
t2.start()
t3.start()

while runFlag:
    if GPIO.input(swPin) == 0:
        runFlag = False
    time.sleep(0.05)

t1.join()
t2.join()
t3.join()
GPIO.cleanup()
Asyncio な3色Lチカ
import RPi.GPIO as GPIO
import time
import asyncio

greenLed = 18
blueLed = 27
redLed = 22
swPin = 23

runFlag = True

GPIO.setmode(GPIO.BCM)
GPIO.setup(greenLed, GPIO.OUT)
GPIO.setup(blueLed, GPIO.OUT)
GPIO.setup(redLed, GPIO.OUT)
GPIO.setup(swPin, GPIO.IN)

async def blink(pinN, waitS):
    while runFlag:
        GPIO.output(pinN, True)
        await asyncio.sleep(waitS)
        GPIO.output(pinN, False)
        await asyncio.sleep(waitS)

async def keyWait():
    global runFlag
    while runFlag:
        if GPIO.input(swPin) == 0:
            runFlag = False
        await asyncio.sleep(0.05)

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(
    keyWait(),
    blink(greenLed, 1.0),
    blink(blueLed, 2.0),
    blink(redLed, 3.0),))
loop.close()

GPIO.cleanup()