Диапазон адресов стека потоков C ++ - PullRequest
0 голосов
/ 18 февраля 2019

Предоставляет ли стандарт C ++ гарантию о неперекрывающейся природе стеков потоков (как в случае запуска std::thread)?В частности, есть ли гарантия того, что потоки будут иметь свой собственный, эксклюзивный, выделенный диапазон в адресном пространстве процесса для стека потоков?Где это описано в стандарте?

Например,

std::uintptr_t foo() {
    auto integer = int{0};
    return std::bit_cast<std::uintptr_t>(&integer); 
    ... 
}

void bar(std::uint64_t id, std::atomic<std::uint64_t>& atomic) {
    while (atomic.load() != id) {}
    cout << foo() << endl;
    atomic.fetch_add(1);
}

int main() {
    auto atomic = std::atomic<std::uint64_t>{0};
    auto one = std::thread{[&]() { bar(0, atomic); }};
    auto two = std::thread{[&]() { bar(1, atomic); }};

    one.join();
    two.join();
}

Может ли это когда-либо печатать одно и то же значение дважды?Такое ощущение, что стандарт должен где-то предоставлять эту гарантию.Но не уверен ..

Ответы [ 2 ]

0 голосов
/ 18 февраля 2019

Стандарт C ++ даже не требует, чтобы вызовы функций были реализованы с использованием стека (или чтобы у потоков был стек в этом смысле).

В текущем черновике C ++ говорится об перекрывающихся объектах :

Два объекта с перекрывающимися временами жизни, которые не являются битовыми полями, могут иметь один и тот же адрес, если один вложен в другой, или если хотя бы один является подобъектом нулевого размера, и они имеют разныетипы;в противном случае они имеют разные адреса и занимают непересекающиеся байты памяти.

И в (ненормативной) сноске:

В соответствии с правилом «как будто»Реализация позволяет хранить два объекта по одному и тому же машинному адресу или не сохранять объект вообще, если программа не может наблюдать разницу ([ intro.execution ]).

Inв вашем примере, я не думаю, что потоки синхронизируются должным образом, как, вероятно, предполагалось, поэтому время жизни объектов integer не обязательно перекрывается, поэтому оба объекта могут быть помещены по одному адресу.

Если кодбыли исправлены для правильной синхронизации и foo были вручную встроены в bar, таким образом, что объект integer все еще существует, когда его адрес печатается, тогда должно быть два объекта, выделенных по разным адресам, потому что различие является наблюдаемым.

Тем не менее, ничего из этого не говорит о том, можно ли реализовать в C ++ стековые сопрограммы безпомощь компилятора.Реальные компиляторы делают предположения о среде выполнения, которые не отражены в стандарте C ++ и подразумеваются только стандартами ABI.Особенно актуальным для сопрограмм с переключением стека является тот факт, что адрес дескриптора потока и локальных переменных потока не изменяется во время выполнения функции (потому что они могут быть дорогостоящими для вычисления, и компилятор выдает код для кэширования их в регистрах или настек).

Вот что может произойти:

  1. Сопрограмма работает в потоке A и обращается к errno.

  2. Сопрограмма приостановлена ​​из нити А.

  3. Сопрограмма возобновляется в потоке B.

  4. Доступ к сопрограммам errno снова.

В этот момент поток B получит доступ к значению errno потока A, который вполне может делать с ним что-то совершенно другое.

Этой проблемы можно избежать, если сопрограммавозобновляется только в том же потоке, в котором он был приостановлен, что является весьма ограничительным и, вероятно, не совсем тем, что имеет в виду большинство авторов сопрограммных библиотек.Хуже всего то, что возобновление в неправильном потоке, вероятно, работает, в большинстве случаев, потому что некоторые широко используемые локальные переменные потока (такие как errno), которые не совсем локальны для потока, не сразу приводят к очевиднымглючные программы.

0 голосов
/ 18 февраля 2019

Для всех стандартных заданий реализации вызывают new __StackFrameFoo, когда foo() требуется кадр стека.Там, где заканчиваются те, кто знает.

Главное правило состоит в том, что разные объекты имеют разные адреса, и это включает в себя объекты, которые «живут в стеке».Но правило применяется только к двум объектам, которые существуют одновременно, и только в том случае, если сравнение выполняется при правильной синхронизации потоков.И, конечно же, сравнение адресов мешает оптимизатору, которому может потребоваться назначить адрес для объекта, который в противном случае мог бы быть оптимизирован.

...