Aikido

競合状態を防ぐ方法:共有状態へのスレッドセーフなアクセス

バグのリスク

ルール
確保 スレッドセーフ アクセス への 共有 状態にアクセスできるようにする。
共有 ミュータブル 状態 アクセス 複数の 複数の スレッド
同期なし 同期 原因 レース 競合状態 そして 実行時エラー エラーを引き起こす。

対応言語 Python、 Java C#

はじめに

複数のスレッドが同期を取らずに共有変数にアクセスしたり変更したりすると、競合状態が発生する。最終的な値は予測不可能なスレッドの実行タイミングに依存し、データの破損や不正な計算、ランタイム・エラーにつながる。ロックのない複数のスレッドによってインクリメントされたカウンターは、スレッドが古い値を読み込んでインクリメントし、競合する結果を書き戻すため、更新を見逃すことになる。

なぜそれが重要なのか

データ破損と不正確な結果:レース状態は、値の一貫性がなくなったり、不正確になったりするサイレント・データ破損を引き起こす。口座残高が間違ったり、在庫数がマイナスになったり、集計された統計値が破損したりします。これらのバグはスレッドの正確なタイミングに依存するため、再現が困難です。

システムの不安定性:共有状態への非同期アクセスはアプリケーションをクラッシュさせる可能性がある。あるスレッドがデータ構造を変更している間に別のスレッドがそれを読み込んでしまい、ヌル・ポインタ・エラーやインデックス・アウト・オブ・バウンズなどの例外が発生する可能性がある。実稼働環境では、これらは負荷がかかると断続的なクラッシュとして現れます。

デバッグの複雑さ:レースコンディションは非決定論的であるため、デバッグが難しいことで有名である。シングルスレッドのテストや低負荷の環境ではバグが現れないかもしれません。再現には特定のスレッドインターリーブが必要ですが、それを強制するのは難しく、問題がランダムに現れたり消えたりします。

コード例

非準拠:

class BankAccount:
    def  __init__(self):
 self.balance = 0

    def deposit(self, amount):
 current = self.balance
        # レース条件: 別のスレッドがここで残高を変更できる
 time.sleep(0.001) # 処理時間をシミュレート
 self.balance = current + amount

    def withdraw(self, amount):
        if self.balance >= amount:
            現在の残高 = self.balance
            time.sleep(0.001)
            self.balance = current - amount
            return True
        return False

なぜ間違っているのか:複数のスレッドが同時にdeposit()やwithdraw()を呼び出すと、競合状態が発生する。100ドルずつ入金する2つのスレッドが、ともに残高を0ドルとして読み込んだ後、ともに100ドルを書き込んだ結果、最終的な残高は200ドルではなく100ドルになってしまうかもしれない。

✅ 準拠:

 インポート・スレッディング

class BankAccount:
    def  __init__(self):
 self.__balance = 0
 self.__lock = threading.Lock()

   @property
    def balance(self):
        with self.__lock:
            return self.__balance

    def deposit(self, amount):
        with self.__lock:
            current = self.__balance
            time.sleep(0.001)
            self.__balance = current + amount

    def withdraw(self, amount):
        with self.__lock:
            if self.__balance >= amount:
                現在の残高 = self.__balance
                time.sleep(0.001)
                self.__balance = current - amount
                return True
            return False

なぜこれが重要なのか: 会社情報 threading.Lock() は、一度に1つのスレッドだけがバランスにアクセスすることを保証する。1つのスレッドがロックを保持すると、他のスレッドは待機し、同時修正を防ぎます。プライベート バランス 読み取り専用 プロパティ 外部コードがロック保護をバイパスするのを防ぎます。

結論

ロック、セマフォ、アトミック操作のような適切な同期プリミティブを使用して、共有されたすべての変更可能な状態を保護する。可能であれば、不変のデータ構造またはスレッドローカルストレージを優先する。同期が必要な場合は、競合を減らしパフォーマンスを向上させるために、クリティカルセクションを最小限にする。

よくある質問

ご質問は?

どの同期プリミティブを使うべきか?

共有状態への排他的アクセスにはロック(ミューテックス)を使用する。リソースへの同時アクセスを制限するにはセマフォを使う。スレッドの調整とシグナリングには条件変数を使う。単純なカウンタやフラグの場合は、ロックよりもアトミック操作の方が速い。相互排他にはロック、単純な操作にはアトミック、producer-consumerパターンにはキューのような高レベルのコンストラクトと、同時実行パターンに応じて選択する。

複数のロックを使用する場合、デッドロックを回避するにはどうすればよいですか?

すべてのコードパスで、常に同じ順序でロックを取得する。A関数がXとYのロックを必要とし、B関数がYとXのロックを必要とする場合、一貫した順序(常にX→Y)でロックを取得する。デッドロックの可能性を検出するために、タイムアウトベースのロック取得を使用する。さらに良いのは、クリティカルセクションごとに1つのロックしか必要としないように再設計するか、ロックフリーのデータ構造を使用することだ。

同期のパフォーマンスへの影響は?

ロックの競合は、スレッドがロック保持者の解放を待つため、高度な並行コードを遅くする。しかし、誤った非同期コードは、誤った結果を生み出すため、限りなく遅くなる。ロック・スコープ(クリティカル・セクション)を最小限にして、状態の変更だけを保護する。複数のリーダが衝突しない場合は、読み書きロックを使う。最適化する前にプロファイルを作成する。

ロックの代わりにスレッドローカルストレージを使うことはできますか?

そう、各スレッドが独自のデータコピーを必要とする場合だ。スレッドローカルストレージは、各スレッドにプライベートな状態を与えることで、同期のオーバーヘッドを排除します。スレッドごとのキャッシュ、バッファ、または後でマージされるアキュムレータに使用します。ただし、スレッドが通信したり最終結果を共有したりする場合は、同期が必要です。

PythonのGlobal Interpreter Lock (GIL)はどうですか?

GILはロックの必要性をなくすものではない。Pythonバイトコードの同時実行を防ぐことはできますが、操作をアトミックにすることはできません。単純なインクリメントカウンタ += 1 は複数のバイトコード操作を含み、その間に GIL を解放することができます。CPython であっても、共有状態には常に適切な同期を使いましょう。

レースコンディションをテストするには?

その言語に特化したスレッドサニタイザーや同時実行テストツールを使用する。並行処理を実行する多数のスレッドを生成し、不変性が保持されることを保証するストレステストを書く。スレッド数と反復回数を増やして、タイミング依存のバグを明らかにする。しかし、テストに合格したからといって、競合状態がないことを証明できるわけではないので、コードレビューと慎重な同期設計が重要であることに変わりはない。

ロックフリーとウェイトフリーのデータ構造とは?

ロックフリーのデータ構造は、ロックの代わりにアトミック操作(比較とスワップ)を使用し、スレッドが遅延してもシステム全体の進捗を保証する。ウェイトフリー構造はスレッドごとの進捗を保証する。これらを正しく実装するのは複雑だが、競合が多い場合にはパフォーマンスが向上する。独自に実装するのではなく、十分にテストされたライブラリ(java.util.concurrent、C++アトミック・ライブラリ)を使用すること。

まずは無料で体験

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要。