c ++ 11多读者/多作者队列,使用原子对象状态和永久递增的索引

时间:2016-06-09 22:02:23

标签: c++ multithreading c++11 atomic

我使用原子和循环缓冲区来实现多读写器线程,多写程序线程对象池。

很难调查,因为仪表代码会导致错误消失!

模型

生产者(或作家线程)向Element请求Ring以便准备'元素。终止时,编写器线程会更改元素状态,以便读者可以使用'它。之后,该元素再次可用于写作。

消费者(或读者线程)向Ring请求一个对象以便阅读'物体。 在'发布'对象,对象处于state::Ready状态,例如可供读者线程使用。 如果没有可用的对象,它可能会失败,例如,Ring中的下一个空闲对象不在state::Unused状态。

这两个课程ElementRing

Element

    要编写
  • ,作者线程必须成功地将_state成员从state::Unused更改为state::LockForWrite
  • 完成后,编写器线程将状态强制为state::Ready(它应该是唯一处理此元素的文件)
  • 要阅读,rader主题必须成功地将_state成员从state::Ready更改为state::LockForRead
  • 完成后,读者线程强制状态为state::Unused(它应该是处理此元素的唯一)

总结:

  • 作家生命周期:state::Unused - > state::LockForWrite - > state::Ready
  • 读者生命周期:state::Ready - > state::LockForRead - > state::Unused

Ring

  • 的向量为Element,被视为循环缓冲区。
  • std::atomic<int64_t> _read, _write;是用于通过以下方式访问元素的2个索引:
    • _elems[ _write % _elems.size() ]为作家,
    • _elems[ _read % _elems.size() ]为读者。

当读者成功LockForRead一个对象时,_read索引会递增。 当作者成功LockForWrite一个对象时,_write索引会递增。

main

我们在向量中添加了一些共享相同Ring的作者和读者线程。每个线程只是尝试get_read或get_write元素并在之后释放它们。

基于Element转换,一切都应该没问题,但可以观察到Ring在某个时刻被阻塞,就像因为环中的某些元素处于状态state::Ready并且_write % _elems.size()索引指向它并且对称地,环中的一些元素处于状态state::Unused,其上有一个_read % _elems.size()索引! 两者=死锁

#include<atomic>
#include<vector>
#include<thread>
#include<iostream>
#include<cstdint>

typedef enum : int
{
    Unused, LockForWrite, Ready,  LockForRead
}state;

class Element
{
    std::atomic<state> _state;
public:
    Element():_state(Unused){ }

    // a reader need to successfully make the transition Ready => LockForRead
    bool lock_for_read() { state s = Ready; return _state.compare_exchange_strong(s, LockForRead); }
    void unlock_read() { state s = Unused; _state.store(s); }

    // a reader need to successfully make the transition Unused => LockForWrite
    bool lock_for_write() { state s = Unused; return _state.compare_exchange_strong(s, LockForWrite); }
    void unlock_write() { state s = Ready;  _state.store(s); }
};

class Ring
{
    std::vector<Element> _elems;
    std::atomic<int64_t> _read, _write;

public:
    Ring(size_t capacity)
        : _elems(capacity), _read(0), _write(0) {}

    Element * get_for_read() {
        Element * ret = &_elems[ _read.load() % _elems.size() ];
        if (!ret->lock_for_read()) // if success, the object belongs to the caller thread as reader
            return NULL;
        _read.fetch_add(1); // success! incr _read index 
        return ret;
    }
    Element * get_for_write() {
        Element * ret = &_elems[ _write.load() % _elems.size() ];
        if (!ret->lock_for_write())// if success, the object belongs to the caller thread as writer
            return NULL;
        _write.fetch_add(1); // success! incr _write index
        return ret;
    }
    void release_read(Element* e) { e->unlock_read();}
    void release_write(Element* e) { e->unlock_write();}
};

int main()
{

    const int capacity = 10; // easy to process modulo[![enter image description here][1]][1]

    std::atomic<bool> stop=false;

    Ring ring(capacity);

    std::function<void()> writer_job = [&]()
    {
        std::cout << "writer starting" << std::endl;
        Element * e;
        while (!stop)
        {
            if (!(e = ring.get_for_write())) 
                continue;
            // do some real writer job ...
            ring.release_write(e);
        }
    };
    std::function<void()> reader_job = [&]()
    {
        std::cout << "reader starting" << std::endl;
        Element * e;
        while (!stop)
        {
            if (!(e = ring.get_for_read())) 
                continue;
            // do some real reader job ...
            ring.release_read(e);
        }
    };

    int nb_writers = 1;
    int nb_readers = 2;

    std::vector<std::thread> threads;
    threads.reserve(nb_writers + nb_readers);

    std::cout << "adding writers" << std::endl;
    while (nb_writers--)
        threads.push_back(std::thread(writer_job));

    std::cout << "adding readers" << std::endl; 
    while (nb_readers--)
        threads.push_back(std::thread(reader_job));

    // wait user key press, halt in debugger after 1 or 2 seconds
    // in order to reproduce problem and watch ring
    std::cin.get();

    stop = true;

    std::cout << "waiting all threads...\n";
    for (auto & th : threads)
        th.join();

    std::cout << "end" << std::endl;
}

这个&#34;观看调试器screeshot&#34;运行1秒后暂停程序。如您所见,_read指向标记为state::Unused的元素8,因此除了编写器之外,没有转换可以解除此读取器的此状态,但_write索引指向元素0州state::Ready

screenshot

我的问题:我错过了什么?在结构上我确信序列是正确的,但我错过了一些原子技巧......

测试结果:rhel5 / gcc 4.1.2,rhel 7 / gcc 4.8,win10 / ms visual 2015,win10 / mingw

2 个答案:

答案 0 :(得分:2)

在两个共享计数器_read和_write的增量周围没有原子部分。 这看起来很糟糕,你可以切换另一个没有意义的元素。

想象一下这个场景, 1位读者R1和1位作家W正在愉快地合作。

Reader 2执行:Element * ret =&amp; _elems [_read.load()%_ elems.size()]; 并被推离cpu。

现在R1和W仍在一起玩,所以_read和_write的位置现在是任意的w.r.t. R2指向的元素ret。

现在在某些时候R2已经被安排了,并且碰巧* ret_是可读的(再次可能,R1和W绕过该块几次)。

哎呀,如你所见,我们会读它,并增加“_read”,但_read与_ret无关。这会产生一些漏洞,这些漏洞是尚未读取但位于_read索引之下的元素。

因此,制作关键部分以确保_read / _write的增量在与实际锁定相同的语义步骤中完成。

答案 1 :(得分:2)

Yann's answer对于这个问题是正确的:你的线程可以创建&#34;漏洞&#34;如果在读/写锁和索引的增量之间存在延迟,则按顺序读取和写入元素。修复是验证索引在初始读取和增量之间没有变化,la:

class Element
{
    std::atomic<state> _state;
public:
    Element():_state(Unused){ }

    // a reader need to successfully make the transition Ready => LockForRead
    bool lock_for_read() {
        state s = Ready;
        return _state.compare_exchange_strong(s, LockForRead);
    }
    void abort_read() { _state = Ready; }
    void unlock_read() { state s = Unused; _state.store(s); }

    // a reader need to successfully make the transition Unused => LockForWrite
    bool lock_for_write() {
        state s = Unused;
        return _state.compare_exchange_strong(s, LockForWrite);
    }
    void abort_write() { _state = Unused; }
    void unlock_write() { state s = Ready;  _state.store(s); }
};

class Ring
{
    std::vector<Element> _elems;
    std::atomic<int64_t> _read, _write;

public:
    Ring(size_t capacity)
        : _elems(capacity), _read(0), _write(0) {}

    Element * get_for_read() {
        auto i = _read.load();
        Element * ret = &_elems[ i % _elems.size() ];
        if (ret->lock_for_read()) {
            // if success, the object belongs to the caller thread as reader
            if (_read.compare_exchange_strong(i, i + 1))
                return ret;
            // Woops, reading out of order.
            ret->abort_read();
        }
        return NULL;
    }
    Element * get_for_write() {
        auto i = _write.load();
        Element * ret = &_elems[ i % _elems.size() ];
        if (ret->lock_for_write()) {
            // if success, the object belongs to the caller thread as writer
            if (_write.compare_exchange_strong(i, i + 1))
                return ret;
            // Woops, writing out of order.
            ret->abort_write();
        }
        return NULL;
    }
    void release_read(Element* e) { e->unlock_read();}
    void release_write(Element* e) { e->unlock_write();}
};