为什么std :: atomic版本的代码仍然失败? (当refCount为非零时,回调会发生变化,doStop(为)为false。
我有一段多线程的代码,行为不正确,并尝试修复它。
然而我的修复仍然不可靠,但我不明白为什么。
原始代码线程A(使用回调): -
if( !IsUpdating ) {
IncrementReference();
if( !IsUpdating && GetCallBackPointer() ) {
cb = GetCallBackPointer();
cb();
}
DecrementReference();
}
原始代码线程B - (修改回调)
IsUpdating = true;
while( ReferencesUsingCallback ) {
Sleep( 10 );
}
callback = newValue;
IsUpdating = false;
如果ReferencesUsingCallback不为0,则不允许修改回调线程更改回调值。
通过测试,AddRef和测试,对竞争条件有“保护”。希望测试不会再次失败。
不幸的是,代码不起作用,我的假设是这是由于某些缓存一致性问题。
我已经使用std :: atomic来尝试再次提供测试用例,但它仍然可能失败。 std :: atomic版本是'AtomicLockedData'。该平台是Intel i7上的Windows
完整代码: -
#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>
#define FAILED_LIMIT 5
#define LOOP_SIZE 1000000000LL
void Function()
{
}
typedef void (*CallbackFunction)(void);
int FailedCount;
__int64 counter = 0;
class lockedData {
public:
lockedData() : value(nullptr), value2(nullptr)
{
doStop = 0;
usageCount = 0;
}
long usageCount;
long doStop;
volatile CallbackFunction value;
void * value2;
int Use()
{
return usageCount++;
}
int UnUse()
{
return usageCount--;
}
int Usage() const
{
return usageCount;
}
void SetStop()
{
doStop = 1;
}
void UnStop()
{
doStop = 0;
}
bool IsStopped()
{
return doStop != 0;
}
void StoreData(CallbackFunction pData )
{
value = pData;
}
CallbackFunction ReadData()
{
return value;
}
};
class AtomicLockedData {
public:
AtomicLockedData() : value(nullptr), value2(nullptr)
{
doStop = false;
usageCount = 0;
}
std::atomic<int> usageCount;
std::atomic<bool> doStop;
std::atomic<CallbackFunction> value;
void * value2;
int Use()
{
return usageCount++;
}
int UnUse()
{
return usageCount--;
}
int Usage() const
{
return usageCount.load();
}
void SetStop()
{
doStop.store( true);
}
void UnStop()
{
doStop.store( false );
}
bool IsStopped()
{
return doStop.load() == true;
}
void StoreData(CallbackFunction pData)
{
value.store( pData );
}
CallbackFunction ReadData()
{
return value.load();
}
};
template < class lockData >
int UpdateState( lockData & aLock, CallbackFunction pData, void * pData2 )
{
aLock.SetStop();
while(aLock.Usage() > 0 )
std::this_thread::sleep_for( std::chrono::milliseconds(10) );
aLock.value = pData;
aLock.UnStop();
return 0;
}
template <class lockData >
int ReadState( lockData * aLock, int fib)
{
if (!aLock->IsStopped()) {
aLock->Use();
CallbackFunction val = aLock->ReadData();
if (!aLock->IsStopped() && val) {
fibonacci(fib);
CallbackFunction pTest = const_cast<CallbackFunction>( aLock->ReadData());
if (pTest == 0) {
FailedCount++; // shouldn't be able to change value if use count is non-zero
printf("Failed\n");
}
else {
pTest();
}
}
aLock->UnUse();
}
return 0;
}
unsigned __int64 fibonacci(size_t n)
{
if (n < 3) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
template< class lockData > void ThreadA( lockData * lkData , int fib )
{
void * pData2 = new char[200];
while (FailedCount < FAILED_LIMIT) {
UpdateState< lockData>(*lkData, Function, pData2);
fibonacci(fib);
UpdateState< lockData>(*lkData, NULL, NULL);
fibonacci(fib);
}
}
template< class lockData > void ThreadB(lockData & lkData, int fib )
{
while (FailedCount < FAILED_LIMIT && counter < LOOP_SIZE) {
ReadState(&lkData, fib);
ReadState(&lkData, fib);
ReadState(&lkData, fib);
ReadState(&lkData, fib);
ReadState(&lkData, fib);
ReadState(&lkData, fib);
ReadState(&lkData, fib);
ReadState(&lkData, fib);
ReadState(&lkData, fib);
ReadState(&lkData, fib);
counter++;
}
}
template <class lockType >
void TestLock()
{
counter = 0;
FailedCount = 0;
lockType lk;
std::thread thr(ThreadA<lockType>, &lk, 3);
ThreadB(lk, 3);
thr.join();
printf("Failed %d times for %I64d iterations", FailedCount, counter);
}
int main(int argc, char ** argv)
{
TestLock< lockedData >();
TestLock< AtomicLockedData >();
return 0;
}
答案 0 :(得分:2)
行
if (!aLock->IsStopped()) {
aLock->Use();
看起来很奇怪。
在IsStopped()
返回false
之后,状态可能会在您致电Use()
之前停止(因此您可能会Use()
停止锁定。
该解决方案的返回值为Use
,以便在被禁止操作时进行通信,而不是进行检查,然后执行Use()
。
答案 1 :(得分:1)
希望测试不会再次失败。
如果你有足够的测试,它肯定可以,并且将。众所周知,双重检查锁定是不安全的。
原子的用法本身不是原子的。因此,您的操作不是原子操作。
或者换句话说,原子性不就像const
一样,它不会隐式传播。通过使用原子变量编写它,您不能简单地进行安全操作。您必须编写完全原子操作以及简单地使用原子变量。
如果您不能完成基于原子基元编写原子算法的任务,则必须使用互斥锁使其成为原子。
此外,您的非原子代码不仅在并发性方面不安全,而且还有未定义的行为,因为那里存在数据竞争。它也是未定义的行为,因为您使用的变量是非易失性的,因此编译器可以假设它们不会在外部进行更改,并且编译器可能会根据此事实进行优化。立即抛出此代码;它无法使用。
答案 2 :(得分:0)
感谢您提供其他答案,但他们似乎无法回答这个问题。
发布的代码有问题,因为它在第二次检查之前读取了回调函数的值。
template <class lockData >
int ReadState( lockData * aLock, int fib)
{
if (!aLock->IsStopped()) {
aLock->Use();
CallbackFunction val;
if (!aLock->IsStopped() && (val = aLock->ReadData() ) ) {
fibonacci(fib);
CallbackFunction pTest = const_cast<CallbackFunction>( aLock->ReadData());
if (pTest == 0) {
FailedCount++; // shouldn't be able to change value if use count is non-zero
printf("Failed\n");
}
else {
pTest();
}
}
aLock->UnUse();
}
return 0;
}
此代码的原子版本不会失败,但非原子版本会失败。将volatile添加到非原子版本(修复数据竞争?)并没有帮助。
该代码旨在确保读取在当前正在更新时不使用回调值。希望能做到这个wuthout锁定,支持ReadState解决方案,因此添加锁定会起作用但是毫无意义。
在执行ReadState之前,我们检查是否未运行更新。我不确定这会有所帮助。
增加使用次数后,会检查IsStopped
。这可确保在使用率为0之前阻止UpdateState进一步操作。
因此,在UpdateState测试了使用计数后,剩余的竞争是ReadState调用增量。
修复是确保在检查IsStopped
后读取val。正确的方法是,如果UpdateState
线程错过了增量,并且仍在执行,那么ReadState
将保释并稍后尝试。否则,我们知道IsStopped
为false,UpdateState
在使用为0之前不会进行更改,那么我们就可以读取该值,并且不会更改。
在val
创建问题之前阅读IsStopped
,其中值可以在读取和第二次测试(pTest)之间更改,并且IsStopped设置为0,这会导致失败。