Являются ли конструкторы потоками безопасными в С++ и/или С++ 11?
Из этого вопроса и связанных с этим вопросом:
Если я построю объект в одном потоке, а затем передаю ссылку/указатель на него в другой поток, не будет ли он потоком небезопасным для этого другого потока для доступа к объекту без явных блокировок/барьеров памяти?
// thread 1
Obj obj;
anyLeagalTransferDevice.Send(&obj);
while(1); // never let obj go out of scope
// thread 2
anyLeagalTransferDevice.Get()->SomeFn();
Альтернативно: существует ли какой-либо законный способ передачи данных между потоками, которые не обеспечивают упорядочение памяти в отношении всего остального, затронутого нитью? С аппаратной точки зрения я не вижу причин, по которым это невозможно.
Прояснить; вопрос в том, что касается согласованности кэша, упорядоченности памяти и много чего. Может ли Thread 2 получить и использовать указатель перед представлением памяти 2-го уровня, включая записи, связанные с построением obj
? Пропустить цитату Alexandrescu (?) "Мог ли разработчик вредоносного процессора и писатель компилятора построить стандартную систему соответствия, которая сделает этот разрыв?"
Ответы
Ответ 1
Обоснование безопасности потоков может быть затруднено, и я не являюсь экспертом в модели памяти С++ 11. К счастью, ваш пример очень прост. Я переписываю пример, потому что конструктор не имеет значения.
Упрощенный пример
Вопрос: Правилен ли следующий код? Или может привести к выполнению undefined поведения?
// Legal transfer of pointer to int without data race.
// The receive function blocks until send is called.
void send(int*);
int* receive();
// --- thread A ---
/* A1 */ int* pointer = receive();
/* A2 */ int answer = *pointer;
// --- thread B ---
int answer;
/* B1 */ answer = 42;
/* B2 */ send(&answer);
// wait forever
Ответ: Может существовать гонка данных в ячейке памяти answer
, и, следовательно, выполнение приводит к поведению undefined. Подробнее см. Ниже.
Реализация передачи данных
Конечно, ответ зависит от возможных и правовых реализаций функций send
и receive
. Я использую следующую реализацию без использования данных. Обратите внимание, что используется только одна атомная переменная, а во всех операциях памяти используется std::memory_order_relaxed
. В основном это означает, что эти функции не ограничивают переопределение памяти.
std::atomic<int*> transfer{nullptr};
void send(int* pointer) {
transfer.store(pointer, std::memory_order_relaxed);
}
int* receive() {
while (transfer.load(std::memory_order_relaxed) == nullptr) { }
return transfer.load(std::memory_order_relaxed);
}
Порядок операций с памятью
В многоядерных системах поток может видеть изменения памяти в другом порядке, как то, что видят другие потоки. Кроме того, как компиляторы, так и процессоры могут изменить порядок операций памяти в одном потоке для эффективности - и они делают это все время. Атомные операции с std::memory_order_relaxed
не участвуют ни в какой синхронизации и не налагают никакого упорядочения.
В приведенном выше примере компилятору разрешено изменять порядок операций потока B и выполнять B2 перед B1, поскольку переупорядочение не влияет на сам поток.
// --- valid execution of operations in thread B ---
int answer;
/* B2 */ send(&answer);
/* B1 */ answer = 42;
// wait forever
Гонка данных
С++ 11 определяет гонку данных следующим образом (N3290 С++ 11 Draft): "Выполнение программы содержит гонку данных, если она содержит два конфликтующих действия в разных потоках, по крайней мере один из которых не является атомарным, и это не происходит до другого. Любая такая гонка данных приводит к поведению undefined". И этот термин происходит раньше, чем ранее в том же документе.
В приведенном выше примере B1 и A2 являются конфликтующими и неатомными операциями, и они не происходят до другого. Это очевидно, потому что я показал в предыдущем разделе, что оба могут произойти одновременно.
Это единственное, что имеет значение в С++ 11. Напротив, модель памяти Java также пытается определить поведение, если есть расы данных, и потребовалось почти десять лет, чтобы придумать разумную спецификацию. С++ 11 не допустил ошибку.
Дополнительная информация
Я немного удивлен, что эти основы не очень хорошо известны. Конечным источником информации является раздел многопоточных исполнений и расчётов данных в стандарте С++ 11. Однако спецификацию трудно понять.
Хорошей отправной точкой являются переговоры Ханса Бёма - например, доступны в виде онлайн-видео:
Также есть много других хороших ресурсов, о которых я упоминал в другом месте, например:
Ответ 2
Параллельный доступ к тем же данным отсутствует, поэтому нет проблем:
- Тема 1 запускает выполнение
Obj::Obj()
.
- Thread 1 завершает выполнение
Obj::Obj()
.
- В потоке 1 передается ссылка на память, занятую
obj
, на поток 2.
- Тема 1 никогда не делает ничего с этой памятью (вскоре после этого она попадает в бесконечный цикл).
- Thread 2 выбирает ссылку на память, занятую
obj
.
- Thread 2 предположительно делает с ней что-то, невозмутимое нитью 1, которая все еще бесконечно зацикливается.
Единственная потенциальная проблема заключается в том, что Send
не действует как барьер памяти, но тогда это не будет действительно "юридическим устройством передачи".
Ответ 3
Как указывали другие, единственный способ, по которому конструктор не является потокобезопасным, - это что-то каким-то образом получает указатель или ссылку на него до завершения конструктора, и единственный способ, который может произойти, - это сам конструктор имеет код, который регистрирует указатель this
для некоторого типа контейнера, который совместно используется потоками.
Теперь в вашем конкретном примере Бранко Димитриевич дал хорошее полное объяснение, как ваше дело в порядке. Но в общем случае я бы сказал, что не использовал что-либо до тех пор, пока конструктор не будет закончен, хотя я не думаю, что есть что-то особенное, чего не происходит, пока конструктор не будет закончен. К тому времени, когда он входит в конструктор (последний) в цепочке наследования, объект в значительной степени полностью "хорош, чтобы идти" со всеми инициализированными переменными-членами и т.д. Так что не хуже, чем любая другая критическая секция, а другой поток сначала нужно знать об этом, и единственный способ, который случается, заключается в том, что вы как-то делитесь this
в самом конструкторе. Так что делайте это только как "последнее", если вы находитесь.
Ответ 4
Это безопасно (только), если вы написали оба потока, и знаете, что первый поток не обращается к нему, пока второй поток. Например, если построение потока никогда не обращается к нему после передачи ссылки/указателя, вы бы в порядке. В противном случае это небезопасно. Это можно изменить, создав все методы, которые обеспечивают доступ к памяти данных (чтение или запись).
Ответ 5
Прочтите этот вопрос до сих пор... Все еще опубликуйте мои комментарии:
Статическая локальная переменная
Существует надежный способ построения объектов, когда вы находитесь в многопоточной среде, использующей статическую локальную переменную (статическая локальная переменная -CppCoreGuidelines),
Из приведенной выше справки: "Это одно из наиболее эффективных решений проблем, связанных с порядком инициализации. В многопоточной среде инициализация статического объекта не приводит к состоянию гонки (если вы небрежно обращаетесь к общему объекту изнутри своего конструктора).
Также обратите внимание на ссылку, если уничтожение X включает операцию, которая должна быть синхронизирована, вы можете создать объект в куче и синхронизировать, когда вызывать деструктор.
Ниже приведен пример, который я написал, чтобы показать Construct On First Use Idiom, о чем в основном говорится в этой статье.
#include <iostream>
#include <thread>
#include <vector>
class ThreadConstruct
{
public:
ThreadConstruct(int a, float b) : _a{a}, _b{b}
{
std::cout << "ThreadConstruct construct start" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "ThreadConstruct construct end" << std::endl;
}
void get()
{
std::cout << _a << " " << _b << std::endl;
}
private:
int _a;
float _b;
};
struct Factory
{
template<class T, typename ...ARGS>
static T& get(ARGS... args)
{
//thread safe object instantiation
static T instance(std::forward<ARGS>(args)...);
return instance;
}
};
//thread pool
class Threads
{
public:
Threads()
{
for (size_t num_threads = 0; num_threads < 5; ++num_threads) {
thread_pool.emplace_back(&Threads::run, this);
}
}
void run()
{
//thread safe constructor call
ThreadConstruct& thread_construct = Factory::get<ThreadConstruct>(5, 10.1);
thread_construct.get();
}
~Threads()
{
for(auto& x : thread_pool) {
if(x.joinable()) {
x.join();
}
}
}
private:
std::vector<std::thread> thread_pool;
};
int main()
{
Threads thread;
return 0;
}
Вывод:
ThreadConstruct construct start
ThreadConstruct construct end
5 10.1
5 10.1
5 10.1
5 10.1
5 10.1