コンピュータサイエンスにおいて、ロックまたはミューテックス(相互排他から派生)は、複数の実行スレッドが同時に状態を変更したりアクセスしたりするのを防ぐ同期プリミティブです。ロックは相互排他的な並行性制御ポリシーを強制し、様々な実装方法が存在するため、異なるアプリケーション向けに複数の独自の実装が存在します。
一般的に、ロックはアドバイザリロックであり、各スレッドは対応するデータにアクセスする前にロックを取得することで協力します。一部のシステムでは、強制ロックも実装されており、ロックされたリソースへの不正アクセスを試みると、アクセスを試みたエンティティで例外が発生します。
最も単純なロック方式はバイナリセマフォです。これはロックされたデータへの排他的アクセスを提供します。他の方式では、データの読み取りに対して共有アクセスを提供するものもあります。その他、広く実装されているアクセスモードには、排他的アクセス、除外意図アクセス、アップグレード意図アクセスなどがあります。
ロックを分類するもう一つの方法は、ロック戦略がスレッドの進行を妨げる場合に何が起こるかによって分類することです。ほとんどのロック設計では、ロックを要求するスレッドがロックされたリソースへのアクセスを許可されるまで、そのスレッドの実行をブロックします。スピンロックの場合、スレッドはロックが利用可能になるまで単に待機(「スピン」)します。これは、スレッドが短時間ブロックされる場合に効率的です。なぜなら、オペレーティングシステムのプロセス再スケジューリングのオーバーヘッドを回避できるからです。ロックが長時間保持される場合、またはロックを保持しているスレッドの進行がロックされたスレッドのプリエンプションに依存する場合は、非効率的です。
ロックを効率的に実装するには、通常ハードウェアによるサポートが必要です。このサポートは通常、 「テストアンドセット」、「フェッチアンドアッド」、「コンペアアンドスワップ」などの1つ以上のアトミック命令の形で提供されます。これらの命令により、単一のプロセスがロックが空いているかどうかをテストし、空いている場合は単一のアトミック操作でロックを取得できます。
単一プロセッサアーキテクチャでは、割り込みを一時的に無効にする特殊な命令や命令プレフィックスを使用して、割り込み不可能な命令シーケンスを使用するオプションがありますが、この手法はマルチプロセッサの共有メモリマシンでは機能しません。マルチプロセッサ環境でロックを適切にサポートするには、非常に複雑なハードウェアまたはソフトウェアのサポートが必要となり、同期に関する大きな問題が発生する可能性があります。
アトミック操作が必要な理由は、複数のタスクが同じロジックを実行する並行処理が存在するためです。例えば、次のC言語のコードを考えてみましょう。
if ( lock == 0 ) { // ロックが空いている場合は、ロックを設定するlock = pid ; }上記の例では、複数のタスクが同時にロックをテストする可能性があるため、タスクがロックを取得できることを保証するものではありません。両方のタスクはロックが空いていることを検出し、互いにロックを設定しようとしますが、もう一方のタスクもロックを設定しようとしていることには気づきません。アトミックロック操作が利用できない場合は、デッカーのアルゴリズムまたはピーターソンのアルゴリズムが代替手段となり得ます。
ロックを不用意に使用すると、デッドロックやライブロックが発生する可能性があります。設計時と実行時の両方において、デッドロックやライブロックを回避または回復するための様々な戦略が存在します。(最も一般的な戦略は、相互依存するロックの組み合わせが常に特定の「カスケード」順序で取得されるように、ロック取得シーケンスを標準化することです。)
一部のプログラミング言語は、構文的にロックをサポートしています。C #の例を以下に示します。
名前空間Wikipedia.Examples ;System.Threadingを使用します。public class Account // これはアカウントのモニターです{ // C# 13 より前のバージョンでは `object` を使用しますprivate readonly Lock _balanceLock = new (); private decimal _balance = 0 ;public void Deposit ( decimal amount ) { // このステートメントは一度に1つのスレッドのみが実行できます。lock ( _balanceLock ) { _balance += amount ; } }public void Withdraw ( decimal amount ) { // このステートメントは一度に 1 つのスレッドのみが実行できます。lock ( _balanceLock ) { _balance -= amount ; } } }C# は、.NET 9上の C# 13 でSystem.Threading.Lockを導入しました。
lock(this)インスタンスが一般にアクセス可能な場合、このコードは問題を引き起こす可能性があります。 [ 1 ]
Javaと同様に、C#でもMethodImplOptions.Synchronized属性を使用することでメソッド全体を同期できます。[ 2 ] [ 3 ]
[MethodImpl(MethodImplOptions.Synchronized)] public void SomeMethod () { // 何らかの処理を行う}ロックの粒度について説明する前に、ロックに関する次の3つの概念を理解しておく必要があります。
同期に使用するロックの数を選択する際には、ロックのオーバーヘッドを減らすことと、ロックの競合を減らすことの間にはトレードオフが存在する。
ロックの重要な特性の 1 つは粒度です。粒度は、ロックが保護するデータの量の尺度です。一般に、粗い粒度 (少数のロックで、それぞれが大きなデータセグメントを保護) を選択すると、単一のプロセスが保護されたデータにアクセスする際のロックのオーバーヘッドは少なくなりますが、複数のプロセスが同時に実行される場合のパフォーマンスは低下します。これは、ロックの競合が増加するためです。ロックが粗いほど、無関係なプロセスが処理を続行できなくなる可能性が高くなります。逆に、細かい粒度 (多数のロックで、それぞれがかなり小さな量のデータを保護) を使用すると、ロック自体のオーバーヘッドは増加しますが、ロックの競合は減少します。各プロセスが共通のロックセットから複数のロックを保持する必要がある粒度の高いロックでは、微妙なロックの依存関係が発生する可能性があります。この微妙さにより、プログラマが知らず知らずのうちにデッドロックを発生させる可能性が高まります。
例えば、データベース管理システムでは、ロックは粒度の小さい順に、フィールドの一部、フィールド、レコード、データページ、またはテーブル全体を保護することができます。テーブルロックのような粗い粒度は、単一ユーザーに対して最高のパフォーマンスを発揮する傾向があり、レコードロックのような細かい粒度は、複数ユーザーに対して最高のパフォーマンスを発揮する傾向があります。
データベースロックは、トランザクションの同期性を確保する手段として使用できます。つまり、トランザクション処理を並行処理(トランザクションのインターリーブ)する場合、2フェーズロックを使用することで、トランザクションの並行実行がトランザクションの直列実行と同等になることが保証されます。しかし、データベースにおけるロックの望ましくない副作用としてデッドロックが発生します。デッドロックは、トランザクション間のロック順序を事前に決定することで防止するか、待機グラフを使用して検出します。デッドロックを回避しながらデータベースの同期性を確保するための代替手段として、完全に順序付けられたグローバルタイムスタンプを使用する方法があります。
データベース上で複数の同時ユーザーによる操作を管理するために、いくつかのメカニズムが用いられています。その目的は、更新の消失やダーティリードを防ぐことです。ロックには、悲観的ロックと楽観的ロックの2種類があります。
これらの主要なロックタイプには、それぞれ異なる動作のバリエーションや改良版が存在します。あるロックが別のロックをブロックする場合、2つのロックは互換性がないと言われます。そうでない場合は、互換性があると言われます。多くの場合、ロックタイプの相互作用のブロックは、技術文献では「ロック互換性表」で示されます。以下は、一般的な主要なロックタイプの例です。
コメント:一部の出版物では、表のエントリは単に「互換性あり」または「互換性なし」、あるいはそれぞれ「はい」または「いいえ」とマークされています。[ 5 ]
ロックベースのリソース保護とスレッド/プロセス同期には多くの欠点がある。
並行性制御戦略の中には、これらの問題の一部または全部を回避するものがあります。例えば、ファネルやトークンのシリアライズは、最大の問題であるデッドロックを回避できます。ロックの代替手段としては、ロックフリープログラミング技術やトランザクショナルメモリなどの非ブロッキング同期方式があります。しかし、こうした代替手段では、実際のロック機構をオペレーティングソフトウェアのより基本的なレベルで実装する必要がある場合が多くあります。そのため、アプリケーションレベルではロックの実装の詳細を省略できるだけで、上記の問題は依然としてアプリケーションレベルで対処する必要があるのです。
ほとんどの場合、適切なロックは、CPUがアトミックな命令ストリーム同期方法を提供することに依存します(たとえば、パイプラインへの項目の追加または削除には、特定の項目を追加または削除するために必要なメモリ内容の操作中、パイプライン内の他の項目を追加または削除する必要のあるすべての同時操作を中断する必要があります)。したがって、アプリケーションは、オペレーティングシステムに与える負荷を認識し、不可能な要求の報告を適切に認識できる場合、より堅牢になることがよくあります。
ロックベースのプログラミングの最大の問題点の 1 つは、「ロックは合成できない」ということです。つまり、小さくて正しいロックベースのモジュールを、モジュールを変更したり、少なくともその内部構造を知らなければ、同じように正しい大きなプログラムに組み合わせるのは難しいのです。ソフトウェアのトランザクション メモリの提唱者であるSimon Peyton Jones は、銀行アプリケーションの次の例を挙げています。[ 6 ]複数の同時クライアントが口座にお金を預けたり引き出したりできるAccountクラスを設計し、ある口座から別の口座にお金を移動するためのアルゴリズムを提供します。
問題の最初の部分に対するロックベースの解決策は次のとおりです。
クラスAccount: メンバーbalance: Integer メンバーmutex: Lock メソッドdeposit(n: Integer) mutex.lock() バランス ← バランス + n mutex.unlock() メソッドwithdraw(n: Integer) 預金(−n)
問題の2番目の部分ははるかに複雑です。逐次プログラムに対して正しい転送ルーチンは次のようになります。
function transfer(from: Account, to: Account, amount: Integer) from.withdraw(amount) 金額を入金する
並行プログラムでは、このアルゴリズムは不適切です。なぜなら、あるスレッドが送金処理の途中で、別のスレッドが、最初の口座から金額が引き出されたものの、もう一方の口座にはまだ入金されていない状態を観測する可能性があるからです。つまり、システムからお金が消えてしまうのです。この問題を完全に解決するには、どちらかの口座を変更する前に両方の口座にロックをかける必要がありますが、その場合、デッドロックを防ぐために、ロックは任意のグローバルな順序で配置しなければなりません。
function transfer(from: Account, to: Account, amount: Integer) if from < to // ロックの順序は任意 from.lock() to.lock() それ以外 to.lock() from.lock() from.withdraw(amount) 金額を入金する from.unlock() to.unlock()
ロックが増えるとこの解決策はより複雑になり、転送関数はすべてのロックについて知る必要があるため、ロックを隠すことはできません。
プログラミング言語によって、同期機能のサポート状況は異なります。
<threads.h>std::mutexstd::lock_guardstd::unique_lockstd::scoped_lock<mutex>synchronizeは同期するメソッドの属性を提供しますが、これはWindowsアーキテクチャの COM オブジェクトとVisual C++コンパイラに固有のものです。[ 12 ]lockでは、スレッドがリソースやクラスに排他的アクセスできるようにするためのキーワードが提供されていますSystem.Threading.Lock。[ 13 ]SyncLockは、C# のキーワードと同様のキーワードを提供しますlock。synchronized、コード ブロック、メソッド、またはオブジェクトをロックするためのキーワード[ 14 ]と、並行処理に安全なデータ構造を備えたライブラリを提供します。Java にはインターフェースもありますjava.util.concurrent.locks.Lock。[ 15 ]@synchronized[ 16 ]を提供し、ロックのためのクラス NSLock [ 17 ] 、 NSRecursiveLock [ 18 ]、NSConditionLock [ 19 ]と NSLocking プロトコル[ 20 ]も提供しています。Mutex内のクラス[ 22 ]pthreadsを提供しています。threading.Lockメカニズムを提供します。[ 23 ]lock_type組み込みモジュールiso_fortran_envとlock/ステートメントに派生型を提供しています。[ 24 ]unlockstd::sync::Mutex<T>[ 26 ]構造体を提供します。[ 27 ]LOCKで、それらの操作の原子性を保証します。MVarは空であるか、通常はリソースへの参照である値を保持することができます。リソースを使用したいスレッドは の値を「取得」してMVar空にし、使用が終わったら元に戻します。空の からリソースを取得しようとすると、MVarリソースが使用可能になるまでスレッドはブロックされます。[ 28 ]ロックの代替として、ソフトウェア トランザクショナル メモリの実装も存在します。[ 29 ]sync.Mutexミューテックスは、バイナリセマフォと基本的な実装が同じである場合もあるロック機構です。しかし、使用方法には違いがあります。バイナリセマフォは口語的にミューテックスと呼ばれることがありますが、真のミューテックスにはより具体的な使用例と定義があり、ミューテックスをロックしたタスクのみがロックを解除できるというものです。この制約は、セマフォを使用する際に発生する可能性のあるいくつかの問題に対処することを目的としています。
{{cite web}}: CS1メンテナンス: アーカイブサービスは非推奨になりました (リンク)保護オブジェクトは、保護サブルーチンまたは保護エントリである可視保護操作の呼び出しを通じて、共有データへの協調アクセスを提供します。
{{cite book}}: CS1 maint: 数値名: 著者リスト (リンク){{cite book}}: CS1 maint: 数値名: 著者リスト (リンク)