IT企業とのインタビューで、次の質問に答えることが提案されました。
チャレンジ。 次のコードを考えます:
static int counter = 0;
void worker()
{
for(int i = 1; i <= 10; i ++)
カウンター++;
}
worker()
プロシージャは2つのスレッドから開始されます。
counter
変数には、両方のスレッドの最後にどのような値が含まれますか?
ちょっとした理論。 スレッドは、同じプロセス内の並列実行タスクです。 プロセスとスレッドの主な違いは、1つのプロセスのすべてのスレッドがそのプロセスの共通アドレス空間で動作することです。
実行
スレッド (
thread )とデータストリーム(
stream )を混同しないように、スレッドスレッドを呼び出しません。
回答番号1の
バリエーション。 counter
変数には20という数値が含まれ
ます。これは最も望ましい結果ですが、最も誤った回答でもあります。
問題は、このコードが同期解除から保護されていないことです。 各スレッドは互いに独立して動作し、一般データ(
counter
変数)が他の誰かによって変更される可能性があるとは考えていません。 したがって、より正しい答えはこれです。
これは安全でないコードであり、 counter
変数の値はundefinedになります。
オプション番号2。プロセッサ、OS、およびコンパイラの基本をまだ理解しているので、どの結果が得られるかをより正確に推測できます。
worker()
ルーチンは非常に単純です。 したがって、最適化の段階で、コンパイラはそのような構成でforループを展開できます。
カウンター+ = 10;
これでコードは非常に単純なため、2番目のスレッドが開始する前であっても、最初のスレッドは実行を終了する時間ができます。 したがって、20という数字が得られます。
そのような場合も可能です。 スレッドは、
counter
変数(ゼロ)の値をRAMから読み取り、プロセッサーのレジスターに入れます。 各スレッドは独自のプロセッサコンテキストで動作するため、各スレッドには独自のレジスタセットがあります。 したがって、コンテキスト内の最初のスレッドは、レジスタの値を0から10に増やしてから、結果をメモリに戻します。 そして、2番目のスレッドは同じことを行い、10個のスレッドを書き換えます。 その結果、10という数字が得られます。
10と20の値は実際には実際に最も可能性が高いですが、これはまだ完全な答えではありません。
オプション番号3。潜在的に実際の、しかし最大限に効果のない作業スキームを想像してみましょう。 したがって、サイクルの各反復は、3つのアトミックアクションを等しく繰り返します。つまり、
counter
値をプロセッサレジスタに読み込み、このレジスタをインクリメントし、レジスタから新しい値をメモリに書き戻します。 (ループは何の役割も果たさないため、ループ自体の実装は考慮しません。)さらに、スレッド実行コンテキストの切り替えは、必要なときに行われます。
このような条件下では、2〜20の結果が得られます。次に、表形式のタイムラインで、デュースを取得する可能性を検討します。 ステップ0は初期状態です。 「→
n 」、「
n +」、「
n →」の各操作は、スレッド番号
nのコンテキストでメモリからレジスタへの読み取り、レジスタのインクリメント、およびレジスタの値のメモリへの書き込みをそれぞれ表します。 行「ループ」は反復回数を示します。 知覚を改善するために、現在のステップの値の変更
が強調表示されます。
ステップ | 0 | | 1 | 2 | | 3 | 4 | 5 | ... | 27 | 28 | 29日 | | 30 | | 31 | 32 | | 33 | 34 | 35 | ... | 57 | 58 | 59 | | 60 |
---|
運営 | - | | →1 | 1+ | | →2 | 2+ | 2→ | | →2 | 2+ | 2→ | | 1→ | | →2 | 2+ | | →1 | 1+ | 1→ | | →1 | 1+ | 1→ | | 2→ |
---|
スレッド番号1 | 登録する | - | | 0 | 1 | | 1 | 1 | 1 | | 1 | 1 | 1 | | 1 | | 1 | 1 | | 1 | 2 | 2 | ... | 9 | 10 | 10 | | 10 |
---|
サイクル | 1 | | 1 | 1 | | 1 | 1 | 1 | | 1 | 1 | 1 | | 1 | | 1 | 1 | | 2 | 2 | 2 | 10 | 10 | 10 | | 10 |
---|
スレッド番号2 | 登録する | - | | - | - | | 0 | 1 | 1 | ... | 8 | 9 | 9 | | 9 | | 1 | 2 | | 2 | 2 | 2 | | 2 | 2 | 2 | | 2 |
---|
サイクル | 1 | | 1 | 1 | | 1 | 1 | 1 | 9 | 9 | 9 | | 9 | | 10 | 10 | | 10 | 10 | 10 | | 10 | 10 | 10 | | 10 |
---|
記憶 | 0 | | 0 | 0 | | 0 | 0 | 1 | | 8 | 8 | 9 | | 1 | | 1 | 1 | | 1 | 1 | 2 | | 9 | 9 | 10 | | 2 |
---|
(追加:読者の要求に応じ
て、ビットマップ付きのテーブルのバリアントがあります 。)
ステップ1〜2:最初のスレッドがレジスタ0に入り、1増加します。 3-29:制御は2番目のスレッドに転送されます。これもゼロを読み取り、9回の完全な反復を実行します。 30:最初のスレッドはループの最初の反復を完了し、カウントされたユニットをメモリに保存します(9個をラビングします)。 31–32:2番目のスレッドが最後の(10番目の)反復を開始し、ユニットを読み取り、無害にします。 33–59:最初のスレッドは残りの9回の反復を完全に実行し、作業を終了します。 60:2番目のスレッドは、カウントされた2つをメモリに書き込み、作業を終了します。
_________
NB:これは私のブログからのクロスポストです。 Habréの視聴者が私のブログよりも多いため、私はそれを再版しました(:そして、素材は興味深いと思います。