この質問と以下の私の回答は、主に別の質問の混乱の領域に対応しています。
回答の最後に、WRT の「揮発性」とスレッドの同期に関するいくつかの問題がありますが、これについては完全には確信が持てません。コメントと別の回答を歓迎します。ただし、質問のポイントは主にCPUレジスタとその使用方法に関連しています。
この質問と以下の私の回答は、主に別の質問の混乱の領域に対応しています。
回答の最後に、WRT の「揮発性」とスレッドの同期に関するいくつかの問題がありますが、これについては完全には確信が持てません。コメントと別の回答を歓迎します。ただし、質問のポイントは主にCPUレジスタとその使用方法に関連しています。
レジスタは、CPU の「作業用ストレージ」です。それらは非常に高速ですが、非常に限られたリソースです。通常、CPU には名前付きレジスタの小さな固定セットがあり、名前はその CPU マシン コードのアセンブラー言語規則の一部です。たとえば、32 ビット Intel x86 CPU には、eax、ebx、ecx、および edx という名前の 4 つのメイン データ レジスタと、多数のインデックスおよびその他の特殊なレジスタがあります。
厳密に言えば、これは最近では正しくありません。たとえば、レジスタの名前変更は一般的です。一部のプロセッサには、名前などを付けるのではなく、番号を付けるのに十分なレジスタがあります。ただし、それでも動作するための優れた基本モデルです。たとえば、レジスタの名前変更は、順不同で実行されても、この基本モデルの錯覚を維持するために使用されます。
手動で記述されたアセンブラーでのレジスターの使用は、レジスターの使用の単純なパターンを持つ傾向があります。いくつかの変数は、サブルーチンの実行中、またはサブルーチンのかなりの部分の間、純粋にレジスタに保持されます。その他のレジスタは、読み取り-変更-書き込みパターンで使用されます。例えば...
mov eax, [var1]
add eax, [var2]
mov [var1], eax
IIRC、これは有効な (おそらく非効率的ですが) x86 アセンブラー コードです。Motorola 68000 では、次のように書くかもしれません...
move.l [var1], d0
add.l [var2], d0
move.l d0, [var1]
今回は、ソースは通常、左側のパラメーターで、右側に宛先があります。68000 には 8 つのデータ レジスタ (d0..d7) と 8 つのアドレス レジスタ (a0..a7) があり、a7 IIRC はスタック ポインターとしても機能していました。
6510 (古き良き Commodore 64 に戻る) では、次のように書くかもしれません...
lda var1
adc var2
sta var1
ここでのレジスタは、ほとんどが命令内で暗示されています。上記のレジスタはすべて、A (アキュムレータ) レジスタを使用しています。
これらの例のばかげたエラーを許してください - 私は少なくとも 15 年間、かなりの量の (仮想ではなく) 「本物の」アセンブラを書いていません。ただし、原則はポイントです。
レジスタの使用法は、特定のコード フラグメントに固有です。レジスタが保持するのは、基本的に最後の命令がそこに残ったものです。コードの各ポイントで各レジスタの内容を追跡するのは、プログラマの責任です。
サブルーチンを呼び出す場合、呼び出し元または呼び出し先のいずれかが、競合がないことを確認する責任を負う必要があります。これは、通常、呼び出しの開始時にレジスタがスタックに保存され、最後に読み戻されることを意味します。割り込みでも同様の問題が発生します。レジスタを保存する責任者 (呼び出し元または呼び出し先) などは、通常、各サブルーチンのドキュメントの一部です。
コンパイラは通常、人間のプログラマよりもはるかに洗練された方法でレジスタの使用方法を決定しますが、同じ原則に基づいて動作します。レジスタから特定の変数へのマッピングは動的であり、見ているコードのフラグメントに応じて劇的に変化します。レジスタの保存と復元は、ほとんどの場合、標準的な規則に従って処理されますが、状況によっては、コンパイラが「カスタム呼び出し規則」を即興で作成する場合があります。
通常、関数内のローカル変数はスタック上にあると想定されます。これは、C の "auto" 変数に関する一般的な規則です。"auto" がデフォルトであるため、これらは通常のローカル変数です。例えば...
void myfunc ()
{
int i; // normal (auto) local variable
//...
nested_call ();
//...
}
上記のコードでは、「i」は主にレジスタに保持されている可能性があります。関数が進行するにつれて、あるレジスタから別のレジスタに移動したり、元に戻したりすることさえできます。ただし、「nested_call」が呼び出されると、そのレジスターの値はほぼ確実にスタック上にあります。これは、変数がスタック変数 (レジスターではない) であるか、またはレジスターの内容が保存されて、nested_call 自体の作業ストレージを許可するためです。 .
マルチスレッド アプリでは、通常のローカル変数は特定のスレッドに対してローカルです。各スレッドは独自のスタックを取得し、実行中は CPU レジスタを排他的に使用します。コンテキスト スイッチでは、これらのレジスタが保存されます。レジスタ内でもスタック上でも、ローカル変数はスレッド間で共有されません。
この基本的な状況は、2 つ以上のスレッドが同時にアクティブになる可能性がある場合でも、マルチコア アプリケーションで維持されます。各コアには独自のスタックと独自のレジスタがあります。
共有メモリに格納されたデータには、より注意が必要です。これには、グローバル変数、クラスと関数の両方の静的変数、およびヒープ割り当てオブジェクトが含まれます。例えば...
void myfunc ()
{
static int i; // static variable
//...
nested_call ();
//...
}
この場合、「i」の値は関数呼び出し間で保持されます。メイン メモリの静的領域は、この値を格納するために予約されています (そのため、「静的」という名前が付けられています)。原則として、"nested_call" の呼び出し中に "i" を保持するための特別なアクションは必要ありません。一見すると、任意のコア (または別の CPU) で実行されている任意のスレッドから変数にアクセスできます。
ただし、コンパイラはコードの速度とサイズを最適化するために引き続き懸命に取り組んでいます。メイン メモリへの読み取りと書き込みの繰り返しは、レジスタ アクセスよりもはるかに低速です。コンパイラーはほぼ確実に、上記の単純な読み取り-変更-書き込みパターンに従わないことを選択しますが、その代わりに比較的長期間にわたって値をレジスターに保持し、同じメモリーへの読み取りと書き込みの繰り返しを回避します。
これは、あるスレッドで行われた変更が、しばらくの間別のスレッドに表示されない可能性があることを意味します。2 つのスレッドが、上記の「i」の値について非常に異なる考えを持つことになる可能性があります。
これに対する魔法のハードウェア ソリューションはありません。たとえば、スレッド間でレジスタを同期するメカニズムはありません。CPU にとって、変数とレジスタは完全に別個のエンティティであり、同期する必要があることを認識していません。異なるスレッド内または異なるコアで実行されているレジスター間で同期が行われないことは確かです。特定の時点で、別のスレッドが同じ目的で同じレジスターを使用していると信じる理由はありません。
部分的な解決策は、変数に「揮発性」のフラグを立てることです...
void myfunc ()
{
volatile static int i;
//...
nested_call ();
//...
}
これにより、変数への読み取りと書き込みを最適化しないようにコンパイラーに指示します。プロセッサには、揮発性の概念がありません。このキーワードは、レジスターを使用してこれらのアクセスを回避するのではなく、割り当てによって指定されたとおりにメモリーへの読み取りと書き込みを即時に実行して、別のコードを生成するようにコンパイラーに指示します。
ただし、これはマルチスレッド同期ソリューションではありません。少なくとも、それ自体はそうではありません。適切なマルチスレッド ソリューションの 1 つは、ある種のロックを使用して、この「共有リソース」へのアクセスを管理することです。例えば...
void myfunc ()
{
static int i;
//...
acquire_lock_on_i ();
// do stuff with i
release_lock_on_i ();
//...
}
ここでは、すぐにわかるよりも多くのことが行われています。原則として、「i」の値を「release_lock_on_i」呼び出しの準備ができている変数に書き戻すのではなく、スタックに保存することができます。コンパイラに関する限り、これは不合理ではありません。とにかくスタックアクセスを行っているため(たとえば、リターンアドレスの保存)、スタックにレジスタを保存する方が「i」に書き戻すよりも効率的です。完全に別のメモリブロックにアクセスするよりもキャッシュフレンドリーです。
残念ながら、リリース ロック関数は、変数がまだメモリに書き戻されていないことを認識していないため、修正することはできません。結局のところ、その関数は単なるライブラリ呼び出し (実際のロック解放は、より深くネストされた呼び出しに隠されている可能性があります) であり、そのライブラリは、アプリケーションの何年も前にコンパイルされている可能性があります。呼び出し元がレジスタを使用する方法や、スタック。これが、私たちがスタックを使用する理由、および呼び出し規約を標準化する必要がある理由 (たとえば、誰がレジスタを保存するか) の大きな部分を占めています。リリース ロック機能は、呼び出し元にレジスタの「同期」を強制することはできません。
同様に、古いアプリを新しいライブラリに再リンクすることもできます。呼び出し元は、「release_lock_on_i」が何をどのように行うかを知りません。これは単なる関数呼び出しです。最初にレジスタをメモリに保存する必要があることを認識していません。
これを解決するために、「揮発性」を元に戻すことができます。
void myfunc ()
{
volatile static int i;
//...
acquire_lock_on_i ();
// do stuff with i
release_lock_on_i ();
//...
}
ロックがアクティブな間、通常のローカル変数を一時的に使用して、コンパイラーにその短い期間レジスターを使用する機会を与えることができます。ただし、原則として、ロックはできるだけ早く解放する必要があるため、それほど多くのコードを含める必要はありません。ただし、そうする場合は、ロックを解放する前に一時変数を「i」に書き戻します。「i」の揮発性により、それがメインメモリに書き戻されます。
原則として、これでは十分ではありません。メイン メモリへの書き込みは、メイン メモリへの書き込みを意味するものではありません。その間を走査するキャッシュの層があり、データはそれらの層のいずれかにしばらく留まる可能性があります。ここには「メモリバリア」の問題があり、私はこれについてあまり知りませんが、幸いなことに、この問題は上記のロックの取得や解放の呼び出しなどのスレッド同期呼び出しの責任です。
ただし、このメモリ バリアの問題によって「volatile」キーワードが不要になるわけではありません。
CPU レジスタは、CPU のシリコン上の小さなデータ ストレージ領域です。ほとんどのアーキテクチャでは、すべての操作が行われる主要な場所です (データはメモリから読み込まれ、操作され、プッシュされます)。
実行中のどのスレッドもレジスタを使用し、命令ポインターを所有します (これは、どの命令が次に来るかを示します)。OS が別のスレッドにスワップすると、レジスタや命令ポインターを含むすべての CPU 状態がどこかに保存され、次にスレッドが復活したときのためにスレッドの状態が効果的に凍結乾燥されます。
もちろん、これらすべてに関するドキュメントは他にもたくさんあります。レジスターに関するウィキペディア。 コンテキスト切り替えに関するウィキペディア。初心者向け。編集:またはSteve314の回答を読んでください。:)