尝试创建原子引用计数失败并出现死锁。这是正确的方法吗?

时间:2014-07-17 01:27:52

标签: c++ multithreading concurrency atomic

所以我试图创建一个写时复制映射,它使用读取端的原子引用计数尝试来锁定。

有些事情不太对劲。我看到一些引用过度增加,有些引用下降,所以某些东西不是真正的原子。在我的测试中,我有10个读取器线程循环100次,每次执行get()和1个写入器线程执行100次写入。

它在作者中陷入困境,因为有些引用永远不会降到零,即使它们应该。

我正在尝试使用放置explained by this blog的128位DCAS技术。

这有什么明显的错误或是否有更简单的方法来调试它而不是在调试器中使用它?

typedef std::unordered_map<std::string, std::string> StringMap;

static const int zero = 0;  //provides an l-value for asm code

class NonBlockingReadMapCAS {

public:

    class OctaWordMapWrapper {
    public:
        StringMap* fStringMap;
        //std::atomic<int> fCounter;
        int64_t fCounter;

        OctaWordMapWrapper(OctaWordMapWrapper* copy) : fStringMap(new StringMap(*copy->fStringMap)), fCounter(0) { }

        OctaWordMapWrapper() : fStringMap(new StringMap), fCounter(0) { }

        ~OctaWordMapWrapper() {
            delete fStringMap;
        }

        /**
         * Does a compare and swap on an octa-word - in this case, our two adjacent class members fStringMap 
         * pointer and fCounter.
         */
        static bool inline doubleCAS(OctaWordMapWrapper* target, StringMap* compareMap, int64_t compareCounter, StringMap* swapMap, int64_t swapCounter ) {
            bool cas_result;
            __asm__ __volatile__
            (
             "lock cmpxchg16b %0;"    // cmpxchg16b sets ZF on success
             "setz       %3;"         // if ZF set, set cas_result to 1

             : "+m" (*target),
               "+a" (compareMap),     //compare target's stringmap pointer to compareMap
               "+d" (compareCounter), //compare target's counter to compareCounter
               "=q" (cas_result)      //results
             : "b"  (swapMap),        //swap target's stringmap pointer with swapMap
               "c"  (swapCounter)     //swap target's counter with swapCounter
             : "cc", "memory"
             );
            return cas_result;
        }



    OctaWordMapWrapper* atomicIncrementAndGetPointer()
    {

        if (doubleCAS(this, this->fStringMap, this->fCounter, this->fStringMap, this->fCounter +1))
            return this;
        else
            return NULL;
    }


        OctaWordMapWrapper* atomicDecrement()
        {
            while(true) {
                if (doubleCAS(this, this->fStringMap, this->fCounter, this->fStringMap, this->fCounter -1))
                    break;
            }
            return this;
        }

        bool atomicSwapWhenNotReferenced(StringMap* newMap)
        {
            return doubleCAS(this, this->fStringMap, zero, newMap, 0);
        }
    }
    __attribute__((aligned(16)));

    std::atomic<OctaWordMapWrapper*> fReadMapReference;
    pthread_mutex_t fMutex;


    NonBlockingReadMapCAS()  {
        fReadMapReference = new OctaWordMapWrapper();
    }

    ~NonBlockingReadMapCAS() {
       delete fReadMapReference;
    }

    bool contains(const char* key) {
        std::string keyStr(key);
        return contains(keyStr);
    }

    bool contains(std::string &key) {
        OctaWordMapWrapper *map;
        do {
            map = fReadMapReference.load()->atomicIncrementAndGetPointer();
        } while (!map);
        bool result = map->fStringMap->count(key) != 0;
        map->atomicDecrement();
        return result;
    }

    std::string get(const char* key) {
        std::string keyStr(key);
        return get(keyStr);
    }

    std::string get(std::string &key) {
        OctaWordMapWrapper *map;
        do {
            map = fReadMapReference.load()->atomicIncrementAndGetPointer();
        } while (!map);
        //std::cout << "inc " << map->fStringMap << " cnt " << map->fCounter << "\n";
        std::string value = map->fStringMap->at(key);
        map->atomicDecrement();
        return value;
    }

    void put(const char* key, const char* value) {
        std::string keyStr(key);
        std::string valueStr(value);
        put(keyStr, valueStr);
    }

    void put(std::string &key, std::string &value) {
        pthread_mutex_lock(&fMutex);
        OctaWordMapWrapper *oldWrapper = fReadMapReference;
        OctaWordMapWrapper *newWrapper = new OctaWordMapWrapper(oldWrapper);
        std::pair<std::string, std::string> kvPair(key, value);
        newWrapper->fStringMap->insert(kvPair);
        fReadMapReference.store(newWrapper);
        std::cout << oldWrapper->fCounter << "\n";
        while (oldWrapper->fCounter > 0);
        delete oldWrapper;
        pthread_mutex_unlock(&fMutex);

    }

    void clear() {
        pthread_mutex_lock(&fMutex);
        OctaWordMapWrapper *oldWrapper = fReadMapReference;
        OctaWordMapWrapper *newWrapper = new OctaWordMapWrapper(oldWrapper);
        fReadMapReference.store(newWrapper);
        while (oldWrapper->fCounter > 0);
        delete oldWrapper;
        pthread_mutex_unlock(&fMutex);

    }

};

4 个答案:

答案 0 :(得分:3)

也许不是答案,但这看起来很可疑:

while (oldWrapper->fCounter > 0);
delete oldWrapper;

当计数器为0时,您可以让读者线程进入atomicIncrementAndGetPointer(),从而通过删除包装器将读取器线程下方的地毯拉出来。

编辑以总结以下评论以寻找潜在解决方案:

我所知道的最佳实现是将fCounterOctaWordMapWrapper移到fReadMapReference(实际上根本不需要OctaWordMapWrapper类)。当计数器为零时,在指针中交换指针。因为你可以有很高的读写器线程争用,它本质上会无限期地阻塞写入器,你可以为读取器锁定分配最高位fCounter,即当这个位置位时,读取器会旋转直到该位被清除。当写入器即将更改指针,等待计数器降至零(即现有读取器完成其​​工作)然后交换指针并清除该位时,写入器设置此位(__sync_fetch_and_or())。

这种方法应该是防水的,尽管它在写入时显然会阻止读者。我不知道在你的情况下这是否可以接受,理想情况下你希望这是非阻塞的。

代码看起来像这样(未经过测试!):

class NonBlockingReadMapCAS
{
public:
  NonBlockingReadMapCAS() :m_ptr(0), m_counter(0) {}

private:
  StringMap *acquire_read()
  {
    while(1)
    {
      uint32_t counter=atom_inc(m_counter);
      if(!(counter&0x80000000))
        return m_ptr;
      atom_dec(m_counter);
      while(m_counter&0x80000000);
    }
    return 0;
  }

  void release_read()
  {
    atom_dec(m_counter);
  }

  void acquire_write()
  {
    uint32_t counter=atom_or(m_counter, 0x80000000);
    assert(!(counter&0x80000000));
    while(m_counter&0x7fffffff);
  }

  void release_write()
  {
    atom_and(m_counter, uint32_t(0x7fffffff));
  }

  StringMap *volatile m_ptr;
  volatile uint32_t m_counter;
};

在&amp;之前调用acquire / release_read / write()在访问指针进行读/写之后。将atom_inc/dec/or/and()分别替换为__sync_fetch_and_add()__sync_fetch_and_sub()__sync_fetch_and_or()__sync_fetch_and_and()。实际上你不需要doubleCAS()

正如@Quuxplusone在下面的评论中正确指出的那样,这是单一制片人&amp;多个消费者实施。我修改了代码以正确断言以强制执行此操作。

答案 1 :(得分:2)

嗯,可能有很多问题,但这里有两个明显的问题。

最琐碎的错误在atomicIncrementAndGetPointer。你写道:

if (doubleCAS(this, this->fStringMap, this->fCounter, this->fStringMap, this->fCounter +1))

也就是说,您尝试以无锁方式递增this->fCounter。但是它不起作用,因为你只需要两次获取旧值,而不能保证每次都读取相同的值。考虑以下事件序列:

  • 线程A获取this->fCounter(值为0)并将参数5计算为this->fCounter +1 = 1
  • 线程B成功递增计数器。
  • 线程A获取this->fCounter(值为1)并将参数3计算为this->fCounter = 1
  • 线程A执行doubleCAS(this, this->fStringMap, 1, this->fStringMap, 1)。当然,它取得了成功,但我们已经失去了增量#34;我们试图这样做。

你想要的更像是

StringMap* oldMap = this->fStringMap;
int64_t oldCounter = this->fCounter;
if (doubleCAS(this, oldMap, oldValue, oldMap, oldValue+1))
    ...

另一个明显的问题是getput之间存在数据竞争。考虑以下事件序列:

  • 线程A开始执行get:它获取fReadMapReference.load()并准备在该内存地址上执行atomicIncrementAndGetPointer
  • 线程B完成执行put:它删除该内存地址。 (这是有权这样做的,因为包装器的引用计数仍为零。)
  • 线程A开始在已删除的内存地址上执行atomicIncrementAndGetPointer。如果你很幸运,那你就是段落错误,但当然在实践中你可能不会。

正如博客文章中所解释的那样:

  

省略了垃圾收集接口,但在实际应用中,您需要在删除节点之前扫描危险指针。

答案 2 :(得分:2)

另一个用户建议采用类似的方法,但是如果你使用gcc(也许还有clang)进行编译,你可以使用内部的__sync_add_and_fetch_4,这类似于你的汇编代码所做的,并且可能更加便携。 我在Ada库中实现refcounting时使用过它(但算法保持不变)。

int __sync_add_and_fetch_4 (int* ptr, int value);
// increments the value pointed to by ptr by value, and returns the new value

答案 3 :(得分:0)

虽然我不确定您的阅读器线程是如何工作的,但我怀疑您的问题是您没有抓住并处理out_of_range方法中可能由此行引起的get()个例外情况:std::string value = map->fStringMap->at(key);。请注意,如果在地图中找不到key,这将抛出并退出函数而不递减计数器,这将导致您描述的条件(陷入编写器线程中的while循环)等待计数器递减)。

无论如何,无论这是您查看与否的问题的原因,您都需要处理此异常(以及其他任何异常)或修改您的代码,以确保没有风险投掷。对于at()方法,我可能只使用find(),然后检查它返回的迭代器。但是,更一般地说,我建议使用 RAII 模式,以确保您不会在没有解锁/减少的情况下让任何意外异常消失。例如,您可以查看boost::scoped_lock来包装fMutex,然后为OctaWordMapWrapper增量/减量写一些简单的东西:

class ScopedAtomicMapReader
{
public:
    explicit ScopedAtomicMapReader(std::atomic<OctaWordMapWrapper*>& map) : fMap(NULL) { 
        do {
            fMap = map.load()->atomicIncrementAndGetPointer();
        } while (NULL == fMap);
    }

    ~ScopedAtomicMapReader() {
        if (NULL != fMap)
            fMap->atomicDecrement();
    }

    OctaWordMapWrapper* map(void) {
        return fMap;
    }

private:
    OctaWordMapWrapper* fMap;

}; // class ScopedAtomicMapReader

有了类似的内容,例如,您的contains()get()方法会简化(并且不受异常影响):

bool contains(std::string &key) {
    ScopedAtomicMapReader mapWrapper(fReadMapReference);
    return (mapWrapper.map()->fStringMap->count(key) != 0);
}

std::string get(std::string &key) {
    ScopedAtomicMapReader mapWrapper(fReadMapReference);
    return mapWrapper.map()->fStringMap->at(key);    // Now it's fine if this throws...
}

最后,虽然我认为您不应 来执行此操作,但您也可以尝试将fCounter声明为volatile(如果您有权访问put()它在fReadMapReference方法的while循环中将与读者线程上的写入不同。

希望这有帮助!

顺便说一下,另一件小事:{{1}}正在泄漏。我想你应该在你的析构函数中删除它。