我使用原子和循环缓冲区来实现多读写器线程,多写程序线程对象池。
很难调查,因为仪表代码会导致错误消失!
模型
生产者(或作家线程)向Element
请求Ring
以便准备'元素。终止时,编写器线程会更改元素状态,以便读者可以使用'它。之后,该元素再次可用于写作。
消费者(或读者线程)向Ring请求一个对象以便阅读'物体。
在'发布'对象,对象处于state::Ready
状态,例如可供读者线程使用。
如果没有可用的对象,它可能会失败,例如,Ring中的下一个空闲对象不在state::Unused
状态。
这两个课程Element
和Ring
Element
:
_state
成员从state::Unused
更改为state::LockForWrite
state::Ready
(它应该是唯一处理此元素的文件)_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
!
我的问题:我错过了什么?在结构上我确信序列是正确的,但我错过了一些原子技巧......
测试结果:rhel5 / gcc 4.1.2,rhel 7 / gcc 4.8,win10 / ms visual 2015,win10 / mingw答案 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();}
};