【问题标题】:C++ Can an atomic variable be declared inside a structure to protect those members?C++ 可以在结构内声明原子变量以保护这些成员吗?
【发布时间】:2018-11-18 22:53:36
【问题描述】:

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

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

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

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

线程 1 不断锁定/解锁以 写入 到结构中的成员(测试值相同):

    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");
    }

期待看到很多:

“正在使用中!”

如果它进入,每次解锁时:

“0,测试”

..确实如此。

但也看到了:

1,测试”

如果 atomic bool 是 1,我不希望看到它。

我做错了什么?

【问题讨论】:

标签: c++ windows multithreading atomic


【解决方案1】:

您的代码不是线程安全的。原子是原子的。但if 声明不是!

会发生什么:

Thread 1                                Thread 2               Comment 

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

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

解决方案:

由于您打算将原子用作锁,因此只需使用专为这种同步设计的真正锁,使用std::mutexstd::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

【讨论】:

  • 啊,现在我明白了……但不确定在另一个线程中更多的锁定是否是使用它的正确方法?
  • @P.S.我认为您将原子操作与锁定混淆了,STL 中的atomic 仅提供对普通类型(在您的情况下为 bool )的不间断修改,这不会使任何线程安全。我认为您正在寻找的是一个适当的锁定对象,在示例中您可以看到 mutex 的用法以及 lock_gaurd
  • @ChrisMc lock_guard 将是此用例的解决方案。
  • @christophe:谢谢......几个问题:在线程 1 中,评论说“// 保护直到下一次迭代”......这是因为它在 while() 语句中超出了范围并摧毁物体?成员“userStruct.name”是否受到保护,因为我们如何解释和具体使用锁?或如何“锁定(userStruct.access);”保护其他成员?谢谢。
  • @P.S.确切地! lock_guard 锁定互斥锁。在循环结束时,它超出范围,被销毁,然后释放互斥锁。保护由互斥体和约定提供,您首先获取互斥体,然后再使用其他成员(或代码中的任何其他资源或对象)。锁不能防止不遵守约定的恶意访问。
【解决方案2】:

您正在对 atomic 变量执行 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 中的值,因为检查锁的值几乎总是一个坏主意,因为在您采取行动之前该值可能会发生变化。

【讨论】:

  • 我可能会使用mutex 甚至shared_mutex,以防受保护部分内的代码引发异常。互斥锁在超出范围时会自动解锁。 atomic_flag 不会自动清除,您需要为此制作自定义 RAII 包装器。
  • 代码无论如何都是错误的。读取器和写入器可以同时在 if 内部 - 首先读取器输入 if 并将 inUse 设置为 true,然后写入器输入到 if 到。代码没有同步和错误
  • write中的if语句需要倒置;如果inUse 标志为假,您想进入该块
  • @LWimsey 我修好了
  • 在这种情况下是的,修复后更正。进入临界区的代码对于两个线程必须是对称的。也用于突出主要概念 - 更好地使用if (!userStruct.inUse.test_and_set(memory_order_acquire)) { ... userStruct.inUse.clear(memory_order_release); }
【解决方案3】:

正如 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");

解决方案是“锁定”读数,但简单地执行以下操作仍然不安全:

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
}

【讨论】:

  • userStruct.inUse.compare_exchange_strong(f,true) 是什么意思?无论如何,在此之后你从userStruct.inUse.load() 获得了随机值
  • Quimby,虽然没有错,但从技术上讲,您不需要 compare_exchange 来换取自旋锁。见this question
猜你喜欢
  • 2020-05-09
  • 1970-01-01
  • 2014-08-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-26
  • 2023-01-12
相关资源
最近更新 更多