为什么将operator()的定义从.h移动到.cpp文件导致数据竞争?

air*_*rne 1 c++ mutex locking header

在我的代码中我有以下头文件:

Global.h:

#ifndef GLOBAL_H_
#define GLOBAL_H_
#include <mutex>
namespace
{
    std::mutex outputMutex;
}
#endif
Run Code Online (Sandbox Code Playgroud)

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 
Run Code Online (Sandbox Code Playgroud)

Test2.h实际上等于Test1.h,只包含一个名为"TestClass2"而不是"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();
}
Run Code Online (Sandbox Code Playgroud)

如果我像这样运行程序,我得到预期的输出:

HelloHelloHelloHelloHelloHelloHello

要么

GoodByeGoodByeGoodByeGoodByeGoodByeGoodByeGoodBye

到现在为止还挺好.但是当我在Test.cpp和Test2.cpp的源文件中放入Test.h和Test2.h的()--operator的定义时:

(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;
    }
}
Run Code Online (Sandbox Code Playgroud)

并相应地从头文件中删除定义:void operator()();我突然开始偶尔输出这样的输出:

GoodByeHelloGoodByeHelloGoodByeHelloGoodByeHelloGoodByeHelloGoodByeHelloGoodByeHello

我不知道为什么带有互斥变量的锁outputMutex不再起作用,但我认为它与正在创建的变量的两个版本有关,但我很想得到一个专业的解释.我正在使用Eclipse和Cygwin.

Yak*_*ont 5

这是未定义行为和匿名命名空间的混合.

首先这个:

namespace {
  std::mutex outputMutex;
}
Run Code Online (Sandbox Code Playgroud)

这是一个包含互斥锁的匿名命名空间outputMatrix.不同的outputMatrix存在于每一个源文件,因为它有一个不同的名称.

这就是匿名命名空间的作用.将它们视为"为每个构建此文件的cpp文件生成唯一的guid".它们旨在防止链接时符号冲突.

class TestClass {
  std::string name;
public:
  // ...
  void operator()() {
    // ...
  }
};
Run Code Online (Sandbox Code Playgroud)

这是一个(含蓄地)inline TestClass::operator().它的主体在每个编译单元中编译.通过ODR ,每个编译单元的主体必须相同,或者您的程序格式错误,无需诊断.(在类定义中定义的方法是隐含的inline,包含所有这些包袱).

它使用来自匿名命名空间的令牌.此标记在每个编译单元中具有不同的含义.如果有多个编译单元,则结果是一个不正确的程序,无需诊断; C++标准对其行为没有限制1.

在这个特定的情况下,相同的编译单元被选择用于operator()TestClassTestClass2.所以它使用了相同的互斥锁.这不可靠; 部分重建可能导致它改变,或月亮的阶段.

当您将其放入自己的.cpp文件中时,它不再是隐式的inline.只存在一个定义,但它们位于不同的编译单元中.

这两个不同的编译单元有一个不同的outputMatrix互斥锁.


1违反该特定规则的最常见影响是链接器根据任意条件(可以从构建更改为构建!)选择一个实现,并以静默方式丢弃其余条件.这不好,因为构建过程的无意识更改(添加更多内核,部分构建等)可能会破坏您的代码.不要违反"内联函数必须具有相同的定义"的规则.这只是最常见的症状; 你不能保证有任何明智的事情发生.