为什么将 operator() 的定义从 .h 文件移动到 .cpp 文件导致数据竞争?
why is moving the definition of the operator() from .h to .cpp file causing a data race?
在我的代码中有以下头文件:
Global.h:
#ifndef GLOBAL_H_
#define GLOBAL_H_
#include <mutex>
namespace
{
std::mutex outputMutex;
}
#endif
Test.h:
#ifndef TEST_H_
#define TEST_H_
#include"Global.h"
#include<string>
#include<iostream>
class TestClass
{
std::string name;
public:
TestClass(std::string n):name{n}{}
void operator()()
{
for (int i=0;i<30;++i)
{
std::lock_guard<std::mutex> lock(outputMutex);
std::cout<<name<<name<<name<<name<<name<<name<<name<<std::endl;
}
}
};
#endif
Test2.h实际上等于Test1.h,只包含一个叫做"TestClass2"的class而不是"TestClass"。
我的 main.cpp 看起来像这样:
#include<iostream>
#include <thread>
#include "Global.h"
#include "Test.h"
#include "Test2.h"
using namespace std;
int main()
{
TestClass obj1("Hello");
TestClass2 obj2("GoodBye");
thread t1(obj1);
thread t2(obj2);
t1.join();
t2.join();
}
如果我 运行 这样的程序,我会得到预期的输出:
HelloHelloHelloHelloHelloHelloHello
或
GoodByeGoodByeGoodByeGoodByeGoodByeGoodByeGoodBye
到目前为止一切顺利。但是,当我将 Test.h 和 Test2.h 的 ()-运算符的定义放入源文件 Test.cpp 和 Test2.cpp:
(Test.cpp, Test2.cpp 相同):
#include "Test.h"
#include"Global.h"
void TestClass::operator()()
{
for (int i=0;i<30;++i)
{
std::lock_guard<std::mutex> lock(outputMutex);
std::cout<<name<<name<<name<<name<<name<<name<<name<<std::endl;
}
}
并相应地从头文件中删除定义:void operator()();
我突然开始偶尔得到这样的输出:
GoodByeHelloGoodByeHelloGoodByeHelloGoodByeHelloGoodByeHelloGoodByeHelloGoodByeHello
我不知道为什么带有互斥变量 outputMutex
的锁不再起作用,但我认为它与正在创建的变量的两个版本有关,但我会喜欢得到专业的解释。我将 Eclipse 与 Cygwin 结合使用。
这是未定义行为和匿名命名空间的混合体。
首先是这个:
namespace {
std::mutex outputMutex;
}
这是一个包含互斥锁 outputMatrix
的匿名命名空间。 每个源文件中存在不同的 outputMatrix
,因为它具有不同的名称。
这就是匿名命名空间的作用。将它们视为 "generate unique guid here for each cpp file that builds this"。它们旨在防止 link 时间符号冲突。
class TestClass {
std::string name;
public:
// ...
void operator()() {
// ...
}
};
这是一个(隐含的)inline
TestClass::operator()
。它的主体在每个编译单元中编译。根据 ODR,主体 在每个编译单元 中必须相同,否则您的程序格式错误,无需诊断。 (在 class 定义中定义的方法是隐含的 inline
,带有所有这些包袱)。
它使用来自匿名命名空间的令牌。此标记在每个编译单元中具有不同的含义。如果有多个编译单元,结果是一个不需要诊断的格式错误的程序; C++ 标准对其行为没有限制1.
在这种特殊情况下,从 TestClass
和 TestClass2
中为 operator()
选择了相同的编译单元。所以它使用了相同的互斥锁。这是不可靠的;部分重建可能会导致它发生变化,或者改变月相。
当您将它放入自己的 .cpp
文件中时,它不再是隐含的 inline
。只有一个定义存在,但它们位于不同的编译单元中。
这两个不同的编译单元获得了不同的 outputMatrix
互斥锁。
1 违反该特定规则的最常见影响是 linker 根据任意标准选择一个实现(可以在构建之间改变! ), 并默默地丢弃其余部分。这并不好,因为构建过程的无害更改(添加更多内核、部分构建等)可能会破坏您的代码。不要违反 "inline functions must have the same definition everywhere" 规则。这只是最常见的症状;不能保证您会发生这种明智的事情。
在我的代码中有以下头文件:
Global.h:
#ifndef GLOBAL_H_
#define GLOBAL_H_
#include <mutex>
namespace
{
std::mutex outputMutex;
}
#endif
Test.h:
#ifndef TEST_H_
#define TEST_H_
#include"Global.h"
#include<string>
#include<iostream>
class TestClass
{
std::string name;
public:
TestClass(std::string n):name{n}{}
void operator()()
{
for (int i=0;i<30;++i)
{
std::lock_guard<std::mutex> lock(outputMutex);
std::cout<<name<<name<<name<<name<<name<<name<<name<<std::endl;
}
}
};
#endif
Test2.h实际上等于Test1.h,只包含一个叫做"TestClass2"的class而不是"TestClass"。 我的 main.cpp 看起来像这样:
#include<iostream>
#include <thread>
#include "Global.h"
#include "Test.h"
#include "Test2.h"
using namespace std;
int main()
{
TestClass obj1("Hello");
TestClass2 obj2("GoodBye");
thread t1(obj1);
thread t2(obj2);
t1.join();
t2.join();
}
如果我 运行 这样的程序,我会得到预期的输出:
HelloHelloHelloHelloHelloHelloHello
或
GoodByeGoodByeGoodByeGoodByeGoodByeGoodByeGoodBye
到目前为止一切顺利。但是,当我将 Test.h 和 Test2.h 的 ()-运算符的定义放入源文件 Test.cpp 和 Test2.cpp:
(Test.cpp, Test2.cpp 相同):
#include "Test.h"
#include"Global.h"
void TestClass::operator()()
{
for (int i=0;i<30;++i)
{
std::lock_guard<std::mutex> lock(outputMutex);
std::cout<<name<<name<<name<<name<<name<<name<<name<<std::endl;
}
}
并相应地从头文件中删除定义:void operator()();
我突然开始偶尔得到这样的输出:
GoodByeHelloGoodByeHelloGoodByeHelloGoodByeHelloGoodByeHelloGoodByeHelloGoodByeHello
我不知道为什么带有互斥变量 outputMutex
的锁不再起作用,但我认为它与正在创建的变量的两个版本有关,但我会喜欢得到专业的解释。我将 Eclipse 与 Cygwin 结合使用。
这是未定义行为和匿名命名空间的混合体。
首先是这个:
namespace {
std::mutex outputMutex;
}
这是一个包含互斥锁 outputMatrix
的匿名命名空间。 每个源文件中存在不同的 outputMatrix
,因为它具有不同的名称。
这就是匿名命名空间的作用。将它们视为 "generate unique guid here for each cpp file that builds this"。它们旨在防止 link 时间符号冲突。
class TestClass {
std::string name;
public:
// ...
void operator()() {
// ...
}
};
这是一个(隐含的)inline
TestClass::operator()
。它的主体在每个编译单元中编译。根据 ODR,主体 在每个编译单元 中必须相同,否则您的程序格式错误,无需诊断。 (在 class 定义中定义的方法是隐含的 inline
,带有所有这些包袱)。
它使用来自匿名命名空间的令牌。此标记在每个编译单元中具有不同的含义。如果有多个编译单元,结果是一个不需要诊断的格式错误的程序; C++ 标准对其行为没有限制1.
在这种特殊情况下,从 TestClass
和 TestClass2
中为 operator()
选择了相同的编译单元。所以它使用了相同的互斥锁。这是不可靠的;部分重建可能会导致它发生变化,或者改变月相。
当您将它放入自己的 .cpp
文件中时,它不再是隐含的 inline
。只有一个定义存在,但它们位于不同的编译单元中。
这两个不同的编译单元获得了不同的 outputMatrix
互斥锁。
1 违反该特定规则的最常见影响是 linker 根据任意标准选择一个实现(可以在构建之间改变! ), 并默默地丢弃其余部分。这并不好,因为构建过程的无害更改(添加更多内核、部分构建等)可能会破坏您的代码。不要违反 "inline functions must have the same definition everywhere" 规则。这只是最常见的症状;不能保证您会发生这种明智的事情。