首页 > 其他分享 >微服务设计原则——高性能:锁

微服务设计原则——高性能:锁

时间:2024-07-21 22:54:58浏览次数:12  
标签:加锁 无锁 原则 lock micro 高性能 分片 设计 struct

文章目录

1.锁的问题

高性能系统中使用锁,往往带来的坏处要大于好处。

并发编程中,锁带解决了安全问题,同时也带来了性能问题,因为锁让并发处理变成了串行操作,所以如无必要,尽量不要显式使用锁。

锁和并发,貌似有一种相克相生的关系。

为了避免严重的锁竞争导致性能的下降,有些场景采用了无锁化设计,特别是在底层框架上。无锁化主要有两种实现,无锁队列和无锁数据结构。

2.无锁

2.1 串行无锁

串行无锁最简单的实现方式可能就是单线程模型了,如 Redis 6.0 之前采用了这种方式。但是这种方式利用不了 CPU 多核的优势,所以在网络编程模型中,常用的是单 Reactor 多线程模型。

单 Reactor 多线程模型中,主线程负责处理 I/O 事件,并将读到的数据压入队列,工作线程则从队列中取出数据进行处理,多线程从队列获取数据时需要对队列加锁。如下图所示:

上图的模式可以改成串行无锁的形式,当 MainReactor accept 一个新连接之后从众多的 SubReactor 选取一个进行注册,通过创建一个 Queue 与 I/O 线程进行绑定,此后该连接的读写都在同一个队列和线程中执行,无需对队列加锁。这种模型叫主从 Reactor 多线程模型。

2.2 无锁数据结构

利用硬件支持的原子操作可以实现无锁的数据结构,很多语言都提供CAS原子操作(如 Go 中的 atomic 包和 C++11 中的 atomic 库),可以用于实现无锁数据结构,如无锁链表。

我们以一个简单的线程安全单链表的插入操作来看下无锁编程和普通加锁的区别。

template<typename T>
struct Node {
    Node(const T &value) : data(value) {}
    T data;
    Node *next = nullptr;
};

有锁链表 WithLockList:

template<typename T>
class WithLockList {
    mutex mtx;
    Node<T> *head;
public:
    void pushFront(const T &value) {
        auto *node = new Node<T>(value);
        lock_guard<mutex> lock(mtx); // (1)
        node->next = head;
        head = node;
    }
};

无锁链表 LockFreeList:

template<typename T>
class LockFreeList {
    atomic<Node<T> *> head;
public:
    void pushFront(const T &value) {
        auto *node = new Node<T>(value);
        node->next = head.load();
        while(!head.compare_exchange_weak(node->next, node)); // (2)
    }
};

从代码可以看出,在有锁版本中 (1) 进行了加锁。在无锁版本中,(2) 使用了原子 CAS 操作 compare_exchange_weak,该函数如果存储成功则返回 true,同时为了防止伪失败(即原始值等于期望值时也不一定存储成功,主要发生在缺少单条比较交换指令的硬件机器上),通常将 CAS 放在循环中。

下面对有锁和无锁版本进行简单的性能比较,分别执行 1000,000 次push操作。测试代码如下:

int main() {
    const int SIZE = 1000000;
    //有锁测试
    auto start = chrono::steady_clock::now();
    WithLockList<int> wlList;
    for(int i = 0; i < SIZE; ++i)
    {
        wlList.pushFront(i);
    }
    auto end = chrono::steady_clock::now();
    chrono::duration<double, std::micro> micro = end - start;
    cout << "with lock list costs micro:" << micro.count() << endl;

    //无锁测试
    start = chrono::steady_clock::now();
    LockFreeList<int> lfList;
    for(int i = 0; i < SIZE; ++i)
    {
        lfList.pushFront(i);
    }
    end = chrono::steady_clock::now();
    micro = end - start;
    cout << "free lock list costs micro:" << micro.count() << endl;

    return 0;
}

三次输出如下,可以看出无锁版本有锁版本性能高一些。

with lock list costs micro:548118
free lock list costs micro:491570
with lock list costs micro:556037
free lock list costs micro:476045
with lock list costs micro:557451
free lock list costs micro:481470

3.减少锁竞争

如果加锁无法避免,则可以采用分片的形式,减少对资源加锁的次数,这样也可以提高整体的性能。

比如 Golang 优秀的本地缓存组件 bigcachego-cachefreecache 都实现了分片功能,每个分片一把锁,采用分片存储的方式减少加锁的次数从而提高整体性能。

以一个简单的示例,通过对map[uint64]struct{}分片前后并发写入的对比,来看下减少锁竞争带来的性能提升。

var (
	num = 1000000
	m0  = make(map[int]struct{}, num)
	mu0 = sync.RWMutex{}
	m1  = make(map[int]struct{}, num)
	mu1 = sync.RWMutex{}
)

// ConWriteMapNoShard 不分片写入一个 map。
func ConWriteMapNoShard() {
	g := errgroup.Group{}
	for i := 0; i < num; i++ {
		g.Go(func() error {
			mu0.Lock()
			defer mu0.Unlock()
			m0[i] = struct{}{}
			return nil
		})
	}
	_ = g.Wait()
}

// ConWriteMapTwoShard 分片写入两个 map。
func ConWriteMapTwoShard() {
	g := errgroup.Group{}
	for i := 0; i < num; i++ {
		g.Go(func() error {
			if i&1 == 0 {
				mu0.Lock()
				defer mu0.Unlock()
				m0[i] = struct{}{}
				return nil
			}
			mu1.Lock()
			defer mu1.Unlock()
			m1[i] = struct{}{}
			return nil
		})
	}
	_ = g.Wait()
}

看下二者的性能差异:

func BenchmarkConWriteMapNoShard(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ConWriteMapNoShard()
	}
}
BenchmarkConWriteMapNoShard-12                 3         472063245 ns/op

func BenchmarkConWriteMapTwoShard(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ConWriteMapTwoShard()
	}
}
BenchmarkConWriteMapTwoShard-12                4         310588155 ns/op

可以看到,通过对分共享资源的分片处理,减少了锁竞争,能明显地提高程序的并发性能。可以预见的是,随着分片粒度地变小,性能差距会越来越大。当然,分片粒度不是越小越好。因为每一个分片都要配一把锁,那么会带来很多额外的不必要的开销。可以选择一个不太大的值,在性能和花销上寻找一个平衡。


参考文献

标签:加锁,无锁,原则,lock,micro,高性能,分片,设计,struct
From: https://blog.csdn.net/K346K346/article/details/140505890

相关文章

  • 一文带你读懂MLIR论文,理解MLIR设计准则.
    论文MLIR:ScalingCompilerInfrastructureforDomainSpecificComputationMLIR:针对特定领域计算扩展编译器基础设施文章目录论文MLIR:ScalingCompilerInfrastructureforDomainSpecificComputation1.论文下载2.TVM关于MLIR的讨论3.论文正文0.摘要1.导......
  • 【网页前端设计HTML】图片
    一、图片的简介任何网页都少不了图片,一个图文并茂的页面,可以使得用户体验更好。如果想让网站获得更多的流量,也需要从“图文并茂”这个角度挖掘一下。在HTML中,我们可以使用img标签来显示一张图片。对于img标签,只需要掌握它的3个属性:src、alt和title。<imgsrc=""alt=""ti......
  • 第九章面向对象程序设计
    两大编程思想面向过程功能上的封装,典型代表:C语言面次对象属性和行为上的封装:典型代表Java和Pathon步骤确定:面向过程类和对象类:由N多个对象抽取出‘像’的属性和行为从而归纳总结出来的一种类别在Pathon中一切皆对象点击查看代码示例9-1查看对象的数据类型a=10b......
  • 毕业设计&毕业项目:基于springboot+vue实现的在线音乐平台
    一、前言        在当今数字化时代,音乐已经成为人们生活中不可或缺的一部分。随着技术的飞速发展,构建一个用户友好、功能丰富的在线音乐平台成为了许多开发者和创业者的目标。本文将介绍如何使用SpringBoot作为后端框架,结合Vue.js作为前端框架,共同实现一个高效、可扩展的......
  • 算法设计与数据结构系列【超详细、超全面、小白可入,期末复习】持续更新中...
    算法设计与数据结构系列【超详细、超全面、小白可入,期末复习】持续更新中…24.07.21代码采用语言:Java1、位运算(BitwiseOperation)常见操作:与(&)、或(I)、非(~)、异或(^)移位运算:>>和<<分别为左移和右移>>>运算符:用0填充高位,>>用符号位填充高位,没有<<<运算符真值表ab~a~b......
  • 基于51单片机的zigbee餐桌呼叫系统设计
    基于51单片机的zigbee餐桌呼叫系统设计0、毕业设计选题原则说明(重点)1、项目简介1.1系统构成1.2系统功能1.3演示视频2、部分电路设计2.1STM32单片机核心板电路设计2.2按键电路设计2.3LCD1602液晶显示电路设计3.4、ZigBee通信模块电路设计3、部分代码展示3.1LCD16......
  • 面向对象设计的原则有哪些?
    1、单一责任原则(SingleResponsibilityPrinciple,SRP)一个类应该仅有一个引起它变化的原因。换句话说,一个类应该只有一个职责。这有助于保持类的内聚性,降低耦合度。2、开放-封闭原则(Open-ClosePrinciple,OCP)软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的。......
  • Java计算机毕业设计家庭装修套餐消费管理(开题报告+源码+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着人们生活水平的提高,家庭装修已成为现代家庭生活中不可或缺的一部分。然而,传统的家庭装修过程往往繁琐复杂,涉及多个环节和众多参与者,导致信息不对......
  • Java计算机毕业设计老来福平台(开题报告+源码+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着老龄化社会的加速到来,如何为老年人提供高质量、个性化的养老服务成为亟待解决的问题。传统的养老模式已难以满足老年人日益增长的需求,特别是在信......
  • Java计算机毕业设计浪漫屋婚纱影楼管理系统(开题报告+源码+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着婚礼文化的日益丰富与个性化需求的不断增长,婚纱影楼行业迎来了前所未有的发展机遇与挑战。传统的人工管理模式已难以满足高效、精准、个性化的服......