※ GPIO は使ったことがないので、一般的な「非同期」と「同期」の間の連携方法として回答します。
まず前提として discord.py は非同期IOで扱われている為、
asyncioのイベントループ外から直接メッセージを送る事は出来ません。
解決案として提示できるのは以下の2通り。
- A. 「別スレッド」から「非同期ループで処理される関数」にメッセージを送る方法
非同期<=>同期 をサポートした Queue の実装。 "janus" ライブラリを使う
- B. GPIO の入出力を非同期IOで扱うライブラリ "apigpio"
末尾のコードで主にAの方法について解説します
A. 「別スレッド」から「非同期ループで処理される関数」にメッセージを送る方法
概要:
- 非同期<=>同期間連携の為のQueue を準備
- janus のインストール (pip install janus)
- 同期処理
- GPIO: callback で通知を受け取る (別スレッドで実行される)
- Queue へメッセージ送信(put)
- 非同期処理
- Queue からの値を読み出す(get)処理のループ
- 通常の discord の処理のループ
- asyncio.gather で複数の非同期ループを纏める
- asyncio.run 非同期処理のイベントループを開始
B. GPIO を非同期IOで扱うライブラリ "apigpio"
"pigpio" というライブラリがありそれを非同期に扱う為のモノの様です。
apigpio のサンプルコードを読む上での注意点:
サンプルコードの対応しているPythonのバージョンが古いスタイルの記述になっています。
実行には支障ないと思いますが以下のように読み替えます。
@asyncio.coroutine
-> async
yield from
-> await
実際に試してないので軽く紹介のみ。
yield
が何かわからない場合、
前提知識として「ジェネレータ」辺り迄は理解しておいた方が良いと思います。
全体像はこのような感じになります(概要のみ)
- apigpio GPIOを扱う非同期IO
- discord を扱う非同期IO (client.runの代わりにclient.startを利用)
- NOTE: 非同期同士の連携には asyncio.Queue が使えます。
- asyncio.gatherで2つの非同期処理をまとめる (後述のコードを参考にして下さい)
- asyncio.runで開始
async/await について
大雑把な説明です。実際のコーディングの際の注意点に焦点を当てました。
-
async は、awaitを用いる関数 の定義等で使います。async付きで定義された関数を呼び出すと「コルーチン」を返します。
asyncを付けると関数自体が自動的に非同期になったりは しません (よくある誤解1)
関数内のコードの実行自体は通常と同じように処理されます。
その為、非同期処理の中ではコードの実行を止めるような操作はNG
(エラーにはなりませんが、他の非同期処理が出来なくなり期待する動作になりません)
非同期(async)の関数(コルーチン)内で出来ない事:
- time.sleep等のコードの実行を止める操作。同一スレッド内ではNG
- await を伴わない無限ループ。await がないとその間他の非同期処理が実行されません。
- ブロッキング I/O 操作 (ファイル書込やsocketからの読書等)
-
asyncio.run (client.run内部)
非同期処理のイベントループの正体ですが、単一スレッド上で動作するループです。
各非同期処理(コルーチン)自体は、通常の関数と同様に実行されます。
順番に実行される
- 非同期処理A await で イベントループに戻る
- 非同期処理B await で イベントループに戻る
- 非同期処理A 以前の await から再開、次のawait でまた イベントループに戻る
繰り返し説明になりますが、今迄「非同期処理」でやってはいけない事として説明した理由がここで、
各非同期処理を実行する為に、イベントループは動き続ける必要があります。
つまり、非同期処理内では、コードの実行を中断するようなブロッキング操作をしてはいけない。
これは、GUIプログラミングに於いてのイベントループと同じで、
イベント処理内で時間のかかることをすると、GUIがフリーズするといったように、
非同期処理内でも同様、時間のかかる処理をすると、その間他の非同期処理が実行されないという事が起こります。
python
1"""
2このプログラムは、非同期で処理されるdiscord と
3別スレッドで動作するGPIOのコールバックとの間でのメッセージ受け渡しを再現するデモです。
4
5非同期IOとスレッドの連携のデモとして書いたコードなので、
6実際のdiscord/GPIOの利用には手直しが必要になる点をご了承ください。
7
8discord, GPI は用いず、動作を模倣した仮のクラスを作って行います。
9このコードの実行には discord.py と RPi.GPIO のインストールは不要です。
10
11
12事前にインストールが必要な外部ライブラリ: "janus"
13
14非同期/同期での読書が可能なQueueの実装です。
15
16- janus(https://pypi.org/project/janus/) ライブラリ
17 pip install janus でインストール
18"""
19
20import time
21import asyncio
22from threading import Thread
23
24from janus import Queue
25
26# ---------- 最初の MockGPIO, MockDiscord は意図的に読み飛ばしてください --------
27
28class MockGPIO(Thread):
29 """
30 NOTE: GPIO.start() 時に run() が呼び出される。
31 """
32
33 def __init__(self, *args, **kw):
34 super().__init__(*args, **kw)
35 self._callback = None
36
37 def run(self):
38 # 別スレッドで稼働。ここではブロッキング操作(time.sleep)は可能
39 for num in range(10): # <-- 仮の入力データ [0..9]
40 if self._callback:
41 self._callback(num)
42 time.sleep(1)
43
44 def add_event_detect(self, callback=None):
45 if callback:
46 self.add_event_callback(callback)
47 # ここは実際にも、add_event_detectを呼び出す事で、新スレッドが立ち上がります。
48 self.start() # Thread.start -> 別スレッドでrun()を実行
49
50 def add_event_callback(self, callback):
51 self._callback = callback
52
53
54class MockDiscordClient:
55
56 def __init__(self):
57 self._callback = None
58
59 async def run_event(self, *args, **kw):
60 await self._callback(*args, **kw)
61
62 async def start(self, *args):
63 # 非同期で動作。
64 # 通常のtime.sleep 等、プログラムの実行を阻害する(ブロッキング)操作は、
65 # asyncioのループを止めてしまうのでNG。非同期での代替の手段を取ります。
66 for num in range(10): # <-- 仮の入力データ [0..9]
67 await asyncio.sleep(1)
68 await self.run_event(num)
69
70 def event(self, coro):
71 if asyncio.iscoroutinefunction(coro):
72 self._callback = coro
73 return coro
74
75 async def send(self, msg):
76 print("send to discord >>>", msg)
77 await asyncio.sleep(1)
78
79
80GPIO = MockGPIO(daemon=True)
81client = MockDiscordClient()
82
83## 上は GPIO モジュールと discord をテスト用にエミュレートする為のクラスです
84## ユーザ視点からは中の実装をそれ程詳細に把握する必要はありません。
85
86## 実際の実装に合わせて模倣している要点は2つ
87## * discoard は非同期IO(asyncio)で扱われる
88## * GPIO のコールバックは別スレッドで動いている
89
90# ------------------------ 解説ここから ------------------------
91
92async def main():
93
94 ##
95 # (1) キューを用いてメッセージのやり取りの(準備)
96 #
97 # janus.Queue (sync <=> async queue)
98 queue = Queue()
99 sync_q = queue.sync_q # GPIO側で使う同期キュー
100 async_q = queue.async_q # awaitで使う非同期キュー
101
102 ##
103 # (2) GPIO からの入力に応じてコールバックを呼び出し
104 #
105 # ※ GPIO については簡略化してるので、実際の呼び出し方と異なります。
106 # 実際の呼び出し方は add_event_detect, add_event_callback 辺りを調べて見て下さい。
107 #
108 # ※ 更に、実際の利用は、discord のon_ready等で discord の準備が出来てから登録する。
109 def callback(num):
110 # GPIO -> callback -> 同期キューにput
111 print("GPIO >>>", num)
112 sync_q.put(num)
113
114 GPIO.add_event_detect(callback=callback)
115
116 ###
117 # (3) ディスコードのメッセージ受信
118 #
119 # ※ 実際のdiscordライブラリにこのような名前のイベントはありません。
120 # discordのclient と 他の非同期ループを扱う為のデモとして仮実装。
121 @client.event
122 async def on_discord_message(msg):
123 await asyncio.sleep(1)
124 print("recv from discord <<<", msg)
125
126 ##
127 # (4) キュー内のメッセージを非同期で読み出し
128 #
129 # GPIOのコールバックで sync.put で送られたものを async_q.get で読み出します
130 #
131 async def listen_queue_loop():
132 while True:
133 msg = await async_q.get()
134 # ディスコードへ送信 (※ 実際はチャンネルに送信)
135 await client.send(str(msg))
136
137 ##
138 # (5) 非同期ループの実行 (client.runの置き換え)
139 # - asyncio.gather 2つの非同期ループの実行に用います
140 # - client.start
141 # - listen_queue_loop
142 #
143 # 非同期処理: ディスコードclient と、キューの読み出しの開始
144
145 BOT_TOKEN = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
146 await asyncio.gather(client.start(BOT_TOKEN), listen_queue_loop())
147
148
149if __name__ == '__main__':
150 try:
151 asyncio.run(main())
152 except KeyboardInterrupt:
153 pass
154
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/05/12 11:47