У меня есть подозрение, что в определенной ситуации многопоточности C ++ может возникнуть состояние гонки, включающее вызовы виртуальных методов в реализации динамической диспетчеризации vtable (для которой указатель vtable хранится как скрытый элемент в объекте с виртуальными методами). Я хотел бы подтвердить, действительно ли это является проблемой, и я указываю библиотеку потоков Boost, чтобы мы могли принять некоторую систему отсчета.
Предположим, что объект "O" имеет элемент boost :: mutex, для которого весь его конструктор / деструктор и методы заблокированы по области (аналогично шаблону параллелизма Monitor). Поток «A» создает объект «O» в куче без внешней синхронизации (т. Е. БЕЗ общего мьютекса, включающего «новую» операцию, для которой он может синхронизироваться с другими потоками; однако, обратите внимание, что все еще существует «внутренний»). Монитор "мьютекс, блокирующий область его конструктора). Затем поток A передает указатель на экземпляр «O» (который он только что сконструировал) в другой поток «B» с помощью синхронизированного механизма - например, синхронизированной очереди читателей-записчиков (примечание: только указатель на объект передается, а не сам объект). После создания ни один из потоков «A» или любые другие потоки не выполняют никаких операций записи для экземпляра «O», созданного «A».
Поток "B" считывает значение указателя объекта "O" из синхронизированной очереди, после чего он немедленно покидает критическую секцию, защищающую очередь. Затем поток «B» выполняет вызов виртуального метода для объекта «O». Здесь я думаю, что проблема может возникнуть.
Теперь я понимаю, что вызовы виртуальных методов в [вполне вероятной] реализации динамической диспетчеризации [vtable] таковы, что вызывающий поток «B» должен разыменовать указатель на «O», чтобы получить указатель vtable, сохраненный как скрытый член. его объекта, и что это происходит ПЕРЕД введением тела метода (естественно, потому что тело метода для выполнения не безопасно и точно определено, пока не будет получен доступ к указателю vtable, сохраненному в самом объекте). Предполагая, что вышеупомянутые утверждения, возможно, верны для такой реализации, разве это не условие гонки?
Поскольку указатель vtable извлекается потоком «B» (путем разыменования указателя на объект «O», расположенный в куче) до выполнения каких-либо операций, гарантирующих видимость памяти (т. Е. Получения переменной-члена mutex в объекте » O "), тогда нет уверенности в том, что" B "будет воспринимать значение указателя vtable, которое" A "изначально записало в конструкции объекта" O ", верно? (т. е. вместо этого он может воспринимать значение мусора, приводящее к неопределенному поведению, верно?).
Если вышеприведенное является допустимой возможностью, не означает ли это, что выполнение виртуальных методов вызывает исключительно внутренне синхронизированные объекты, которые совместно используются потоками, является неопределенным поведением?
И - аналогично - поскольку стандарт не зависит от реализации vtable, как можно гарантировать, что указатель vtable безопасно виден другим потокам до виртуального вызова? Я полагаю, что можно внешне синхронизировать («внешне», как, например, «окружая общим блокированием mutex lock () / unlock ()») вызов конструктора, а затем, по крайней мере, начальный вызов виртуального метода в каждом из потоков, но это похоже на какое-то ужасно диссонирующее программирование.
Итак, если мои подозрения верны, то, возможно, более элегантным решением было бы использование встроенных, не виртуальных функций-членов, которые блокируют мьютекс члена, а затем перенаправляют на виртуальный вызов. Но - даже тогда - можем ли мы гарантировать, что конструктор инициализировал указатель vtable в границах lock () и unlock (), защищающих само тело конструктора?
Если бы кто-то мог помочь мне разобраться в этом и подтвердить / опровергнуть мои подозрения, я был бы очень признателен.
РЕДАКТИРОВАТЬ: код, демонстрирующий выше
class Interface
{
public:
virtual ~Interface() {}
virtual void dynamicCall() = 0;
};
class Monitor : public Interface
{
boost::mutex mutex;
public:
Monitor()
{
boost::unique_lock<boost::mutex> lock(mutex);
// initialize
}
virtual ~Monitor()
{
boost::unique_lock<boost::mutex> lock(mutex);
// destroy
}
virtual void dynamicCall()
{
boost::unique_lock<boost::mutex> lock(mutex);
// do w/e
}
};
// for simplicity, the numbers following each statement specify the order of execution, and these two functions are assumed
// void passMonitorToSharedQueue( Interface * monitor )
// Thread A passes the 'monitor' pointer value to a
// synchronized queue, pushes it on the queue, and then
// notifies Thread B that a new entry exists
// Interface * getMonitorFromSharedQueue()
// Thread B blocks until Thread A notifies Thread B
// that a new 'Interface *' can be retrieved,at which
// point it retrieves and returns it
void threadBFunc()
{
Interface * if = getMonitorFromSharedQueue(); // (1)
if->dynamicCall(); // (4) (ISSUE HERE?)
}
void threadAFunc()
{
Interface * monitor = new Monitor; // (2)
passMonitorToSharedQueue(monitor); // (3)
}
- в точке (4)
У меня сложилось впечатление, что значение указателя виртуальной таблицы, которое «поток А» записал в память, может быть невидимым для «потока В», так как я не вижу никаких оснований предполагать, что компилятор будет генерировать код таким образом, чтобы указатель виртуальной таблицы записывается в заблокированном блоке мьютекса конструктора.
Например, рассмотрим ситуацию с многоядерными системами, где каждое ядро имеет выделенный кэш. Согласно этой статье , кэши обычно агрессивно оптимизируются и - несмотря на принудительную когерентность кэша - не применяют строгий порядок когерентности кэша, если не задействованы примитивы синхронизации.
Возможно, я неправильно понимаю смысл статьи, но разве это не означает, что запись "A" vtable указателя на построенный объект (и нет никаких признаков того, что эта запись происходит в заблокированном мьютексе конструктора) блок) может не восприниматься "B" до того, как "B" прочитает указатель vtable? Если и A, и B выполняются на разных ядрах («A» на core0 и «B» на core1), механизм согласованности кэша может переупорядочить обновление значения указателя vtable в кэше core1 (обновление, которое сделает его непротиворечивым). со значением указателя vtable в кэше core0, который написал «A»), так что это происходит после чтения «B» ... если я правильно интерпретирую статью.