C++ 可以在结构内声明原子变量以保护这些成员吗?

C++ Can an atomic variable be declared inside a structure to protect those members?

在共享文件中声明和定义了一个结构。

由 Windows API CreateThread() 创建的两个线程都可以看到它的实例:

struct info
{
    std::atomic<bool> inUse; 
    string name;

};
info userStruct; //this guy shared between two threads

线程 1 连续 locking/unlocking 到 写入结构中的成员(测试值相同):

    while (1)
    {
        userStruct.inUse = true;
        userStruct.name= "TEST";
        userStruct.inUse = false;
    }   

线程 2 只是读取 和打印,只有当它碰巧抓住它解锁时

    while (1)
    {
        while (! userStruct.inUse.load() )
        {
            printf("** %d, %s\n\n", userStruct.inUse.load(), userStruct.name.c_str());
            Sleep(500); //slower reading
        }

        printf("In Use!\n");
    }

期待看到很多:

"In Use!"

解锁后每进入一次:

"0, TEST"

..确实如此。

还看到:

"1, TEST"

如果原子布尔值是 1,我不希望看到它。

我做错了什么?

您的代码不是线程安全的。原子就是原子。但是 if 语句不是!

会发生什么:

Thread 1                                Thread 2               Comment 

while (! userStruct.inUse.load() )                             ---> InUse is false 
==> continues loop 
                                        inUse = true           
==> continues loop already started
printf(...) 

在最坏的情况下,您可能会因为数据竞争而出现 UB(线程 2 修改字符串,线程 1 在修改期间读取字符串)。

解决方案:

由于您打算将原子用作锁,因此只需使用为这种同步设计的真正锁,使用 std::mutex with a std::lock_guard

例如:

struct info
{
    std::mutex access; 
    string name;
}; 

第一个线程将是:

while (1)
{
    std::lock_guard<std::mutex> lock(userStruct.access); // protected until next iteration
    userStruct.name= "TEST";
}   

然后第二个线程可以尝试以非阻塞方式访问互斥量:

while (1)
{
    {  //  trying to lock the mutex
        std::unique_lock<std::mutex> lock(userStruct.access, std::try_to_lock);
        if(!lock.owns_lock()){   // if not successful do something else
            std::cout << "No lock" <<std::endl; 
        }
        else                     // if lock was successfull
        {
            std::cout << "Got access:" << userStruct.name <<std::endl;
        }
    } // at this stage, the lock is released.
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
}

Online demo

您正在对原子变量执行 2 次 distict 加载以检查然后输出。该值可以在负载之间改变。你的字符串变量也有数据竞争。

您可以使用 std::atomic_flag 或互斥体

轻松修复它
struct info
{
    std::atomic_flag inUse;
    std::string name;

};

//writer
while (1)
{
    if (!userStruct.inUse.test_and_set()) {
        userStruct.name= "TEST";
        userStruct.inUse.clear();
    }
}

//reader
while (1)
{
    if (!userStruct.inUse.test_and_set())
    {
        printf("** %s\n\n", userStruct.name.c_str());
        userStruct.inUse.clear();
    }
    printf("In Use!\n");
}

您无法检查 atomic_flag 中的值,因为检查锁的值几乎总是一个坏主意,因为在您采取行动之前该值可能会发生变化。

正如 Tyker 在评论中指出的那样,您遇到了竞争条件。(如果它处于无限循环中,则不需要内部 while。)

if (! userStruct.inUse.load() )
{
    //inUse might change in the middle printf
    printf("** %d, %s\n\n", userStruct.inUse.load(), userStruct.name.c_str());
    Sleep(500); //slower reading
}
else
   printf("In Use!\n");

解决方法是"lock"阅读,但简单地做以下仍然不安全:

if (! userStruct.inUse.load() ) //#1
{
    //inUse might already be true here, so we didn't lock quickly enough. 
    userStruct.inUse=true; //#2
    printf("** %d, %s\n\n", userStruct.inUse.load(), userStruct.name.c_str());
    userStruct.inUse=false;
    Sleep(500); //slower reading
}

因此,真正安全的代码是将#1、#2 融合在一起:

bool f=false;
//Returns true if inUse==f and sets it to true
if(userStruct.inUse.compare_exchange_strong(f,true))
{
    printf("** %d, %s\n\n", userStruct.inUse.load(), userStruct.name.c_str());
    userStruct.inUse=false;
    Sleep(500); //slower reading
}