windows 多线程应用程序中的访问冲突,C++
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/4430388/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Access violation in a multithreaded application, C++
提问by James
I am not very good in multithreading programming so I would like to ask for some help/advice.
我不太擅长多线程编程,所以我想寻求一些帮助/建议。
In my application I have two threads trying to access a shared object. One can think about two tasks trying to call functions from within another object. For clarity I will show some parts of the program which may not be very relevant but hopefully can help to get my problem better.
在我的应用程序中,我有两个线程试图访问一个共享对象。可以考虑尝试从另一个对象中调用函数的两个任务。为清楚起见,我将展示程序的某些部分,这些部分可能不是很相关,但希望可以帮助解决我的问题。
Please take a look at the sample code below:
请看下面的示例代码:
//DataLinkLayer.h
class DataLinkLayer: public iDataLinkLayer {
public:
DataLinkLayer(void);
~DataLinkLayer(void);
};
Where iDataLinkLayer is an interface (abstract class without any implementation) containing pure virtual functions and a reference (pointer) declaration to the isntance of DataLinkLayer object (dataLinkLayer).
其中 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");
}
Implementation of task 1 is below. The dataLinLayer interface (iDataLinkLayer) pointer is passed to the class cosntructor in order to be able to access necessary functions from within the dataLinkLayer isntance.
任务 1 的实现如下。dataLinLayer 接口 (iDataLinkLayer) 指针被传递给类构造函数,以便能够从 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
...
And task 2 looks simialrly:
任务 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
...
So as I understand correctly I access the shared pointer from two different threads (tasks) and it is not allowed. Frankly I thought that I will be able to access the object nevertheless however the results might be unexpected.
因此,据我所知,我从两个不同的线程(任务)访问共享指针,这是不允许的。坦率地说,我认为我仍然可以访问该对象,但结果可能出乎意料。
It seems that something goes terribly wrong at the point when dll_task_2 tries to call the function using pointer to the DataLinkLayer. dll_task_2 has lower priority hence it is started afterwards. I don't understand why i still cannot at least access the object... I can use the mutex to lock the variable but I thought that the primary reason for this is to protect the variable/object.
当 dll_task_2 尝试使用指向 DataLinkLayer 的指针调用该函数时,似乎出现了严重错误。dll_task_2 的优先级较低,因此它在之后启动。我不明白为什么我至少仍然无法访问对象......我可以使用互斥锁来锁定变量,但我认为这样做的主要原因是为了保护变量/对象。
I am using Microsoft Visual C++ 2010 Express. I don't know much about multithreading so maybe you can suggest a better solution to this problem as well as explain the reason of the problem.
我正在使用 Microsoft Visual C++ 2010 Express。我对多线程了解不多,所以也许您可以提出一个更好的解决方案来解决这个问题,并解释问题的原因。
回答by Ben Voigt
The address of the access violation is a very small positive offset from 0xcdcdcdcd
访问冲突的地址是一个非常小的正偏移量 0xcdcdcdcd
Wikipediasays:
维基百科说:
CDCDCDCD Used by Microsoft's C++ debugging runtime library to mark uninitialised heap memory
CDCDCDCD 被微软的 C++ 调试运行库用来标记未初始化的堆内存
Here is the relevant MSDN page.
The corresponding value after free is 0xdddddddd, so it's likely to be incomplete initialization rather than use-after-free.
free 后对应的值为 0xdddddddd,所以很可能是初始化不完整,而不是use-after-free。
EDIT: James asked how optimization could mess up virtual function calls. Basically, it's because the currently standardized C++ memory model makes no guarantees about threading. The C++ standard defines that virtual calls made from within a constructor will use the declaring type of the constructor currently being run, not the final dynamic type of the object. So this means that, from the perspective of the C++ sequential execution memory model, the virtual call mechanism (practically speaking, a v-table pointer) must be set up before the constructor starts running (I believe the specific point is after base subobject construction in the ctor-initializer-listand before member subobject construction).
编辑:James 询问优化如何弄乱虚函数调用。基本上,这是因为当前标准化的 C++ 内存模型不保证线程。C++ 标准定义在构造函数中进行的虚拟调用将使用当前正在运行的构造函数的声明类型,而不是对象的最终动态类型。所以这意味着,从C++顺序执行内存模型的角度来看,必须在构造函数开始运行之前设置虚调用机制(实际上就是一个v-table指针)(我相信具体点是在基子对象构造之后在ctor-initializer-list和成员子对象构造之前)。
Now, two things can happen to make the observable behavior different in a threaded scenario:
现在,在线程场景中,可能发生两件事使可观察行为不同:
First, the compiler is free to perform any optimization that would, in the C++ sequential execution model, act as-if the rules were being followed. For example, if the compiler can prove that no virtual calls are made inside the constructor, it could wait and set the v-table pointer at the end of the constructor body instead of the beginning. If the constructor doesn't give out the this
pointer, since the caller of the constructor also hasn't received its copy of the pointer yet, then none of the functions called by the constructor can call back (virtually or statically) to the object under construction. But the constructor DOES give away the this
pointer.
首先,编译器可以自由地执行任何优化,在 C++ 顺序执行模型中,就像遵循规则一样。例如,如果编译器可以证明在构造函数内部没有进行虚拟调用,它可以等待并在构造函数主体的末尾而不是开头设置 v-table 指针。如果构造函数没有给出this
指针,因为构造函数的调用者还没有收到它的指针副本,那么构造函数调用的任何函数都不能回调(虚拟或静态)下的对象建造。但是构造函数确实放弃了this
指针。
We have to look closer. If the function to which the this pointer is given is visible to the compiler (i.e. included in the current compilation unit), the the compiler can include its behavior in the analysis. We weren't given that function in this question (the constructor and member functions of class task
), but it seems likely that the only thing that happens is that said pointer is stored in a subobject which is also not reachable from outside the constructor.
我们必须仔细观察。如果给出 this 指针的函数对编译器是可见的(即包含在当前编译单元中),则编译器可以将其行为包含在分析中。在这个问题中我们没有给出那个函数( 的构造函数和成员函数class task
),但似乎唯一发生的事情是所述指针存储在一个子对象中,该子对象也无法从构造函数外部访问。
"Foul!", you cry, "I passed the address of that task
subobject to a library CreateThread
function, therefore it is reachable and through it, the main object is reachable." Ah, but you do not comprehend the mysteries of the "strict aliasing rules". That library function does not accept a parameter of type task *
, now does it? And being a parameter whose type is perhaps intptr_t
, but definitely neither task *
nor char *
, the compiler is permitted to assume, for purposes of as-if optimization, that it does not point to a task
object (even if it clearly does). And if it does not point to a task
object, and the only place our this
pointer got stored is in a task
member subobject, then it cannot be used to make virtual calls to this
, so the compiler may legitimately delay setting up the virtual call mechanism.
“犯规!”,你哭了,“我把那个task
子对象的地址传递给了一个库CreateThread
函数,因此它是可达的,通过它,主对象是可达的。” 啊,但是你没有理解“严格别名规则”的奥秘。那个库函数不接受 type 的参数task *
,现在是吗?并且作为一个参数,其类型可能是intptr_t
,但绝对不是task *
也不是char *
,编译器被允许假设,为了 as-if 优化的目的,它不指向一个task
对象(即使它明确指向)。如果它不指向一个task
对象,并且我们的this
指针存储的唯一位置是在task
成员子对象中,那么它不能用于对this
,因此编译器可以合理地延迟设置虚拟调用机制。
But that's not all. Even if the compiler does set up the virtual call mechanism on schedule, the CPU memory model only guarantees that the change is visible to the current CPU core. Writes may become visible to other CPU cores in a completely different order. Now, the library create thread function ought to introduce a memory barrier that constrains CPU write reordering, but that fact that Koz's answer introducing a critical section (which definitely includes a memory barrier) changes the behavior suggests that perhaps no memory barrier was present in the original code.
但这还不是全部。即使编译器确实按计划设置了虚拟调用机制,CPU 内存模型也只保证更改对当前 CPU 内核可见。写入可能会以完全不同的顺序对其他 CPU 内核可见。现在,库创建线程函数应该引入一个内存屏障来限制 CPU 写入重新排序,但事实上 Koz 的回答引入了一个临界区(其中肯定包括一个内存屏障)改变了行为表明可能没有内存屏障存在于原始代码。
And, CPU write reordering can not only delay the v-table pointer, but the storage of the this pointer into the task
subobject.
并且,CPU 写重排序不仅可以延迟 v-table 指针,还可以延迟 this 指针到task
子对象中的存储。
I hope you have enjoyed this guided tour of one small corner of the cave of "multithreaded programming is hard".
我希望你喜欢这个关于“多线程编程很难”洞穴的一个小角落的导览游。
回答by Goz
printf is not, afaik, thread safe. Try surrounding the printf with a critical section.
printf 不是,afaik,线程安全的。尝试用临界区包围 printf。
To do this you InitializeCriticalSectioninside iDataLinkLayer class. Then around the printfs you need an EnterCriticalSectionand a LeaveCriticalSection. This will prevent both functions entering the printf simultaneously.
为此,您需要在 iDataLinkLayer 类中InitializeCriticalSection。然后在 printfs 周围,您需要一个EnterCriticalSection和一个LeaveCriticalSection。这将防止两个函数同时进入 printf。
Edit: Try changing this code:
编辑:尝试更改此代码:
dll_task_1* task1 = new task(this);
dll_task_2* task2 = new task(this);
to
到
dll_task_1* task1 = new dll_task_1(this);
dll_task_2* task2 = new dll_task_2(this);
Im guessing that task is in fact the base class of dll_task_1 and dll_task_2 ... so, more than anything, im surprised it compiles ....
我猜这个任务实际上是 dll_task_1 和 dll_task_2 的基类......所以,最重要的是,我很惊讶它编译......
回答by pagra
I think it's not always safe to use 'this' (i.e. to call a member function) before the end of the constructor. It could be that task are calling member function of DataLinkLayer before the end of DataLinkLayer constructor. Especially if this member function is virtual: http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.7
我认为在构造函数结束之前使用“this”(即调用成员函数)并不总是安全的。可能是任务在 DataLinkLayer 构造函数结束之前调用了 DataLinkLayer 的成员函数。特别是如果这个成员函数是虚拟的:http: //www.parashift.com/c++-faq-lite/ctors.html#faq-10.7
回答by James
I wanted to comment on the creation of the DataLinkLayer.
我想评论一下 DataLinkLayer 的创建。
When I call the DataLinkLayer constructor from main:
当我从 main 调用 DataLinkLayer 构造函数时:
int main () {
DataLinkLayer* dataLinkLayer = new DataLinkLayer();
while(true); // to keep the main thread running
}
I, of coruse, do not destruct the object, this is first. Now, inside the DataLinkLayer cosntructor I initialize many (not only these two tasks) other objects isntances and pass to most of them dataLinkLayer pointer (using this
). This is legal, as far as I am concerned. Put it further - it compiles and runs as expected.
我当然不会破坏物体,这是第一点。现在,在 DataLinkLayer 构造器中,我初始化了许多(不仅是这两个任务)其他对象,并将 dataLinkLayer 指针(使用this
)传递给其中的大多数。就我而言,这是合法的。更进一步 - 它按预期编译和运行。
What I became curious about is the overall design idea that I am following (if any :) ).
我开始好奇的是我所遵循的整体设计理念(如果有的话:))。
The DataLinkLayer is a parent class that is accessed by several tasks which try to modify it parameters or perform some other processing. Since I want that everything remain as decoupled as possible I provide only interfaces for the accessors and encapsulate the data so that I don't have any global variables, friend functions etc.
DataLinkLayer 是一个父类,它被几个试图修改它的参数或执行一些其他处理的任务访问。因为我希望一切都尽可能保持解耦,所以我只为访问器提供接口并封装数据,这样我就没有任何全局变量、友元函数等。
It would have been a pretty easy task to do if only multithreading would not be there. I beleive I will encounter many other pitfalls on my way.
如果不存在多线程,这将是一项非常容易的任务。我相信我会在途中遇到许多其他陷阱。
Feel free to discuss it please and merci for your generous comments!
请随时讨论它,并感谢您的慷慨评论!
UPD:
更新:
Speaking of passing the iDataLinkLayer interface pointer to the tasks - is this a good way to do it? In Java it would be pretty usual thing to realize a containment or so called strategy pattern to make things decoupled and stuff. However I am not 100% sure whether it is a good solution in c++... Any suggestions/commnets on it?
说到将 iDataLinkLayer 接口指针传递给任务 - 这是一个好方法吗?在 Java 中,实现包含或所谓的策略模式来使事物解耦等是很常见的事情。但是我不是 100% 确定它是否是 C++ 中的一个很好的解决方案......有什么建议/commnets 吗?