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:
            current = self.balance
            time.sleep(0.001)
            self.balance = current - amount
            return True
        return False

誤っている理由: 複数のスレッドが同時に deposit() または withdraw() を呼び出すと、競合状態が発生します。それぞれ100ドルを預金する2つのスレッドが、両方とも残高を0ドルと読み取り、その後両方とも100ドルを書き込むことで、最終的な残高が200ドルではなく100ドルになる可能性があります。

✅ 準拠済み:

import threading

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:
                current = self.__balance
                time.sleep(0.001)
                self.__balance = current - amount
                return True
            return False

これが重要である理由: 会社情報 threading.Lock() 一度に1つのスレッドのみがバランスにアクセスすることを保証します。1つのスレッドがロックを保持している間、他のスレッドは待機し、同時変更を防ぎます。プライベート __balance readonlyで @property 外部コードによるロック保護のバイパスを防止します。

まとめ

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

よくある質問

ご質問がありますか?

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

共有状態への排他的アクセスにはロック(ミューテックス)を使用してください。リソースへの同時アクセスを制限するにはセマフォを使用してください。スレッドの協調とシグナリングには条件変数を使用してください。単純なカウンターやフラグの場合、アトミック操作はロックよりも高速です。並行処理パターンに基づいて選択してください:相互排他にはロック、単純な操作にはアトミック、プロデューサー・コンシューマーパターンにはキューのような高レベルの構成要素。

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

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

同期がパフォーマンスに与える影響は何ですか?

ロック競合は、スレッドがロック保持者の解放を待つため、高並行性コードの速度を低下させます。しかし、誤った非同期コードは、誤った結果を生成するため、無限に遅くなります。ロックのスコープ(クリティカルセクション)を最小限に抑え、状態の変更のみを保護するようにしてください。複数のリーダーが競合しない場合は、読み書きロックを使用してください。最適化の前にプロファイリングを行い、正確性を最優先してください。

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

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

Pythonのグローバルインタプリタロック (GIL) についてはどうですか?

GILはロックの必要性を排除しません。GILはPythonバイトコードの同時実行を防ぎますが、操作をアトミックにするわけではありません。単純なインクリメント操作 `counter += 1` は複数のバイトコード操作を含み、その間にGILが解放される可能性があります。CPythonであっても、共有状態には常に適切な同期を使用してください。

競合状態をどのようにテストしますか?

各言語に特化したスレッドサニタイザーや並行性テストツールを使用します。多数のスレッドを生成し、並行操作を実行して不変条件が維持されることをアサートするストレステストを記述します。スレッド数とイテレーションを増やして、タイミング依存のバグを特定します。ただし、テストの合格は競合状態の不在を証明するものではないため、コードレビューと慎重な同期設計が依然として重要です。

ロックフリーおよびウェイトフリーのデータ構造とは何ですか?

ロックフリーなデータ構造は、ロックの代わりにアトミック操作(compare-and-swap)を使用し、スレッドが遅延してもシステム全体の進行を保証します。ウェイトフリーな構造は、スレッドごとの進行を保証します。これらは正しく実装するのが複雑ですが、高い競合下ではより優れたパフォーマンスを提供します。独自のものを実装するのではなく、実績のあるライブラリ(java.util.concurrent、C++ atomic library)を使用してください。

今すぐ、安全な環境へ。

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

クレジットカードは不要です | スキャン結果は32秒で表示されます。