Нарушение прав доступа в многопоточном приложении, C ++ - PullRequest
3 голосов
/ 13 декабря 2010

Я не очень хорош в многопоточном программировании, поэтому я хотел бы попросить некоторую помощь / совет.

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

Пожалуйста, ознакомьтесь с примером кода ниже:

//DataLinkLayer.h 
class DataLinkLayer: public iDataLinkLayer {

public:
DataLinkLayer(void);
~DataLinkLayer(void);
};

Где iDataLinkLayer - это интерфейс (абстрактный класс без какой-либо реализации), содержащий чисто виртуальные функции и объявление ссылки (указателя) на происхождение объекта DataLinkLayer (dataLinkLayer).

// DataLinkLayer.cpp
#include "DataLinkLayer.h"

DataLinkLayer::DataLinkLayer(void) {

/* In reality task constructors takes bunch of other parameters  
 but they are not relevant (I believe) at this stage. */
dll_task_1* task1 = new dll_task_1(this); 
dll_task_2* task2 = new dll_task_2(this); 

/* Start multithreading */
task1->start(); // task1 extends thread class
task2->start(); // task2 also extends thread class
}

/* sample stub functions for testing */
void DataLinkLayer::from_task_1() {
printf("Test data Task 1");
}

void DataLinkLayer::from_task_2() {
printf("Test data Task 2");
}

Реализация задачи 1 ниже. Указатель интерфейса dataLinLayer (iDataLinkLayer) передается классу-центруктору, чтобы иметь возможность доступа к необходимым функциям из isntance dataLinkLayer.

//data_task_1.cpp
#include "iDataLinkLayer.h"  // interface to DataLinkLayer
#include "data_task_1.h"

dll_task_1::dll_task_1(iDataLinkLayer* pDataLinkLayer) {
this->dataLinkLayer = pDataLinkLayer; // dataLinkLayer declared in dll_task_1.h
}

// Run method - executes the thread
void dll_task_1::run() {
// program reaches this point and prints the stuff
this->datalinkLayer->from_task_1();
}
// more stuff following - not relevant to the problem
...

И задача 2 выглядит одинаково:

//data_task_2.cpp
#include "iDataLinkLayer.h"  // interface to DataLinkLayer
#include "data_task_2.h"

dll_task_2::dll_task_2(iDataLinkLayer* pDataLinkLayer){
this->dataLinkLayer = pDataLinkLayer; // dataLinkLayer declared in dll_task_2.h
}

// // Run method - executes the thread
void dll_task_2::run() {
// ERROR: 'Access violation reading location 0xcdcdcdd9' is signalled at this point
this->datalinkLayer->from_task_2();
}
// more stuff following - not relevant to the problem
...

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

Кажется, что-то идет не так, как надо в тот момент, когда dll_task_2 пытается вызвать функцию, используя указатель на DataLinkLayer. dll_task_2 имеет более низкий приоритет, поэтому он запускается впоследствии. Я не понимаю, почему я все еще не могу по крайней мере получить доступ к объекту ... Я могу использовать мьютекс для блокировки переменной, но я подумал, что основной причиной этого является защита переменной / объекта.

Я использую Microsoft Visual C ++ 2010 Express. Я не знаю много о многопоточности, поэтому, возможно, вы сможете предложить лучшее решение этой проблемы, а также объяснить причину проблемы.

Ответы [ 4 ]

4 голосов
/ 13 декабря 2010

Адрес нарушения доступа - очень маленькое положительное смещение от 0xcdcdcdcd

Википедия говорит:

CDCDCDCD Используется библиотекой времени выполнения отладки C ++ от Microsoft для маркировки неинициализированной кучи памяти

Вот соответствующая страница MSDN .

Соответствующее значение после free равно 0xdddddddd , поэтому скорее всего это будет неполная инициализация, а не использование после освобождения.

РЕДАКТИРОВАТЬ: Джеймс спросил, как оптимизация может испортить вызовы виртуальных функций. По сути, это потому, что в настоящее время стандартизированная модель памяти C ++ не дает никаких гарантий о многопоточности. Стандарт C ++ определяет, что виртуальные вызовы, сделанные из конструктора, будут использовать объявленный тип выполняемого в данный момент конструктора, а не конечный динамический тип объекта. Таким образом, это означает, что с точки зрения модели памяти с последовательным выполнением C ++ механизм виртуального вызова (практически говоря, указатель v-таблицы) должен быть настроен до того, как конструктор начнет работать (я думаю, что конкретная точка находится после построения базового подобъекта в ctor-initializer-list и до создания подобъекта члена).

Теперь две вещи могут сделать наблюдаемое поведение другим в многопоточном сценарии:

Во-первых, компилятор может выполнять любую оптимизацию, которая в модели последовательного выполнения C ++ действовала бы так, как если бы правила выполнялись. Например, если компилятор может доказать, что внутри конструктора не выполняются виртуальные вызовы, он может подождать и установить указатель v-таблицы в конце тела конструктора вместо начала. Если конструктор не выдает указатель this, поскольку вызывающая сторона конструктора также еще не получила свою копию указателя, то ни одна из функций, вызываемых конструктором, не может вызвать (виртуально или статически) обратный вызов строящийся объект. Но конструктор ДАЕТ указатель this.

Мы должны присмотреться. Если функция, которой дан этот указатель, видна компилятору (то есть включена в текущий модуль компиляции), компилятор может включить его поведение в анализ. Мы не получили эту функцию в этом вопросе (конструктор и функции-члены class task), но, вероятно, единственное, что происходит, это то, что указанный указатель хранится в подобъекте, который также недоступен извне конструктора .

«Грязь!», Вы кричите: «Я передал адрес этого task подобъекта в библиотечную CreateThread функцию, поэтому он достижим, и через него достижим главный объект». Ах, но вы не понимаете тайны "строгих правил наложения имен". Эта библиотечная функция не принимает параметр типа task *, не так ли? И будучи параметром, тип которого, возможно, intptr_t, но определенно ни task *, ни char *, компилятору разрешается предполагать, для целей оптимизации, как если бы, что он не указывает на объект task ( даже если это явно так). И если он не указывает на объект task и единственное место, в котором хранится наш указатель this, находится в подобъекте члена task, то его нельзя использовать для виртуальных вызовов this, поэтому компилятор может законно отложить настройку механизма виртуального вызова.

Но это еще не все. Даже если компилятор настраивает механизм виртуальных вызовов по расписанию, модель памяти ЦП гарантирует только то, что изменения видны текущему ядру ЦП. Записи могут стать видимыми для других ядер процессора в совершенно ином порядке. Теперь функция создания потока библиотеки должна вводить барьер памяти, который ограничивает переупорядочение записи ЦП, но тот факт, что ответ Коза о введении критического раздела (который, безусловно, включает в себя барьер памяти) меняет поведение, говорит о том, что, возможно, в оригинальный код.

И, переупорядочение записи ЦП может не только задержать указатель v-таблицы, но и сохранить этот указатель в подобъекте task.

Надеюсь, вам понравился этот экскурсионный тур по одному маленькому уголку пещеры «Многопоточное программирование - это сложно».

1 голос
/ 13 декабря 2010

printf небезопасен, афаик.Попробуйте окружить printf критическим разделом.

Для этого вы InitializeCriticalSection внутри класса iDataLinkLayer.Затем вокруг printfs вам понадобятся EnterCriticalSection и LeaveCriticalSection .Это предотвратит одновременный ввод обеих функций в printf.

Редактировать: Попробуйте изменить этот код:

dll_task_1* task1 = new task(this); 
dll_task_2* task2 = new task(this);

на

dll_task_1* task1 = new dll_task_1(this); 
dll_task_2* task2 = new dll_task_2(this);

Я предполагаю, что задача на самом делебазовый класс dll_task_1 и dll_task_2 ... так что, больше всего, я удивлен, что он компилируется ...

0 голосов
/ 14 декабря 2010

Я хотел прокомментировать создание DataLinkLayer.

Когда я вызываю конструктор DataLinkLayer из main:

int main () {
DataLinkLayer* dataLinkLayer = new DataLinkLayer();
while(true); // to keep the main thread running
}

Я, конечно, не разрушаю объект, это первое. Теперь внутри cosntructor DataLinkLayer я инициализирую многие (не только эти две задачи) другие объекты isntances и передаю большинству из них указатель dataLinkLayer (используя this). Насколько мне известно, это законно. Поместите это далее - это компилируется и работает как ожидалось.

Что меня заинтересовало, так это общая идея дизайна, которой я следую (если есть :)).

DataLinkLayer - это родительский класс, доступ к которому осуществляется несколькими задачами, которые пытаются изменить его параметры или выполнить некоторую другую обработку. Поскольку я хочу, чтобы все оставалось как можно отделенным, я предоставляю только интерфейсы для методов доступа и инкапсулирую данные, чтобы у меня не было глобальных переменных, функций-друзей и т. Д.

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

Не стесняйтесь обсуждать это, пожалуйста, и Мерси за ваши щедрые комментарии!

UPD:

Говоря о передаче указателя интерфейса iDataLinkLayer на задачи - это хороший способ сделать это? В Java было бы довольно обычным делом реализовать сдерживание или так называемый шаблон стратегии для разделения вещей и вещей. Однако я не уверен на 100%, является ли это хорошим решением в c ++ ... Какие-нибудь предложения / комнеты на нем?

0 голосов
/ 13 декабря 2010

Я думаю, что не всегда безопасно использовать 'this' (т.е. вызывать функцию-член) до конца конструктора. Возможно, эта задача вызывает функцию-член DataLinkLayer до конца конструктора DataLinkLayer. Особенно, если эта функция-член является виртуальной: http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.7

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