trioに"guest mode"なるものが実装されていたので使ってみる

[Trio](https://trio.readthedocs.io/en/stable/index.html)にいつの間にか[guest mode](https://trio.readthedocs.io/en/stable/reference-lowlevel.html#using-guest-mode-to-run-trio-on-top-of-other-event-loops)なるものが実装されていたので試してみました。

# Guest Modeとは？

通常Trioは自身でevent loopを持つため別のevent loopと同じthread内では共存できません。例えば`asyncio`,`tkinter`,`Qt`等とは共存できないはずです。ところがこの`guest mode`という状態だと`trio`がevent loop持たずに動くためそれができてしまうらしいです。言い換えるなら**どんなGUI libraryを使っていようとも簡単に`async/await`による入出力待ちやsleepができるようになった**という事だと思います、すばらしい。

# 実際に使ってみる

私が唯一使えるGUI libraryはKivyなのですがKivyは既に`asyncio`と`trio`に対応していてわざわざ`guest mode`を使う利がないので`tkinter`でいきます。

## 必要なもの

### thread-safeな形で関数を予約できるようにする

この`guest mode`を使うにはまずthread-safeな形で関数の実行を予約できる必要があるようです。`tkinter`における関数の予約は`widget.after()`ですがこれは[thread-safeでは無さそう](https://stackoverflow.com/questions/58118723/is-tkinters-after-method-thread-safe)なので自前で用意しました。

```python3
import queue

class ThreadSafeScheduler:
    def __init__(self, widget, *, polling_interval=100):
        self.queue = queue.Queue()
        self.after = widget.after
        self.polling_interval = polling_interval
        self.after(polling_interval, self._polling)

    def __call__(self, callback):
        self.queue.put(callback)

    def _polling(self):
        queue = self.queue
        after = self.after
        while not queue.empty():
            queue.get_nowait()()
        after(self.polling_interval, self._polling)
```

これのinstance化をmain-threadでする事さえ守れば後は自由に別threadから予約できます。

```python3
# 必ずmain-threadで作る
scheduler = ThreadSafeScheduler(...)
# その後は好きなthreadから
scheduler(実行を予約したい関数)
```

### trioの合図が来るまでprocessを閉じない

そして`trio`からの合図があるまでprocessを閉じない方が良いらしい？ので、次のようにして待ってあげます。

```python3
from time import sleep
import trio

cannot_quit = True
def done_callback(result):
    '''この関数はtrioから呼ばれる'''
    global cannot_quit
    cannot_quit = False

rootのwidget.mainloop()
while cannot_quit:
    sleep(1)
```

## programのおおまかな構造

結果programは以下のような構造になりました。

```python3
import tkinter as tk
import queue


class ThreadSafeScheduler:
    '''(略)'''


def run_with_trio(root, trio_main):
    from functools import partial
    from time import sleep
    import trio

    cannot_quit = True
    def done_callback(result):
        '''(略)'''

    trio.lowlevel.start_guest_run(
        trio_main, root,
        run_sync_soon_threadsafe=ThreadSafeScheduler(root),
        run_sync_soon_not_threadsafe=partial(root.after, 1),
        done_callback=done_callback,
    )
    root.mainloop()
    while cannot_quit:
        sleep(1)


root = tk.Tk()

async def trio_main(root):
    '''ここにやりたい事を書いていく'''

run_with_trio(root, trio_main)
```

`trio.lowlevel.start_guest_run()`が`trio`を`guest mode`で開始する関数です。`run_sync_soon_not_threadsafe`ですがこれは任意の引数で、非thread-safeな形で関数を予約できるのならそれを教えてあげることで速度の向上が見込めます。

## tkinterのeventを`await`できるようにする

tkinterのeventの発生を待てたら便利なので以下のような物も用意しておきました。

```python
async def tk_event(widget, name):
    trio_event = trio.Event()
    def callback(e):
        nonlocal r; r = e
        widget.unbind(name, bind_id)
        trio_event.set()
    bind_id = widget.bind(name, callback, '+')
    r = None
    await trio_event.wait()
    return r
```

これで以下のような事ができるようになりました。

```python
e = await tk_event(widget, '<Button>')
print(f"座標(x={e.x}, y={e.y}が押されました")
```

## BMI計算機でも作ってみる

実際に簡単なprogramでも作って動作を確かめてみます。

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144673/fd05d9e0-d968-dc50-1a7d-77b0f1d2fc1d.png)
![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144673/25b09f1b-e120-74ba-add7-f8b265fdd569.png)
![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144673/d07d31bd-4027-0263-e220-1ffe84814477.png)
![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144673/687e140e-b886-6235-bb92-ddefc09f7b19.png)


```python
async def trio_main(root):
    from tkinter.font import Font

    font = Font(size=50)
    frame = tk.Frame(root)
    frame.pack()

    # title画面
    label = tk.Label(frame, text='BMIを算出する', font=font)
    label.pack(padx=10, pady=10)
    await tk_event(label, '<Button>')

    # 体重を訊く
    label['text'] = '体重は何kgですか？'
    label.pack(padx=10, pady=10)
    child_frame = tk.Frame()
    child_frame.pack()
    entry = tk.Entry(child_frame, bg='white', font=font)
    entry.pack(side='left', padx=10, pady=10)
    label2 = tk.Label(child_frame, text='kg', font=font)
    label2.pack(padx=10, pady=10)
    ok_button = tk.Button(frame, text='決定', font=font)
    ok_button.pack()
    weight = None
    while weight is None:
        await tk_event(ok_button, '<Button>')
        text = entry.get()
        if not text:
            continue
        try:
            weight = float(text)
        except ValueError:
            pass
    print(f'体重: {weight}kg')

    # 身長を訊く
    label['text'] = '身長は何mですか？'
    entry.delete(0, tk.END)
    label2['text'] = 'm'
    height = None
    while height is None:
        await tk_event(ok_button, '<Button>')
        text = entry.get()
        if not text:
            continue
        try:
            height = float(text)
        except ValueError:
            pass
    print(f'身長: {height}m')

    # 結果を見せる    
    bmi = weight / (height * height)
    print(f'BMI指数: {bmi}')
    if bmi < 18.5:
        bmi_class = '痩せ過ぎ'
    elif bmi < 25.0:
        bmi_class = '健康'
    else:
        bmi_class = '肥り過ぎ'
    label['text'] = f'あなたは{bmi_class}です'
    child_frame.destroy()
    ok_button.destroy()
```

[全code](https://gist.github.com/gottadiveintopython/845330e10c33f0ae4f9b56c39f02dd69)

<!--
`Trio`は標準moduleである`asyncio`と比べると他のlibraryによる支援が少ないため選ぶべきかは難しいですが、なかなか面白いAPIを備えています。その一つが`Nursery`で、作者はこれを非同期処理におけるダイクストラの構造化programmingのような物だと[言っています](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/)。詳しくはlink先に譲りますがとにかく`Trio`は`asyncio`と比べると利用者ができる事を強く制限する事で安全性とAPIの簡潔さを確保しているようです。実際私は`asyncio`が難しくて困っていた所を`Trio`のおかげで`async/await`の世界に足に踏み入れられるようになりました。なので同じように
-->