6

スレッド ローカル ストレージの初期値に関して、MSDN で矛盾を見つけました。 このページには次のように書かれています。

スレッドが作成されると、システムは TLS に LPVOID 値の配列を割り当てます。これらは NULL に初期化されます。

これにより、同じインデックスに対して TlsSetValue を呼び出したことのないスレッドから有効なインデックスを使用して TlsGetValue を呼び出すと、null ポインターを取得する必要があると考えるようになります。

ただし、このページには次のように記載されています。

スレッドが TlsGetValue を呼び出す前に TlsSetValue を呼び出すことを確認するのは、プログラマの責任です。

これは、TlsSetValue で明示的に初期化されていることが確実でない限り、TlsGetValue から返された値に依存できないことを示唆しています。

しかし、2 番目のページでは、次のようにも言って、null への初期化の動作を同時に強化しています。

TLS スロットに格納されたデータは、まだ初期値を持っているか、スレッドが TlsSetValue 関数を 0 で呼び出したため、値が 0 になることがあります。

したがって、データが null (または 0) に初期化されるという 2 つのステートメントと、値を読み取る前に明示的に初期化する必要があるという 1 つのステートメントがあります。実験的には、値は自動的に null ポインターに初期化されるように見えますが、運が良かっただけなのか、常にそうなのかどうかを知る方法はありません。

DLL_THREAD_ATTACH に割り当てるためだけに DLL を使用しないようにしています。次の行に沿って遅延割り当てを行いたいです。

LPVOID pMyData = ::TlsGetValue(g_index);
if (pMyData == nullptr) {
  pMyData = /* some allocation and initialization*/;
  // bail out if allocation or initialization failed
  ::TlsSetValue(g_index, pMyData);
}
DoSomethingWith(pMyData);

これは信頼できる安全なパターンですか?または、スロットを読み取ろうとする前に、各スレッドのスロットを明示的に初期化する必要がありますか?

更新:ドキュメントには、TlsAlloc が割り当てられた index のスロットをゼロにすることも記載されています。そのため、スロットが以前にプログラムの別の部分で使用されていたかどうかは関係ないようです。

4

3 に答える 3

10

システムが TLS に割り当てる初期値がゼロであると記載されている場合、ドキュメントは非常に役に立ちます。ステートメントは正しいですが、役に立ちません。

その理由は、アプリケーションが を呼び出して TLS スロットを解放できるためです。そのためTlsFree、スロットを割り当てたときに、そのスロットが最初に割り当てられるという保証はありません。したがって、値がシステムによって割り当てられた最初の 0 なのか、それともスロットの前の所有者によって割り当てられたその他のジャンク値なのかはわかりません。

検討:

  • コンポーネント A が呼び出さTlsAllocれ、スロット 1 が割り当てられます。スロット 1 は使用されていないため、初期値 0 が含まれています。
  • コンポーネント A が を呼び出しますTlsSetValue(1, someValue)
  • コンポーネント A が呼び出しTlsGetValue(1)、それがsomeValue返されます。
  • コンポーネント A が終了し、 を呼び出しますTlsFree(1)
  • コンポーネント B が呼び出しTlsAlloc、割り当てられたスロット 1 を取得します。
  • コンポーネント B が呼び出さTlsGetValue(1)れてsomeValue返されるのは、それがコンポーネント A によって残されたガベージ値であるためです。

したがって、プログラマは、スレッドが を呼び出すTlsSetValue前に確実に呼び出す必要がありますTlsGetValue。そうしないと、TlsGetValue残りのゴミが読み取られます。

誤解を招くドキュメントには、「残りのガベージのデフォルト値はゼロです」と書かれていますが、システムがスロットを初期化してから最終的に与えられるまでの間にスロットに何が起こったのかがわからないため、これは役に立ちません。

Adrian のフォローアップにより、状況をもう一度調査するようになりました。実際、コンポーネント A が を呼び出すと、カーネルはスロットをゼロに設定し、TlsFree(1)コンポーネント B を呼び出すTlsGetValue(1)とスロットがゼロになるようにします。これは、コンポーネント A にバグがないことを前提としていTlsSetValue(1)ますTlsFree(1)

于 2013-02-07T18:56:14.750 に答える
2

Raymond Chen の推論は完全に理にかなっていたので、昨日彼の答えを受け入れましたが、今日、MSDN ドキュメントでTlsAllocに関する別の重要な行を見つけました。

関数が成功した場合、戻り値は TLS インデックスです。インデックスのスロットはゼロに初期化されます。

TlsAlloc が実際にスロットをゼロに初期化する場合、そのスロットの前のユーザーが残したガベージ値について心配する必要はありません。TlsAlloc が実際にこのように動作することを確認するために、次の実験を実行しました。

void TlsExperiment() {
  DWORD index1 = ::TlsAlloc();
  assert(index1 != TLS_OUT_OF_INDEXES);
  LPVOID value1 = ::TlsGetValue(index1);
  assert(value1 == 0);  // Nobody else has used this slot yet.
  value1 = reinterpret_cast<LPVOID>(0x1234ABCD);
  ::TlsSetValue(index1, value1);
  assert(value1 == ::TlsGetValue(index1));
  ::TlsFree(index1);

  DWORD index2 = ::TlsAlloc();
  // There's nothing that requires TlsAlloc to give us back the recently freed slot,
  // but it just so happens that it does, which is convenient for our experiment.
  assert(index2 == index1); // If this assertion fails, the experiment is invalid.

  LPVOID value2 = ::TlsGetValue(index2);
  assert(value2 == 0);  // If the TlsAlloc documentation is right, value2 == 0.
                        // If it's wrong, you'd expect value2 == 0x1234ABCD.
}

VS 2010 でコンパイルされた 32 ビットと 64 ビットの両方のテスト プログラムを使用して、Windows 7 で実験を実行しました。

結果は、TlsAlloc が値を 0 に再初期化するという考えを支持しています。TlsAlloc は、現在のスレッドだけの値をゼロにするなどの不自由なことをしている可能性があると思いますが、ドキュメントには明示的に「スロット」(複数) と書かれているので、安全に思えます。スレッドがまだスロットを使用していない場合、値は 0 になると仮定します。

更新:さらなる実験では、スロットを 0 に再初期化するのは TlsAlloc ではなく TlsFree であることが示唆されています。したがって、Raymond が指摘したように、スロットを解放した後に別のコンポーネントが TlsSetValue を呼び出すリスクがまだあります。これは他のコンポーネントのバグですが、コードに影響します。

于 2013-02-08T17:37:34.807 に答える
1

矛盾は見当たりません。2番目のページは、システムがすべてのTLSスロットを初期化する0が、いくつかの基本的な境界チェックを超えて、特定のTLSインデックスに有効なデータが含まれているかどうかを知る方法がないことを示しています(0完全に有効なユーザー設定値である可能性もあります) !)そして、要求するインデックスが必要なインデックスであり、有効なデータが含まれていることを確認するのはあなた次第です。

于 2013-02-07T17:01:47.690 に答える