単純なカウンターのインクリメントの並行処理がアトミックでないことの確認

# はじめに
[Effective Python 第2版 ―Pythonプログラムを改良する90項目](https://www.amazon.co.jp/dp/4873119170) の項目54「スレッドにおけるデータ競合を防ぐためにLockを使う」に掲載されているプログラムを試したところ、自分の Linux マシンでは期待される結果（スレッドセーフではない）が得られなかったので、Effective Python で学んだことを組み合わせて確認してみました。

# 環境
|| バージョン |
|---|---|
| Linux | Ubuntu 20.04.3 LTS |
| Python | 3.10.0 |

| リソース名 | 値 |
|---|---|
| メモリ | 3GB |
| CPU | Intel(R) Core(TM) i7-4510U |
| core数 | 2 |

※ ホストOSが Windows の環境で Oracle Virtual Box をインストールし、上記リソースを割り当てて Ubuntu を動かしています。

# 実験
227～228ページに記載されている以下のコード[^1]を実行してみます。

[^1]: プログラムコードは、書籍に掲載されているものから若干修正しています。


```python:sample_01.py
from threading import Thread

N_SENSOR = 5

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self, offset):
        self.count += offset

def worker(_sensor_index, how_many, counter):
    for _ in range(how_many):
        # ...
        counter.increment(1)

def main():
    how_many = 10**5
    counter = Counter()
    threads = []
    for i in range(N_SENSOR):
        thread = Thread(target=worker,
                        args=(i, how_many, counter))
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()

    expected = how_many * N_SENSOR
    found = counter.count
    print(f'Counter should be {expected}, got {found}')

main()
```

```bash:実行結果
$ python sample_01.py
Counter should be 500000, got 500000
```

結果だけ見ると、インクリメント処理がアトミックに処理されているように見えます。

本項目の説明によれば、`self.count += offset` は内部的には以下の 3 つの処理が実行されています。

1. getattr() でクラス属性 `count` の値を取得
1. そこに offset を加える
1. 結果を setattr() でクラス属性 `count` の値を更新

# インクリメント処理部分の確認
項目51「合成可能なクラス拡張のためにはメタクラスではなくクラスデコーレータを使う」において、特殊メソッドの呼び出しを出力させることを学びました。同じことをやってみましょう。

まず sample_01.py にクラスデコレータを追加します[^2]。

[^2]: オリジナルのコードに `types.WrapperDescriptorType` を追加しています。そして無限再帰呼び出しされないように `__repr__` だけは除外しています。

```python:sample_02.py(追加分)
import types
from functools import wraps

trace_types = (
    types.MethodType,
    types.FunctionType,
    types.BuiltinFunctionType,
    types.BuiltinMethodType,
    types.MethodDescriptorType,
    types.ClassMethodDescriptorType,
    types.WrapperDescriptorType,
)

def trace_func(func):
    if hasattr(func, 'tracing'):
        return func

    @wraps(func)
    def wrapper(*args, **kwargs):
        result = None
        try:
            result = func(*args, **kwargs)
            return result
        except Exception as e:
            result = e
            raise
        finally:
            print(f'{func.__name__}({args!r}, {kwargs!r}) -> '
                  f'{result!r}')

    wrapper.tracing = True
    return wrapper

def trace(klass):
    for key in dir(klass):
        value = getattr(klass, key)
        if isinstance(value, trace_types) and key != '__repr__':
            wrapped = trace_func(value)
            setattr(klass, key, wrapped)
    return klass
```

そして `Counter` クラスをデコレートします。

```python
@trace
class Counter:
    ...
```

このまま動かすと時間がかかるので、`how_many` の値を `10**2` に変更して試してみましょう。

```bash:実行結果
$ python sample_02.py
...(snip)...
__getattribute__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count'), {}) -> 351
__setattr__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count', 352), {}) -> None
increment((<__main__.Counter object at 0x7fe020d4fcd0>, 1), {}) -> None
__getattribute__((<__main__.Counter object at 0x7fe020d4fcd0>, 'increment'), {}) -> <bound method Counter.increment of <__main__.Counter object at 0x7fe020d4fcd0>>
__getattribute__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count'), {}) -> 352
__setattr__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count', 353), {}) -> None
increment((<__main__.Counter object at 0x7fe020d4fcd0>, 1), {}) -> None
__getattribute__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count'), {}) -> 353
Counter should be 500, got 353
```

書籍に記載されている例の通り、カウンターの値が期待値とは異なる値になりました。出力されたメッセージを遡ってみてみると、解説された通りのことが起きていることを確認できます（カウンターが 213 から 209 に巻き戻っています）。

```
__setattr__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count', 212), {}) -> None
increment((<__main__.Counter object at 0x7fe020d4fcd0>, 1), {}) -> None
__getattribute__((<__main__.Counter object at 0x7fe020d4fcd0>, 'increment'), {}) -> <bound method Counter.increment of <__main__.Counter object at 0x7fe020d4fcd0>>
__getattribute__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count'), {}) -> 212
__setattr__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count', 213), {}) -> None
increment((<__main__.Counter object at 0x7fe020d4fcd0>, 1), {}) -> None
__getattribute__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count'), {}) -> 208
increment((<__main__.Counter object at 0x7fe020d4fcd0>, 1), {}) -> None
__setattr__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count', 209), {}) -> None
increment((<__main__.Counter object at 0x7fe020d4fcd0>, 1), {}) -> None
__getattribute__((<__main__.Counter object at 0x7fe020d4fcd0>, 'increment'), {}) -> <bound method Counter.increment of <__main__.Counter object at 0x7fe020d4fcd0>>
increment((<__main__.Counter object at 0x7fe020d4fcd0>, 1), {}) -> None
__getattribute__((<__main__.Counter object at 0x7fe020d4fcd0>, 'increment'), {}) -> <bound method Counter.increment of <__main__.Counter object at 0x7fe020d4fcd0>>
__getattribute__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count'), {}) -> 209
__setattr__((<__main__.Counter object at 0x7fe020d4fcd0>, 'count', 210), {}) -> None
increment((<__main__.Counter object at 0x7fe020d4fcd0>, 1), {}) -> None
```

# まとめ
Effective Python で説明されているように、単純なカウンターのインクリメントの並行処理がアトミックでないことを確認することができました。

なぜ同じコードで同じ結果が得られなかったのでしょうか？確かに理論的には、（おおまかに） 3 ステップで行われているカウンターのインクリメント処理がコンテキストスイッチ時に分断されてしまう可能性はありますが、おそらく私の環境ではその 3 ステップが瞬時に終わってしまい、その結果、分断されることがなかったのではないかと考えられます。ところが trace デコレータで態とそれぞれのステップで時間がかかるようにしたことによって、アトミックな処理でないことの確認ができるようになったのでしょう。

このように、並行処理のプログラムを書いて、ローカルでテストして問題がないと思っても、実はスレッドセーフではないということがあり得るので、そのあたりの確認には注意深さが必要であることを示唆する事例だったのではないかと思います。