trioによる並行処理⑤（CancelScope）

[目次](https://qiita.com/Kyosuke_Ichikawa/items/b7fdffcbfe00773074a6)

今回は[trio](https://trio.readthedocs.io/en/stable/index.html)の[CancelScope](https://trio.readthedocs.io/en/stable/reference-core.html?highlight=shield#trio.CancelScope)について扱います。

# CancelScope

下記のようにしてブロックを作ることができます。
```python
with CancelScope() as cancel_scope:
   ...
```


| メンバ | 内容 |
| --- | --- |
| deadline | タイムアウトの時間（動的に変更可能、コンストラクタでも設定可能） |
| cancel() | 呼ぶとブロック内の処理をキャンセルしてブロックを抜ける |
| cancelled_caught | このブロックがタイムアウトかキャンセルしたらTrue（ただし、実際にコードがキャンセルされていなければTrueにならない） |
| cancel_called | このブロックがタイムアウトかキャンセルしたらTrue（実際にコードがキャンセルされていなくてもTrueになる） |
| shield | Trueにするとこのブロック内ではブロック外のタイムアウトやキャンセルによって終了しない（動的に変更可能、コンストラクタでも設定可能） |


## deadline, cancelled_caughtを使ってみる
```python:サンプル
import trio

async def main():
    with trio.CancelScope() as cancel_scope:
        now = trio.current_time()
        print(f"now={now:.0f}, deadline={cancel_scope.deadline:.0f}")
        cancel_scope.deadline = now + 2
        print(f"now={now:.0f}, deadline={cancel_scope.deadline:.0f}")
        while True:
            await trio.sleep(1)
    print(cancel_scope.cancelled_caught)

trio.run(main)
```
```:実行結果例
now=235232, deadline=inf
now=235232, deadline=235234
True
```
このように、```deadline```のデフォルトはinfとなります（コンストラクタで指定可能）。
上の例の通り動的に変えることができます。

## move_on_after(), move_on_at()との関係

```CancelScope```は[trioによる並行処理③（move_on_after, move_on_at）](https://qiita.com/Kyosuke_Ichikawa/items/ab35c098e819316bdcf9)の回でもしれっと出てきてました。
[move_on_after()](https://trio.readthedocs.io/en/stable/reference-core.html#trio.move_on_after)や[move_on_at()](https://trio.readthedocs.io/en/stable/reference-core.html#trio.move_on_at)のwithブロックを作った時に帰ってくるオブジェクトが```CancelScope```です。
```python:サンプル
import trio

async def main():
    with trio.move_on_after(1) as cancel_scope:
        print(f"now={trio.current_time():.0f}, deadline={cancel_scope.deadline:.0f}")
        while True:
            await trio.sleep(1)
    print(cancel_scope.cancelled_caught)

trio.run(main)
```
```python:実行結果例
now=174955, deadline=174956
True
```
```move_on_after()```でタイムアウトを1秒後にしているため、deadlineは1秒後を指します。

## cancel()を使ってみる
下記はタイムアウトではなく```cancel()```により終了するサンプルです。
```python:サンプル
import trio

async def main():
    with trio.CancelScope() as cancel_scope:
        now = trio.current_time()
        print(f"now={now:.0f}, deadline={cancel_scope.deadline:.0f}")
        while True:
            await trio.sleep(3)
            cancel_scope.cancel()
    print(cancel_scope.cancelled_caught)
    print(f"elapsed_time={trio.current_time() - now:.1f}")

trio.run(main)
```
```:実行結果例
now=99488, deadline=inf
True
elapsed_time=3.0
```
```deadline```がinfであるにも関わらず終了できています。```cancelled_caught```もTrueになりました。

## cancelled_caughtとcancel_calledの違い
```cancelled_caught```と```cancel_called```はどちらも「このブロックがタイムアウトかキャンセルしたらTrueになるフラグ」です。
```python:サンプル
import trio

async def main():
    with trio.CancelScope() as cancel_scope:
        cancel_scope.cancel()
        await trio.sleep(1)
    print(cancel_scope.cancelled_caught)
    print(cancel_scope.cancel_called)

trio.run(main)
```
```:実行結果
True
True
```
このように多くの場合はどちらも同じ結果になります。
しかし、実際にコードがキャンセルされなければ```cancelled_caught```はTrueになりません。
例えば```cancel_scope.cancel()```と```await trio.sleep(1)```の順番を入れ替えると
```python:サンプル
import trio

async def main():
    with trio.CancelScope() as cancel_scope:
        await trio.sleep(1)
        cancel_scope.cancel()
    print(cancel_scope.cancelled_caught)
    print(cancel_scope.cancel_called)

trio.run(main)
```
```:実行結果
False
True
```
ここで、```cancel()```はブロックの最後で呼ばれていて意味がないです（呼ばなくてもブロックを抜ける）。
コードをキャンセルしてるわけではないので、```cancelled_caught```はFalseとなりました。

## shieldを使ってみる

shieldをTrueにすると、このブロック内ではブロック外のタイムアウト・キャンセルによって終了しなくなります。
まず、下記コードはshieldを使わない例です。

```python:shieldを使わない例
import trio

async def main():
    start_time = trio.current_time()
    with trio.CancelScope() as cancel_scope1:
        with trio.CancelScope() as cancel_scope2:
            cancel_scope1.cancel()
            await trio.sleep(3)
    print(f"elapsed_time={trio.current_time() - start_time:.1f}")
    print(f"cancel_scope1.cancelled_caught={cancel_scope1.cancelled_caught}")
    print(f"cancel_scope2.cancelled_caught={cancel_scope2.cancelled_caught}")

trio.run(main)
```
```:実行結果
elapsed_time=0.0
cancel_scope1.cancelled_caught=True
cancel_scope2.cancelled_caught=False
```
```cancel_scope1.cancel()```でただちにブロックを抜けています。
そのため、```elapsed_time```(開始からブロックを抜けるまでの時間)は0秒です。
2つ目のブロックでshieldをTrueにすると下記のようになります。
```python:shieldをTrueにした例
import trio

async def main():
    start_time = trio.current_time()
    with trio.CancelScope() as cancel_scope1:
        with trio.CancelScope(shield=True) as cancel_scope2:
            cancel_scope1.cancel()
            await trio.sleep(3)
    print(f"elapsed_time={trio.current_time() - start_time:.1f}")
    print(f"cancel_scope1.cancelled_caught={cancel_scope1.cancelled_caught}")
    print(f"cancel_scope2.cancelled_caught={cancel_scope2.cancelled_caught}")

trio.run(main)
```
```:実行結果
elapsed_time=3.0
cancel_scope1.cancelled_caught=False
cancel_scope2.cancelled_caught=False
```
```elapsed_time```(開始からブロックを抜けるまでの時間)が3秒になりました。
2つ目のブロックで```cancel_scope1.cancel()```の影響を受けていない事が分かります。
ちなみに2つ目のブロック終了後に```trio.sleep(5)```を置くと、そちらはただちに終了します。
```python:サンプル
import trio

async def main():
    start_time = trio.current_time()
    with trio.CancelScope() as cancel_scope1:
        with trio.CancelScope(shield=True) as cancel_scope2:
            cancel_scope1.cancel()
            await trio.sleep(3)
        await trio.sleep(5)
    print(f"elapsed_time={trio.current_time() - start_time:.1f}")
    print(f"cancel_scope1.cancelled_caught={cancel_scope1.cancelled_caught}")
    print(f"cancel_scope2.cancelled_caught={cancel_scope2.cancelled_caught}")

trio.run(main)
```
```:実行結果
elapsed_time=3.0
cancel_scope1.cancelled_caught=True
cancel_scope2.cancelled_caught=False
```
```elapsed_time```(開始からブロックを抜けるまでの時間)が3秒のままです。
よって、あくまでもshieldをTrueにした2つ目のブロックだけがキャンセルされていない事が分かります。