在以读取为主的多线程程序中,可以使用原子来减少锁定吗



最近,我发现自己经常遇到共享数据被读取很多,但很少被写入的情况,所以我开始怀疑是否有可能加快同步速度。

以下面的例子为例,多个线程偶尔会写入数据,一个线程频繁地读取数据,所有这些都与一个正常的互斥体同步。

#include <iostream>
#include <unistd.h>
#include <unordered_map>
#include <mutex>
#include <thread>
using namespace std;
unordered_map<int, int> someData({{1,10}});
mutex mu;
void writeData(){
while(true){
{
lock_guard<mutex> lock(mu);
int r = rand()%10;
someData[1] = r;
printf("data changed to %dn", r);
}
usleep(rand()%100000000 + 100000000);
}
}
void readData(){
while(true){
{
lock_guard<mutex> lock(mu);
for(auto &i:someData){
printf("%d:%dn", i.first, i.second);
}
}
usleep(100);
}
}
int main() {
thread writeT1(&writeData2);
thread writeT2(&writeData2);
thread readT(&readData2);
readT.join();
}

使用普通的锁机制,每次读取都需要一个锁,我想在大多数情况下加快到单个原子读取

unordered_map<int, int> someData({{1,10}});
mutex mu;
atomic_int dataVersion{0};
void writeData2(){
while(true){
{
lock_guard<mutex> lock(mu);
dataVersion.fetch_add(1, memory_order_acquire);
int r = rand()%10;
someData[1] = r;
printf("data changed to %dn", r);
}
usleep(rand()%100000000 + 100000000);
}
}
void readData2(){
mu.lock();
int versionCopy = dataVersion.load();
auto dataCopy = someData;
mu.unlock();
while(true){
if (versionCopy != dataVersion.load(memory_order_relaxed)){
lock_guard<mutex> lock(mu);
versionCopy = dataVersion.load(memory_order_relaxed);
dataCopy = someData;
}
else{
for(auto &i:dataCopy){
printf("%d:%dn", i.first, i.second);
}
usleep(100);
}
}
}

这里的数据类型unordered_map只是一个例子,它可以是任何类型,我不想寻找纯粹的无锁算法,因为这可能是另一回事。只是对于一个普通的基于锁的同步,在大多数操作都被读取的情况下,使用这样的技巧,在逻辑上可以吗?对此有任何既定的方法吗

[编辑]

  • 我知道共享互斥,但这并不是我所说的真正情况。首先,共享锁并不便宜,可能比普通互斥锁更贵,当然比原子锁更重;其次,在这个例子中,我展示了一个单一的阅读线程,它不能充分利用它
  • 我特别感兴趣的是锁定操作的成本。减少阻塞,关键部分当然是在真实案例中首先要考虑的,但我在这里并没有针对这一点
  • unordered_map数据类型只是一个例子,不寻找更适合特定任务的数据结构或无锁算法,数据类型可以是任何类型
  • 睡眠时间是为了证明读取的次数远远多于写入,以至于我们开始不那么关心if块中额外的锁定和复制时间

谢谢~

您将数据存储在一个无序映射中。unordered_map类对读者的并发访问有什么保证;作家。如果它对这种前景不满意,原子论者就不是你的朋友。

在大多数(每个?(操作系统中,在无争议的情况下,锁定基元本身由原子处理;只有在有争议时才恢复到内核。考虑到这一点,您最好在保持锁定的同时尽量减少代码量,因此您的第一个循环应该是:

int r = rand()%10;
mu.lock();
someData[1] = r;
mu.unlock();
printf("data changed to %dn", r);

我不知道你会如何修复读取端,但如果你选择了一个更友好的数据存储,你可以用同样的方式最大限度地减少对它的访问。

我将首先尝试描述我自己对您的想法的理解:

  1. 经常阅读,偶尔写作
  2. 锁很贵。。。如果要进行基准测试,请尝试std::shared_mutex或Slim Reader/Writer SRW Locks-仅限Windows,或其他Slim实现,这些实现通常使用一些廉价且乐观的(原子/旋转锁(机制,在没有冲突的情况下(大多数时候没有编写器(,这种机制几乎没有影响
  3. 你似乎不在乎你的副本有多旧/多新。这对于一些信息丰富的性能计数器来说是可以接受的,但我会三思而后行——这不是其他必须维护你的代码的人所期望甚至考虑的。后果可能是灾难性的
  4. 您只能访问锁下的可写数据,读取您创建的持有锁的副本。这意味着从简单的线程同步视图来看,您的方法是安全的,除了以上这一点(使用旧数据的读取器,多个读取器可以有不同的副本…值得吗?(

无论如何,在尝试提出自己的同步机制(通常很难正确做到这一点(之前,你应该先尝试进行基准测试,然后尝试找到其他人已经编写的更好的解决方案(瘦rw锁(。

EDIT:发现一些文章使用std::atomic:实现了concreate shared_mutex

代码项目:我们制作了一个速度快10倍的std::shared_mutex

Coliru测试在这里

相关内容

最新更新