Переменные `thread_local` и сопрограммы

До сопрограмм мы использовали обратные вызовы для запуска асинхронных операций. Обратные вызовы были обычными функциями и могли иметь thread_local переменные.

Давайте посмотрим на этот пример:

void StartTcpConnection(void) 
{
    using namespace std;
    thread_local int my_thread_local = 1;
    cout << "my_thread_local = " << my_thread_local << endl;
    auto tcp_connection = tcp_connect("127.0.0.1", 8080);
    tcp_connection.async_wait(TcpConnected);
}

void TcpConnected(void)
{
    using namespace std;
    thread_local int my_thread_local = 2;
    cout << "my_thread_local = " << my_thread_local << endl;
}

Как видно из кода, у меня есть некоторая (здесь не задокументированная) tcp_connect функция, которая подключается к конечной точке TCP и возвращает tcp_connection объект. Этот объект может дождаться, пока TCP-соединение действительно произойдет, и вызвать функцию TcpConnected. Поскольку мы не знаем конкретной реализации tcp_connect и tcp_connection, мы не знаем, будет ли он вызывать TcpConnected в том же или в другом потоке, возможны обе реализации. Но мы точно знаем, что my_thread_local отличается для 2 разных функций, потому что каждая функция имеет свою область видимости.

Если нам нужно, чтобы эта переменная была одинаковой (как только поток будет таким же), мы можем создать третью функцию, которая будет возвращать ссылку на переменную thread_local:

int& GetThreadLocalInt(void)
{
    thread_local int my_variable = 1;
    return my_variable;
}

Итак, у нас есть полный контроль и предсказуемость: мы точно знаем, что переменные будут разными, если TcpConnected и StartTcpConnection будут выполняться в разных потоках, и мы знаем, что они могут быть разными или одинаковыми в зависимости от нашего выбора, когда эти функции будут выполняться в разных потоках. та же нить.

Теперь давайте посмотрим версию той же операции в сопрограмме:

void Tcp(void)
{
    thread_local int my_thread_local = 1;
    auto tcp_connection = co_await tcp_connect("127.0.0.1", 8080);
    cout << "my_thread_local = " << my_thread_local << endl;
}

Эта ситуация для меня немного сомнительна. Мне все еще нужно локальное хранилище потоков, это важная языковая функция, от которой я не хочу отказываться. Однако здесь у нас есть 2 случая:

  1. Нить перед co_await такая же, как и после co_await. Что будет с my_thread_local? Будет ли это одна и та же переменная до и после co_await, особенно если мы будем использовать функцию GetThreadLocalInt для получения ссылки вместо значения?
  2. Тема меняется после co_await. Будет ли среда выполнения C++ повторно инициализировать my_thread_local значением из нового потока, или делать копию значения предыдущего потока, или может использовать ссылку на те же данные? И аналогичный вопрос для функции GetThreadLocalInt, она возвращает ссылку на объект thread_local, но само хранилище ссылок auto, будет ли сопрограмма повторно инициализировать его в новый поток, или мы получим (опасно!!!) состояние гонки, потому что поток 2 странно получит ссылаться на локальные данные потока 1 и потенциально использовать их параллельно?

Даже если легко отлаживать и тестировать то, что произойдет на любом конкретном компиляторе, важный вопрос заключается в том, говорит ли стандарт нам что-то об этом, в противном случае, даже если мы протестируем его на VC++ или gcc, увидим, как он ведет себя как-то на этих двух популярных компиляторы, код может потерять переносимость и компилироваться по-другому на некоторых экзотических компиляторах.

Локальная переменная потока, как сказано, - локальная переменная потока. Если после co_await сопрограмму откроет другой поток, он получит доступ к своему экземпляру локальной переменной потока.

ALX23z 14.02.2023 15:01

Если вы сделаете локальную ссылку на локальную переменную потока, то новый поток будет получать доступ через ссылку на локальную переменную потока старого потока, что может быть небезопасно по разным причинам.

ALX23z 14.02.2023 15:03

@ ALX23z Вы уверены, что каждый раз, когда мы обращаемся к переменной thread_local в C++, компилятор выдает код, который вызывает API уровня ОС для доступа к локальному хранилищу потока вместо того, чтобы делать это один раз и использовать ранее полученный указатель для локальных данных потока? т. е. этот код thread_local o = new object(); o.method1();o.method2();o.method3() выдаст код, который вызывает TlsGetValue 4 раза? Это гарантия стандарта? Или может произойти какая-то оптимизация, незначительная для стандартной функции, но способная изменить поведение сопрограммы?

Vitalii 14.02.2023 15:14

@Vitalii: Если бы это изменило поведение, эта реализация была бы ошибочной. Компилятор может видеть, что вы делаете co_await, поэтому он знает, что после этого ему нужно снова получить доступ к памяти, а не использовать кешированную копию. Это ничем не отличается от доступа к глобальному объекту и вызова функции, определения которой компилятор не видит; компилятор должен предположить, что глобальная переменная была изменена вызовом функции, поэтому более поздние обращения к функции должны быть реальными выборками памяти.

Nicol Bolas 14.02.2023 15:32

Существует ошибка MSVC, которая указывает, что GCC и Clang компилируют это правильно. В этой ситуации спецификация не позволяет компиляторам кэшировать локальные переменные потока. (без ссылок). Таким образом, кажется, что thread_local требуется привязать к текущему потоку выполнения, но MSVC в настоящее время не обрабатывает этот случай правильно.

dewaffled 14.02.2023 15:45

@Vitalii стандарт гарантирует, что доступ к thread_local действительно обращается к локальной переменной. Как это достигается - детали реализации. Как отмечает dewaffled, в некоторых реализациях в настоящее время есть ошибки... что не лишено смысла, поскольку сопрограммы довольно новы.

ALX23z 14.02.2023 15:51

@Vitalii: Ответы должны быть в разделе ответов. Это нормально, чтобы ответить на свой вопрос.

Nicol Bolas 15.02.2023 17:12
Конечные и Readonly классы в PHP
Конечные и Readonly классы в PHP
В прошлом, когда вы не хотели, чтобы другие классы расширяли определенный класс, вы могли пометить его как final.
От React к React Native: Руководство для начинающих по разработке мобильных приложений с использованием React
От React к React Native: Руководство для начинающих по разработке мобильных приложений с использованием React
Если вы уже умеете работать с React, создание мобильных приложений для iOS и Android - это новое приключение, в котором вы сможете применить свои...
БЭМ: Конвенция об именовании CSS
БЭМ: Конвенция об именовании CSS
Я часто вижу беспорядочный код CSS, особенно если проект большой. Кроме того, я совершал эту ошибку в профессиональных или личных проектах и...
Революционная веб-разработка ServiceNow
Революционная веб-разработка ServiceNow
В быстро развивающемся мире веб-разработки ServiceNow для достижения успеха крайне важно оставаться на вершине последних тенденций и технологий. По...
Как добавить SEO(Search Engine Optimization) в наше веб-приложение и как это работает?
Как добавить SEO(Search Engine Optimization) в наше веб-приложение и как это работает?
Заголовок веб-страницы играет наиболее важную роль в SEO, он помогает поисковой системе понять, о чем ваш сайт.
Конфигурация Jest в angular
Конфигурация Jest в angular
В этой статье я рассказываю обо всех необходимых шагах, которые нужно выполнить при настройке jest в angular.
4
7
94
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Для глобальных thread_local переменных поведение сопрограмм должно быть таким, как ожидалось (и MSVC, похоже, имеет ошибку с этим). Но для локальных переменных thread_local в сопрограммах, похоже, есть дыра в спецификации. Действительно, я не уверен, что формулировка имеет смысл даже без сопрограмм.

[stmt.dcl]/3 говорит:

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

Проблема в том, что, хотя по правилам C++ существует только одна переменная thread_local, существует несколько объектов, представленных этой одной переменной. И эти объекты должны быть инициализированы. Итак... как это происходит?

Единственная разумная интерпретация этого состоит в том, что объект для конкретного потока инициализируется в первый раз, когда поток управления проходит через объявление в этом потоке. И в этом проблема.

Если использовать co_await поток, который функция выполняет при изменениях, то как вы могли бы пройти через объявление в новом потоке? Это означает, что thread_local для этого потока должен быть инициализирован нулем.

В конечном счете, я бы сказал, что вы никогда не должны использовать thread_local в функции сопрограммы. Просто непонятно, какое значение должно иметь thread_local. И единственное логическое значение для нового thread_local — это значение из предыдущего треда. Но это не то, как thread_local должен работать. В целом идея кажется бессмысленной по своей сути (и я бы сказал, что стандарт должен был явно запрещать объявление thread_locals в сопрограмме, точно так же, как они сделали для использования co_await в инициализаторе thread_local).

Просто используйте переменную thread_local с областью имен. Помимо вышеупомянутой ошибки MSVC, это должно работать и иметь смысл.

Другие вопросы по теме