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));
}
您正在对原子变量执行 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
}
在共享文件中声明和定义了一个结构。
由 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));
}
您正在对原子变量执行 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
}