我不确定我的术语是否正确但是这里有 - 我有这个函数,多个线程用来写数据(在评论中使用伪代码来说明我想要的东西)
//these are initiated in the constructor
int* data;
std::atomic<size_t> size;
void write(int value) {
//wait here while "read_lock"
//set "write_lock" to "write_lock" + 1
auto slot = size.fetch_add(1, std::memory_order_acquire);
data[slot] = value;
//set "write_lock" to "write_lock" - 1
}
写入的顺序并不重要,我需要的是每次写入都要转到一个独特的插槽
每隔一段时间,我需要一个线程来使用此函数读取数据
int* read() {
//set "read_lock" to true
//wait here while "write_lock"
int* ret = data;
data = new int[capacity];
size = 0;
//set "read_lock" to false
return ret;
}
因此它基本上交换缓冲区并返回旧缓冲区(我已删除容量逻辑以缩短代码段)
理论上,这应该导致2种操作场景:
1 - 只是一堆写入容器的线程
2 - 当某个线程执行读取功能时,所有新写入器都必须等待,读取器将等待所有现有写入完成,然后它将执行读取逻辑,方案1可以继续。
问题部分是我不知道用于锁的障碍是什么 -
螺旋锁是浪费的,因为有很多像这样的容器,它们都需要cpu循环
我不知道如何应用std :: mutex,因为我只希望写入函数在触发读取函数时处于关键部分。将整个写入函数包含在互斥锁中会导致操作方案1不必要地减速。
那么这里的最佳解决方案是什么?
答案 0 :(得分:2)
如果您具有C++14
功能,则可以使用std::shared_timed_mutex分隔读者和作者。在这种情况下,您似乎需要为您的编写者线程共享访问(允许其他编写器线程同时)和您的读者线程唯一访问(踢出所有其他线程) )。
所以这样的事情可能就是你所需要的:
class MyClass
{
public:
using mutex_type = std::shared_timed_mutex;
using shared_lock = std::shared_lock<mutex_type>;
using unique_lock = std::unique_lock<mutex_type>;
private:
mutable mutex_type mtx;
public:
// All updater threads can operate at the same time
auto lock_for_updates() const
{
return shared_lock(mtx);
}
// Reader threads need to kick all the updater threads out
auto lock_for_reading() const
{
return unique_lock(mtx);
}
};
// many threads can call this
void do_writing_work(std::shared_ptr<MyClass> sptr)
{
auto lock = sptr->lock_for_updates();
// update the data here
}
// access the data from one thread only
void do_reading_work(std::shared_ptr<MyClass> sptr)
{
auto lock = sptr->lock_for_reading();
// read the data here
}
shared_lock允许其他线程同时获得shared_lock但阻止unique_lock获得同时访问权限。当读者线程试图获得unique_lock所有shared_lock时,unique_lock将获得独占控制权。
答案 1 :(得分:-1)
您也可以使用常规互斥锁和条件变量而不是共享来执行此操作。据说shared_mutex
有更高的开销,所以我不确定哪个更快。有了Gallik的解决方案,您可能需要付费才能在每次write
通话时锁定共享互斥锁;我从你的帖子中得到的印象是write
被称为方式而不是阅读,所以这可能是不可取的。
int* data; // initialized somewhere
std::atomic<size_t> size = 0;
std::atomic<bool> reading = false;
std::atomic<int> num_writers = 0;
std::mutex entering;
std::mutex leaving;
std::condition_variable cv;
void write(int x) {
++num_writers;
if (reading) {
--num_writers;
if (num_writers == 0)
{
std::lock_guard l(leaving);
cv.notify_one();
}
{ std::lock_guard l(entering); }
++num_writers;
}
auto slot = size.fetch_add(1, std::memory_order_acquire);
data[slot] = x;
--num_writers;
if (reading && num_writers == 0)
{
std::lock_guard l(leaving);
cv.notify_one();
}
}
int* read() {
int* other_data = new int[capacity];
{
std::unique_lock enter_lock(entering);
reading = true;
std::unique_lock leave_lock(leaving);
cv.wait(leave_lock, [] () { return num_writers == 0; });
swap(data, other_data);
size = 0;
reading = false;
}
return other_data;
}
这有点复杂,花了我一些时间来解决问题,但我认为这应该很有用。
在只发生写作的常见情况下,阅读总是错误的。所以你按常规做,并支付两个额外的原子增量和两个未加工的分支。因此,共同路径不需要锁定任何互斥锁,与涉及共享互斥锁的解决方案不同,这应该是昂贵的:http://permalink.gmane.org/gmane.comp.lib.boost.devel/211180。
现在,假设读取被调用。首先发生昂贵,缓慢的堆分配,同时写入继续不间断。接下来,获取进入锁定,其不会立即生效。现在,reading
设置为true。立即,任何新的写入调用都会进入第一个分支,并最终点击他们无法获取的进入锁定(因为它已经被占用),然后这些线程就会进入休眠状态。
与此同时,读取线程现在正在等待写入者的数量为0.如果我们很幸运,这实际上可以立即通过。但是,如果在递增和递减num_writers之间的两个位置中的任何一个中都有写入线程,则它不会。每次写入线程递减num_writers
时,它会检查它是否已将该数字减少为零,并且当它执行时它将发出条件变量的信号。因为num_writers
是原子会阻止各种重新排序的恶作剧,所以保证最后一个线程会看到num_writers == 0
;它也可以被多次通知,但这没关系,不会导致不良行为。
一旦发出该条件变量的信号,就表明所有的编写器都被捕获在第一个分支中或者修改了数组。因此,读取线程现在可以安全地交换数据,然后解锁所有内容,然后返回它所需的内容。
如前所述,在典型的操作中,没有锁,只有增量和未分支。即使读取确实发生,读取线程将有一个锁定和一个条件变量等待,而典型的写入线程将有一个锁定/解锁互斥锁并且全部(一个或少数个)写线程,也会执行条件变量通知)。