无法使用std :: atomic来正确保护数据

时间:2015-12-14 23:21:50

标签: c++ multithreading c++11

为什么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;
}

3 个答案:

答案 0 :(得分:2)

if (!aLock->IsStopped()) {
    aLock->Use();
看起来很奇怪。

IsStopped()返回false之后,状态可能会在您致电Use()之前停止(因此您可能会Use()停止锁定。

该解决方案的返回值为Use,以便在被禁止操作时进行通信,而不是进行检查,然后执行Use()

答案 1 :(得分:1)

  

希望测试不会再次失败。

如果你有足够的测试,它肯定可以,并且。众所周知,双重检查锁定是不安全的。

原子的用法本身不是原子的。因此,您的操作不是原子操作。

或者换句话说,原子性就像const一样,它不会隐式传播。通过使用原子变量编写它,您不能简单地进行安全操作。您必须编写完全原子操作以及简单地使用原子变量。

如果您不能完成基于原子基元编写原子算法的任务,则必须使用互斥锁使其成为原子。

此外,您的非原子代码不仅在并发性方面不安全,而且还有未定义的行为,因为那里存在数据竞争。它也是未定义的行为,因为您使用的变量是非易失性的,因此编译器可以假设它们不会在外部进行更改,并且编译器可能会根据此事实进行优化。立即抛出此代码;它无法使用。

答案 2 :(得分:0)

感谢您提供其他答案,但他们似乎无法回答这个问题。

发布的代码有问题,因为它在第二次检查之前读取了回调函数的值。

更正了ReadState

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,这会导致失败。