如何从更基本的同步原语创建多读/单写锁?

我们发现,在代码中有几个地方,受互斥锁保护的数据的并发读取非常常见,而写操作则很少。我们的测量结果似乎表明,使用简单的互斥锁严重阻碍了代码读取该数据的性能。所以我们需要的是一个多读/单写互斥体。我知道这可以建立在更简单的原语之上,但在我尝试这一点之前,我宁愿询问现有的知识:

用更简单的同步原语构建多读/单写锁定的认可方法是什么?

我确实有办法做到这一点,但我希望答案不受我(可能是错误的)想法的影响。(注意:我期望的是一个如何实现的解释,可能是伪代码,而不是完整的实现。我当然可以自己编写代码。)

注意事项:

  • 这需要有合理的性能。(我的想法是每个访问需要两个锁定/解锁操作。现在这可能不够好,但需要很多操作似乎不合理。)

  • 通常,读取的数量更多,但写入比读取更重要,对性能更敏感。读者决不能让作家挨饿

  • 我们被困在一个相当旧的嵌入式平台(VxWorks 5.5的专有变体)、一个相当旧的编译器(GCC 4.1.2)和boost 1.52上——除了大部分依赖POSIX的boost部分,因为POSIX没有在该平台上完全实现。可用的锁定原语基本上是几种信号量(二进制、计数等),在此基础上我们已经创建了互斥体、条件变量和监视器

  • 这是IA32,单核

乍一看,我认为我认识到这个答案和亚历山大·特列霍夫介绍的算法是一样的。但经过研究,我认为它是有缺陷的。两个写入程序可以同时等待m_exclusive_cond。当其中一个写入程序唤醒并获得独占锁时,它会将独占\u waiting\u blocked=false设置为打开解锁,从而将互斥锁设置为不一致的状态。在那之后,互斥锁可能会被冲洗掉

N2406首先提出了std::shared_mutex,它包含一个部分实现,下面用更新的语法重复该部分实现

类共享\u互斥
{
互斥互斥;
条件变量门1;
条件变量门2;
无符号状态;
静态常量无符号写入\u输入=1U<;(sizeof(无符号)*字符\u位-1);
静态常量未签名n\u读取器\u=~ write\u输入\u;
公众:
共享的互斥体():状态{(0){}
//专有权
无效锁();
bool try_lock();
无效解锁();
//共享所有权
void lock_shared();
bool try_lock_shared();
void unlock_shared();
};
//专有权
无效的
共享互斥体::锁()
{
唯一锁<互斥锁>lk(多锁);
while(输入状态和写入状态)
网关1。等待(lk);
状态124;=写入124;输入124;;
while(state_和n_readers_)
网关2。等待(lk);
}
布尔
共享互斥锁::try_lock()
{
唯一锁<互斥锁>lk(多锁,尝试锁);
if(lk.owns_lock()&state_==0)
{
状态=写入输入的状态;
返回true;
}
返回false;
}
无效的
共享互斥体::解锁()
{
{
锁定保护<互斥>u(mut);
状态=0;
}
gate1.通知所有人();
}
//共享所有权
无效的
共享互斥锁::锁共享()
{
唯一锁<互斥锁>lk(多锁);
while((state_&write_输入)| |(state_&n_读卡器)==n_读卡器)
网关1。等待(lk);
unsigned num_readers=(state_u和n_readers)+1;
州=n读者;
state|=num_读取器;
}
布尔
共享互斥锁::try_lock_shared()
{
唯一锁<互斥锁>lk(多锁,尝试锁);
unsigned num_readers=state_u&n_readers\uu;
if(lk.owns_lock()&!(state_&write_entered_)&num_readers!=n_readers_)
{
++num_阅读器;
州=n读者;
state|=num_读取器;
返回true;
}
返回false;
}
无效的
共享互斥锁::解锁共享
{
锁定保护<互斥>u(mut);
unsigned num_readers=(state_u&n_readers_u)-1;
州=n读者;
state|=num_读取器;
如果(输入状态和写入状态)
{
if(num_readers==0)
网关2。通知网关1();
}
其他的
{
if(num\u readers==n\u readers\u1)
网关1。通知网关1();
}
}

该算法源自Alexander Terekhov的旧新闻组帖子。它既不让读者也不让作家挨饿

有两个“门”gate1和gate2。读者和作者必须通过gate1,这样做可能会被阻止。一旦读卡器通过gate1,它就已经读取并锁定了互斥锁。只要拥有所有权的读卡器数量不超过上限,并且只要写入者没有通过gate1,读卡器就可以通过gate1

一次只能有一个编写器通过gate1。即使读者拥有所有权,作者也可以通过gate1。但是一旦通过了gate1,写入程序仍然没有所有权。它必须首先通过网关2。在拥有所有权的所有读卡器都放弃它之前,作者无法通过gate2。回想一下,新读者无法通过gate1\uuu,而编写者正在gate2\uu等待。当一个新的编写器在gate2\uu等待时,它也不能通过gate1\uu

读写器都被阻塞在gate1\uu上,要求(几乎)相同才能通过,这一特点使得该算法对读写器都公平,既不饥饿也不饥饿

互斥“状态”有意保留在一个单词中,以表明部分使用原子(作为优化)进行某些状态更改是可能的(即,对于无争议的“快速路径”)。然而,这里没有演示这种优化。一个例子是,如果写入线程可以自动地将状态从0更改为写入,那么他就可以获得锁,而不必阻塞甚至锁定/解锁mut。而unlock()可以通过原子存储实现。这里没有显示这些优化,因为它们比这个简单的描述听起来更难正确实现

发表评论