Я обнаружил противоречие в MSDN относительно начальных значений для локального хранилища потоков. Эта страница говорит:
Когда потоки созданы, система выделяет массив значений LPVOID для TLS, которые инициализируются в NULL.
Это приводит меня к мысли, что если я вызову TlsGetValue с допустимым индексом из потока, который никогда не вызывал TlsSetValue для того же индекса, то я должен получить нулевой указатель.
Эта страница, однако говорит:
Программист должен убедиться, что поток вызывает TlsSetValue перед вызовом TlsGetValue.
Это говорит о том, что вы не можете полагаться на значение, возвращаемое из TlsGetValue, если вы не уверены, что оно было явно инициализировано с помощью TlsSetValue.
Все же вторая страница одновременно усиливает инициализированное к нулю поведение, также говоря:
Данные, хранящиеся в слоте TLS, могут иметь значение 0, потому что они все еще имеют свое начальное значение, или потому что поток вызвал функцию TlsSetValue с 0.
Итак, у меня есть два утверждения о том, что данные инициализируются нулем (или 0), и одно из них о том, что я должен инициализировать их явно перед чтением значения. Экспериментально значения, похоже, автоматически инициализируются нулевыми указателями, но у меня нет возможности узнать, повезло ли мне просто так и будет ли это всегда так.
Я пытаюсь избежать использования DLL просто для размещения на DLL_THREAD_ATTACH. Я хотел бы сделать ленивое распределение по направлениям:
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 обнуляет слоты для выделенного индекса. Поэтому неважно, использовался ли ранее слот другой частью программы.
Документация слишком полезна, когда говорится, что начальное значение, выделяемое системой для TLS, равно нулю. Утверждение верно, но не полезно.
Причина в том, что приложения могут освобождать слоты TLS, вызывая TlsFree
Таким образом, когда вы выделяете слот, вы не гарантируете, что вы первый, кто получил этот слот. Следовательно, вы не знаете, является ли значение начальным 0, назначенным системой, или каким-либо другим значением нежелательной почты, назначенным предыдущим владельцем слота.
Рассматривать:
TlsAlloc
и получает назначенный слот 1. Слот 1 никогда не использовался, поэтому он содержит его начальное значение 0.TlsSetValue(1, someValue)
,TlsGetValue(1)
и это становится someValue
назад.TlsFree(1)
,TlsAlloc
и получает назначенный слот 1.TlsGetValue(1)
и получает someValue
назад, потому что это значение мусора, оставленное компонентом А.Следовательно, программист должен убедиться, что поток вызывает TlsSetValue
перед звонком TlsGetValue
, В противном случае ваш TlsGetValue
будет читать остатки мусора.
Вводящая в заблуждение документация гласит: «По умолчанию оставшийся мусор равен нулю», но это бесполезно, потому что вы не представляете, что произошло со слотом между моментом, когда система его инициализировала, и он в конечном итоге был передан вам.
Последующие действия Адриана побудили меня снова изучить ситуацию, и, действительно, ядро обнуляет слот, когда компонент А вызывает TlsFree(1)
, чтобы при вызове компонента B TlsGetValue(1)
оно получает ноль. Это предполагает, что нет ошибки в компоненте A, где он вызывает TlsSetValue(1)
после TlsFree(1)
,
Рассуждения Раймонда Чена имели смысл, поэтому я принял его вчера, но сегодня я обнаружил еще одну ключевую строку в документации 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.
}
Я провел эксперимент на Windows 7 с 32- и 64-разрядными тестовыми программами, скомпилированными с VS 2010.
Результаты подтверждают идею о том, что TlsAlloc повторно инициализирует значение в 0. Я полагаю, что TlsAlloc может делать что-то нехорошее, например обнуление значения только для текущего потока, но в документации явно сказано «slots» (множественное число), так что это кажется безопасным Предположим, что если ваш поток еще не использовал слот, значение будет 0.
Обновить: Дальнейшие эксперименты показывают, что это TlsFree, а не TlsAlloc, который повторно инициализирует слоты до 0. Таким образом, как отметил Рэймонд, все еще существует риск, что другой компонент с именем TlsSetValue после освобождая слот. Это будет ошибка в другом компоненте, но это повлияет на ваш код.
Я не вижу противоречия. Вторая страница просто говорит вам, что система инициализирует все слоты TLS для 0
но за пределами некоторых элементарных проверок границ невозможно узнать, содержит ли конкретный индекс TLS действительные данные (0
также может быть вполне допустимым значением, заданным пользователем!), и вы должны убедиться, что запрашиваемый вами индекс — это тот, который вам нужен, и что он содержит действительные данные.