スレッドセーフ性の保証¶
このページでは、Python のフリースレッドビルドにおける組み込み型のスレッドセーフ性の保証を説明します。 ここで説明する保証は、GIL を無効にして Python を使う場合 (フリースレッドモード) に適用されます。 GIL が有効な場合、ほとんどの操作は暗黙に直列化されます。
フリースレッドの Python でスレッドセーフなコードを書くための一般的な指針は、Python のフリースレッド対応 を参照してください。
スレッドセーフ性のレベル¶
C API のドキュメントでは、各関数のスレッドセーフ性の保証を次のレベルで表します。 安全性の低いものから高いものの順に示します。
並行使用不可 (Incompatible)¶
外部で同期しても、安全に並行使用できない関数や操作です。 このレベルのコードは通常、同期せずにグローバル状態へアクセスするため、プログラムの存続期間を通して、単一のスレッドからだけ呼び出さなければなりません。
例: シグナルハンドラや環境変数など、プロセス全体の状態を変更する関数です。 外部でロックを取得していても、複数のスレッドからの並行呼び出しが、ランタイムやほかのライブラリと競合する可能性があります。
外部同期により並行使用可能 (Compatible)¶
呼び出し側が外部で適切に同期する ことを条件に、複数のスレッドから安全に呼び出せる関数や操作です。 たとえば、呼び出しのたびに、呼び出しが続く間 ロック を保持して同期します。 こうした同期がないと、並行呼び出しによって 競合状態 や データ競合 が発生する可能性があります。
例: 内部状態がロックで保護されていないオブジェクトを読み書きする関数です。 呼び出し側は、二つのスレッドが同じオブジェクトに同時にアクセスしないことを保証しなければなりません。
異なるオブジェクトなら安全¶
各スレッドが 異なる オブジェクトを操作する限り、外部で同期せずに複数のスレッドから安全に呼び出せる関数や操作です。 二つのスレッドが同時にこの関数を呼び出してもかまいませんが、同じオブジェクト (または内部の状態を共有するオブジェクト) を引数に渡してはなりません。
例: アトミックでない書き込みを使って、構造体のフィールドを変更する関数です。 二つのスレッドがそれぞれ自分の構造体インスタンスに対してこの関数を呼び出すのは安全ですが、同じ インスタンスに対する並行呼び出しには、外部での同期が必要です。
アトミック¶
ほかのスレッドから見ると アトミック に実行される関数や操作です。 つまり、ほかのスレッドの視点では、一瞬で実行されるように見えます。 これはスレッドセーフ性の最も強い保証です。
例: PyMutex_IsLocked() はミューテックスの状態をアトミックに読み取り、どのスレッドからでも、いつでも呼び出せます。
list オブジェクトのスレッドセーフ性¶
lst[i] # list.__getitem__
次のメソッドはリストを走査し、各要素を アトミック に読み取って処理を行います。 そのため、並行して行われる変更の影響を受けた結果を返すことがあります。
item in lst
lst.index(item)
lst.count(item)
All of the above operations avoid acquiring per-object locks. They do not block concurrent modifications. Other operations that hold a lock will not block these from observing intermediate states.
これ以降のほかの操作はすべて、オブジェクトごとのロック を使ってブロックします。
lst[i] = x による単一要素の書き込みは、複数のスレッドから安全に実行でき、リストを破損させません。
次の操作は新しいオブジェクトを返し、ほかのスレッドから見ると アトミック に実行されます。
lst1 + lst2 # 二つのリストを結合して新しいリストを作成する
x * lst # lst を x 回繰り返して新しいリストを作成する
lst.copy() # リストの浅いコピーを返す
次のメソッドは、要素をずらす必要がなく、単一の要素だけを操作するため、アトミック です。
lst.append(x) # リストの末尾に追加するため、要素をずらす必要はない
lst.pop() # リストの末尾の要素を取り出して削除するため、要素をずらす必要はない
clear() メソッドも アトミック です。
ほかのスレッドから、要素が削除されていく途中の状態は観測できません。
sort() メソッドは アトミック ではありません。
ほかのスレッドからソート途中の状態は観測できませんが、ソート中のリストは空に見えます。
次の操作は複数の要素をその場で変更するため、ロックフリーな 操作から途中の状態が観測されることがあります。
lst.insert(idx, item) # 要素をずらす
lst.pop(idx) # idx がリストの末尾でなければ、要素をずらす
lst *= x # 要素をその場でコピーする
要素の比較で (__eq__() を通じて) 任意の Python コードを実行する可能性があるため、remove() メソッドの実行中に並行して変更が行われることがあります。
extend() は複数のスレッドから安全に呼び出せます。
ただし、その保証は渡されたイテラブルによって異なります。
渡されたイテラブルが list、tuple、set、frozenset、dict、辞書ビューオブジェクト のいずれか (サブクラスは除く) である場合、extend 操作はそのイテラブルに対する並行変更から保護されます。
それ以外の場合は、ほかのスレッドによって並行して変更される可能性があるイテレータが作成されます。
lst += iterable を使ってリストとほかのイテラブルをインプレースに結合する場合も、同じことが当てはまります。
同様に、lst[i:j] = iterable によるリストのスライスへの代入も、複数のスレッドから安全に実行できますが、iterable がロックされるのは、iterable も list (サブクラスは除く) である場合だけです。
複数回のアクセスを伴う操作やイテレーションは、アトミックではありません。 たとえば、次のような操作です。
# アトミックではない: 読み取り、変更、書き込み
lst[i] = lst[i] + 1
# アトミックではない: 確認してから実行
if lst:
item = lst.pop()
# スレッドセーフではない: 変更中のイテレーション
for item in lst:
process(item) # ほかのスレッドが lst を変更する可能性がある
スレッド間で list インスタンスを共有する場合は、外部での同期を検討してください。
dict オブジェクトのスレッドセーフ性¶
Creating a dictionary with the dict constructor is atomic when the
argument to it is a dict or a tuple. When using the
dict.fromkeys() method, dictionary creation is atomic when the
argument is a dict, tuple, set or
frozenset.
The following operations and functions are lock-free and atomic.
d[key] # dict.__getitem__
d.get(key) # dict.get
key in d # dict.__contains__
len(d) # dict.__len__
All other operations from here on hold the per-object lock.
単一の要素の書き込みや削除は、複数のスレッドから安全に実行でき、辞書を破損させません。
d[key] = value # 書き込む
del d[key] # 削除する
d.pop(key) # 削除して返す
d.popitem() # 最後の要素を削除して返す
d.setdefault(key, v) # 存在しなければ挿入する
これらの操作は __eq__() を使ってキーを比較することがあり、その際に任意の Python コードを実行する可能性があります。
このような比較中に、ほかのスレッドが辞書を変更する可能性があります。
__eq__() を C で実装している str、int、float などの組み込み型では、比較中に内部のロックを解放しないため、この心配はありません。
The following operations return new objects and hold the per-object lock for the duration of the operation:
d.copy() # 辞書の浅いコピーを返す
d | other # 二つの辞書をマージして新しい辞書を作成する
d.keys() # 新しい dict_keys ビューオブジェクトを返す
d.values() # 新しい dict_values ビューオブジェクトを返す
d.items() # 新しい dict_items ビューオブジェクトを返す
clear() メソッドは、処理中ずっとロックを保持します。
ほかのスレッドから、要素が削除されていく途中の状態は観測できません。
次の操作は両方の辞書をロックします。
update() と |= では、もう一方のオペランドが標準の辞書イテレータを使う dict である場合だけ、この保証が適用されます (イテレーションをオーバーライドするサブクラスは除きます)。
等値比較では、dict とそのサブクラスにこの保証が適用されます。
d.update(other_dict) # other_dict が dict の場合は両方をロックする
d |= other_dict # other_dict が dict の場合は両方をロックする
d == other_dict # dict とそのサブクラスの場合は両方をロックする
比較操作はすべて、値の比較にも __eq__() を使うため、組み込み型でない場合は比較中にロックを解放することがあります。
イテラブルが dict、set、frozenset そのもの (サブクラスは除く) である場合、fromkeys() は新しい辞書とそのイテラブルの両方をロックします。
dict.fromkeys(a_dict) # 両方をロックする
dict.fromkeys(a_set) # 両方をロックする
dict.fromkeys(a_frozenset) # 両方をロックする
辞書ではないイテラブルから更新する場合、更新先の辞書だけをロックします。 イテラブルは、ほかのスレッドによって並行して変更される可能性があります。
d.update(iterable) # iterable が dict ではない: d だけをロックする
d |= iterable # iterable が dict ではない: d だけをロックする
dict.fromkeys(iterable) # iterable が dict/set/frozenset ではない: 結果だけをロックする
複数回のアクセスを伴う操作やイテレーションは、アトミックではありません。
# アトミックではない: 読み取り、変更、書き込み
d[key] = d[key] + 1
# アトミックではない: 確認してから実行 (TOCTOU)
if key in d:
del d[key]
# スレッドセーフではない: 変更中のイテレーション
for key, value in d.items():
process(key) # ほかのスレッドが d を変更する可能性がある
確認から使用までの間に状態が変わる問題 (TOCTOU: time-of-check to time-of-use) を避けるには、アトミックな操作を使うか、例外を処理してください。
# 確認してから削除する代わりに、デフォルト値付きの pop() を使う
d.pop(key, None)
# または例外を処理する
try:
del d[key]
except KeyError:
pass
ほかのスレッドによって変更される可能性のある辞書を安全にイテレートするには、コピーをイテレートしてください。
# 安全にイテレートするためにコピーを作成する
for key, value in d.copy().items():
process(key)
スレッド間で dict インスタンスを共有する場合は、外部での同期を検討してください。
set オブジェクトのスレッドセーフ性¶
The following read operation is lock-free. It does not block concurrent modifications and may observe intermediate states from operations that hold the per-object lock:
elem in s # set.__contains__
This operation may compare elements using __eq__(), which can
execute arbitrary Python code. During such comparisons, the set may be
modified by another thread. For built-in types like str,
int, and float, __eq__() does not release the
underlying lock during comparisons and this is not a concern.
これ以降のほかの操作はすべて、オブジェクトごとのロックを保持します。
単一の要素の追加や削除は、複数のスレッドから安全に実行でき、集合を破損させません。
s.add(elem) # 要素を追加する
s.remove(elem) # 要素を削除し、存在しなければ例外を送出する
s.discard(elem) # 要素が存在すれば削除する
s.pop() # 任意の要素を削除して返す
これらの操作も要素を比較するため、__eq__() についての上記と同じ注意事項が当てはまります。
copy() メソッドは新しいオブジェクトを返し、処理中ずっとオブジェクトごとのロックを保持するため、常にアトミックです。
clear() メソッドは、処理中ずっとロックを保持します。
ほかのスレッドから、要素が削除されていく途中の状態は観測できません。
The following operations only accept set or frozenset
as operands and always lock both objects:
s |= other # other は set/frozenset でなければならない
s &= other # other は set/frozenset でなければならない
s -= other # other は set/frozenset でなければならない
s ^= other # other は set/frozenset でなければならない
s & other # other は set/frozenset でなければならない
s | other # other は set/frozenset でなければならない
s - other # other は set/frozenset でなければならない
s ^ other # other は set/frozenset でなければならない
set.update()、set.union()、set.intersection()、set.difference() は、引数として複数のイテラブルを受け取れます。
これらはいずれも、渡されたすべてのイテラブルをイテレートし、次の処理を行います。
set.update()andset.union()lock both objects only when the other operand is aset,frozenset, ordict.
set.intersection()とset.difference()は、常にすべてのオブジェクトのロックを試みます。
set.symmetric_difference() は両方のオブジェクトのロックを試みます。
上記のメソッドに対応する更新用メソッドにも、次のような違いがあります。
set.difference_update()とset.intersection_update()は、すべてのオブジェクトを一つずつロックしようとします。
set.symmetric_difference_update()は、引数の型がset、frozenset、dictのいずれかである場合だけ、その引数をロックします。
次のメソッドは、常に両方のオブジェクトのロックを試みます。
s.isdisjoint(other) # 両方をロックする
s.issubset(other) # 両方をロックする
s.issuperset(other) # 両方をロックする
複数回のアクセスを伴う操作やイテレーションは、アトミックではありません。
# アトミックではない: 確認してから実行
if elem in s:
s.remove(elem)
# スレッドセーフではない: 変更中のイテレーション
for elem in s:
process(elem) # ほかのスレッドが s を変更する可能性がある
スレッド間で set インスタンスを共有する場合は、外部での同期を検討してください。
詳細については、Python のフリースレッド対応 を参照してください。
bytearray オブジェクトのスレッドセーフ性¶
結合と比較はバッファプロトコルを使います。 バッファプロトコルはサイズの変更を防ぎますが、オブジェクトごとのロックは保持しません。 これらの操作は、並行して行われる変更の途中の状態を観測することがあります。
ba + other # 並行して行われる書き込みを観測する可能性がある ba == other # 並行して行われる書き込みを観測する可能性がある ba < other # 並行して行われる書き込みを観測する可能性があるこれ以降のほかの操作はすべて、オブジェクトごとのロックを保持します。
単一の要素やスライスの読み取りは、複数のスレッドから安全に実行できます。
ba[i] # bytearray.__getitem__ ba[i:j] # スライス次の操作は複数のスレッドから安全に実行でき、バイト配列を破損させません。
ba[i] = x # 単一のバイトを書き込む ba[i:j] = values # スライスに書き込む ba.append(x) # 単一のバイトを末尾に追加する ba.extend(other) # イテラブルを使って拡張する ba.insert(i, x) # 単一のバイトを挿入する ba.pop() # 最後のバイトを削除して返す ba.pop(i) # 指定のインデックスのバイトを削除して返す ba.remove(x) # 最初に現れた値を削除する ba.reverse() # その場で順序を反転する ba.clear() # すべてのバイトを削除する
valuesがbytearrayの場合、スライスへの代入は両方のオブジェクトをロックします。ba[i:j] = other_bytearray # 両方をロックする次の操作は新しいオブジェクトを返し、処理中ずっとオブジェクトごとのロックを保持します。
ba.copy() # 浅いコピーを返す ba * n # 繰り返して新しいバイト配列を作成する要素が含まれるかどうかの判定では、処理中ずっとロックを保持します。
x in ba # bytearray.__contains__All other bytearray methods (such as
find(),replace(),split(),decode(), etc.) hold the per-object lock for their duration.複数回のアクセスを伴う操作やイテレーションは、アトミックではありません。
# アトミックではない: 確認してから実行 if x in ba: ba.remove(x) # スレッドセーフではない: 変更中のイテレーション for byte in ba: process(byte) # ほかのスレッドが ba を変更する可能性があるほかのスレッドによって変更される可能性のあるバイト配列を安全にイテレートするには、コピーをイテレートしてください。
# 安全にイテレートするためにコピーを作成する for byte in ba.copy(): process(byte)スレッド間で
bytearrayインスタンスを共有する場合は、外部での同期を検討してください。 詳細については、Python のフリースレッド対応 を参照してください。
memoryview オブジェクトのスレッドセーフ性¶
memoryview オブジェクトは、参照先のオブジェクトの内部データに、コピーせずにアクセスする手段を提供します。
スレッドセーフ性は、メモリービュー自体と、参照先のバッファを提供するオブジェクトの両方によって決まります。
The memoryview implementation uses atomic operations to track its own
exports in the free-threaded build. Creating and
releasing a memoryview are thread-safe. Attribute access (e.g.,
shape, format) reads fields that
are immutable for the lifetime of the memoryview, so concurrent reads
are safe as long as the memoryview has not been released.
ただし、メモリービューを通じてアクセスする実際のデータは、参照先のオブジェクトが所有しています。 このデータへの並行アクセスが安全なのは、参照先のオブジェクトがそれをサポートしている場合だけです。
bytesのような不変のオブジェクトでは、複数のメモリービューを通じて並行して読み取っても安全です。bytearrayのような可変のオブジェクトでは、外部で同期せずに、複数のスレッドから同じメモリー領域を読み書きするのは安全ではなく、データの破損を引き起こす可能性があります。 可変のオブジェクトに対する読み取り専用のメモリービューであっても、ほかのスレッドから参照先のオブジェクトが変更されると、データ競合を防げないことに注意してください。
# 安全ではない: 同じバッファへの並行書き込み
data = bytearray(1000)
view = memoryview(data)
# スレッド 1: view[0:500] = b'x' * 500
# スレッド 2: view[0:500] = b'y' * 500
# 安全: 並行アクセスにはロックを使う
import threading
lock = threading.Lock()
data = bytearray(1000)
view = memoryview(data)
with lock:
view[0:500] = b'x' * 500
メモリービューによってバッファが公開されている間に、参照先のオブジェクトのサイズを変更したり、メモリーを再割り当てしたりすると (bytearray.resize() の呼び出しなど)、BufferError が送出されます。
これは、スレッドの使用に関係なく適用されます。