当前位置: 首页 > news >正文

golang中三种线程安全的MAP

一、map 是什么

map 是 Go 中用于存储 key-value 关系数据的数据结构,类似 C++ 中的 map,Python 中的 dict。Go 中 map 的使用很简单,但是对于初学者,经常会犯两个错误:没有初始化,并发读写。

1、未初始化的 map 都是 nil,直接赋值会报 panic。map 作为结构体成员的时候,很容易忘记对它的初始化。

2、并发读写是我们使用 map 中很常见的一个错误。多个协程并发读写同一个 key 的时候,会出现冲突,导致 panic。

Go 内置的 map 类型并没有对并发场景场景进行优化,但是并发场景又很常见,如何实现线程安全(并发安全)的 map就很重要了 

二、三种线程安全的 map

1、加读写锁(RWMutex)

这是最容易想到的一种方式。常见的 map 的操作有增删改查和遍历,这里面查和遍历是读操作,增删改是写操作,因此对查和遍历需要加读锁,对增删改需要加写锁。

以 map[int]int 为例,借助 RWMutex,具体的实现方式如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

type RWMap struct // 一个读写锁保护的线程安全的map

    sync.RWMutex // 读写锁保护下面的map字段

    map[int]int

}

// 新建一个RWMap

func NewRWMap(n int) *RWMap {

    return &RWMap{

        m: make(map[int]int, n),

    }

}

func (m *RWMap) Get(k int) (int, bool) { //从map中读取一个值

    m.RLock()

    defer m.RUnlock()

    v, existed := m.m[k] // 在锁的保护下从map中读取

    return v, existed

}

func (m *RWMap) Set(k int, v int) { // 设置一个键值对

    m.Lock()              // 锁保护

    defer m.Unlock()

    m.m[k] = v

}

func (m *RWMap) Delete(k int) { //删除一个键

    m.Lock()                   // 锁保护

    defer m.Unlock()

    delete(m.m, k)

}

func (m *RWMap) Len() int { // map的长度

    m.RLock()   // 锁保护

    defer m.RUnlock()

    return len(m.m)

}

func (m *RWMap) Each(f func(k, v int) bool) { // 遍历map

    m.RLock()             //遍历期间一直持有读锁

    defer m.RUnlock()

    for k, v := range m.m {

        if !f(k, v) {

            return

        }

    }

}

2、分片加锁

通过读写锁 RWMutex 实现的线程安全的 map,功能上已经完全满足了需要,但是面对高并发的场景,仅仅功能满足可不行,性能也得跟上。锁是性能下降的万恶之源之一。所以并发编程的原则就是尽可能减少锁的使用。当锁不得不用的时候,可以减小锁的粒度和持有的时间。

在第一种方法中,加锁的对象是整个 map,协程 A 对 map 中的 key 进行修改操作,会导致其它协程无法对其它 key 进行读写操作。一种解决思路是将这个 map 分成 n 块,每个块之间的读写操作都互不干扰,从而降低冲突的可能性。

Go 比较知名的分片 map 的实现是 orcaman/concurrent-map,它的定义如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

var SHARD_COUNT = 32

   

// 分成SHARD_COUNT个分片的map

type ConcurrentMap []*ConcurrentMapShared

   

// 通过RWMutex保护的线程安全的分片,包含一个map

type ConcurrentMapShared struct {

    items        map[string]interface{}

    sync.RWMutex // Read Write mutex, guards access to internal map.

}

   

// 创建并发map

func New() ConcurrentMap {

    m := make(ConcurrentMap, SHARD_COUNT)

    for i := 0; i < SHARD_COUNT; i++ {

        m[i] = &ConcurrentMapShared{items: make(map[string]interface{})}

    }

    return m

}

   

// 根据key计算分片索引

func (m ConcurrentMap) GetShard(key string) *ConcurrentMapShared {

    return m[uint(fnv32(key))%uint(SHARD_COUNT)]

}

ConcurrentMap 其实就是一个切片,切片的每个元素都是第一种方法中携带了读写锁的 map。

这里面 GetShard 方法就是用来计算每一个 key 应该分配到哪个分片上。

再来看一下 Set 和 Get 操作。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

func (m ConcurrentMap) Set(key string, value interface{}) {

    // 根据key计算出对应的分片

    shard := m.GetShard(key)

    shard.Lock() //对这个分片加锁,执行业务操作

    shard.items[key] = value

    shard.Unlock()

}

func (m ConcurrentMap) Get(key string) (interface{}, bool) {

    // 根据key计算出对应的分片

    shard := m.GetShard(key)

    shard.RLock()

    // 从这个分片读取key的值

    val, ok := shard.items[key]

    shard.RUnlock()

    return val, ok

}

Get 和 Set 方法类似,都是根据 key 用 GetShard 计算出分片索引,找到对应的 map 块,执行读写操作。

3、sync 中的 map

分片加锁的思路是将大块的数据切分成小块的数据,从而减少冲突导致锁阻塞的可能性。如果在一些特殊的场景下,将读写数据分开,是不是能在进一步提升性能呢?

在内置的 sync 包中(Go 1.9+)也有一个线程安全的 map,通过将读写分离的方式实现了某些特定场景下的性能提升。

其实在生产环境中,sync.map 用的很少,官方文档推荐的两种使用场景是:

a) when the entry for a given key is only ever written once but read many times, as in caches that only grow.
b) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys.

两种场景都比较苛刻,要么是一写多读,要么是各个协程操作的 key 集合没有交集(或者交集很少)。所以官方建议先对自己的场景做性能测评,如果确实能显著提高性能,再使用 sync.map。

sync.map 的整体思路就是用两个数据结构(只读的 read 和可写的 dirty)尽量将读写操作分开,来减少锁对性能的影响。

下面详细看下 sync.map 的定义和增删改查实现。

sync.map 数据结构定义

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

type Map struct {

    mu Mutex

    // 基本上你可以把它看成一个安全的只读的map

    // 它包含的元素其实也是通过原子操作更新的,但是已删除的entry就需要加锁操作了

    read atomic.Value // readOnly

    // 包含需要加锁才能访问的元素

    // 包括所有在read字段中但未被expunged(删除)的元素以及新加的元素

    dirty map[interface{}]*entry

    // 记录从read中读取miss的次数,一旦miss数和dirty长度一样了,就会把dirty提升为read,并把dirty置空

    misses int

}

type readOnly struct {

    m       map[interface{}]*entry

    amended bool // 当dirty中包含read没有的数据时为true,比如新增一条数据

}

// expunged是用来标识此项已经删掉的指针

// 当map中的一个项目被删除了,只是把它的值标记为expunged,以后才有机会真正删除此项

var expunged = unsafe.Pointer(new(interface{}))

// entry代表一个值

type entry struct {

    p unsafe.Pointer // *interface{}

}

Map 的定义中,read 字段通过 atomic.Values 存储被高频读的 readOnly 类型的数据。dirty 存储

Store 方法

Store 方法用来设置一个键值对,或者更新一个键值对。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

func (m *Map) Store(key, value interface{}) {

    read, _ := m.read.Load().(readOnly)

    // 如果read字段包含这个项,说明是更新,cas更新项目的值即可

    if e, ok := read.m[key]; ok && e.tryStore(&value) {

        return

    }

    // read中不存在,或者cas更新失败,就需要加锁访问dirty了

    m.mu.Lock()

    read, _ = m.read.Load().(readOnly)

    if e, ok := read.m[key]; ok { // 双检查,看看read是否已经存在了

        if e.unexpungeLocked() {

            // 此项目先前已经被删除了,需要添加到 dirty 中

            m.dirty[key] = e

        }

        e.storeLocked(&value) // 更新

    else if e, ok := m.dirty[key]; ok { // 如果dirty中有此项

        e.storeLocked(&value) // 直接更新

    else // 否则就是一个新的key

        if !read.amended { //如果dirty为nil

            // 需要创建dirty对象,并且标记read的amended为true,

            // 说明有元素它不包含而dirty包含

            m.dirtyLocked()

            m.read.Store(readOnly{m: read.m, amended: true})

        }

        m.dirty[key] = newEntry(value) //将新值增加到dirty对象中

    }

    m.mu.Unlock()

}

// tryStore利用 cas 操作来更新value。

// 更新之前会判断这个键值对有没有被打上删除的标记

func (e *entry) tryStore(i *interface{}) bool {

    for {

        p := atomic.LoadPointer(&e.p)

        if p == expunged {

            return false

        }

        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {

            return true

        }

    }

}

// 将值设置成 nil,表示没有被删除

func (e *entry) unexpungeLocked() (wasExpunged bool) {

    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)

}

// 通过复制 read 生成 dirty

func (m *Map) dirtyLocked() {

    if m.dirty != nil {

        return

    }

    read, _ := m.read.Load().(readOnly)

    m.dirty = make(map[interface{}]*entry, len(read.m))

    for k, e := range read.m {

        if !e.tryExpungeLocked() {

            m.dirty[k] = e

        }

    }

}

// 标记删除

func (e *entry) tryExpungeLocked() (isExpunged bool) {

    p := atomic.LoadPointer(&e.p)

    for p == nil {

        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {

            return true

        }

        p = atomic.LoadPointer(&e.p)

    }

    return p == expunged

}

第2-6行,通过 cas 进行键值对更新,更新成功直接返回。

第8-28行,通过互斥锁加锁来处理处理新增键值对和更新失败的场景(键值对被标记删除)。

第11行,再次检查 read 中是否已经存在要 Store 的 key(双检查是因为之前检查的时候没有加锁,中途可能有协程修改了 read)。

如果该键值对之前被标记删除,先将这个键值对写到 dirty 中,同时更新 read。

如果 dirty 中已经有这一项了,直接更新 read。

如果是一个新的 key。dirty 为空的情况下通过复制 read 创建 dirty,不为空的情况下直接更新 dirty。

Load 方法

Load 方法比较简单,先是从 read 中读数据,读不到,再通过互斥锁锁从 dirty 中读数据。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {

    // 首先从read处理

    read, _ := m.read.Load().(readOnly)

    e, ok := read.m[key]

    if !ok && read.amended { // 如果不存在并且dirty不为nil(有新的元素)

        m.mu.Lock()

        // 双检查,看看read中现在是否存在此key

        read, _ = m.read.Load().(readOnly)

        e, ok = read.m[key]

        if !ok && read.amended {//依然不存在,并且dirty不为nil

            e, ok = m.dirty[key]// 从dirty中读取

            // 不管dirty中存不存在,miss数都加1

            m.missLocked()

        }

        m.mu.Unlock()

    }

    if !ok {

        return nil, false

    }

    return e.load() //返回读取的对象,e既可能是从read中获得的,也可能是从dirty中获得的

}

func (m *Map) missLocked() {

    m.misses++ // misses计数加一

    if m.misses < len(m.dirty) { // 如果没达到阈值(dirty字段的长度),返回

        return

    }

    m.read.Store(readOnly{m: m.dirty}) //把dirty字段的内存提升为read字段

    m.dirty = nil // 清空dirty

    m.misses = 0  // misses数重置为0

}

这里需要注意的是,如果出现多次从 read 中读不到数据,得到 dirty 中读取的情况,就直接把 dirty 升级成 read,以提高 read 效率。

Delete 方法

下面是 Go1.13 中 Delete 的实现方式,如果 key 在 read 中,就将值置成 nil;如果在 dirty 中,直接删除 key。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

func (m *Map) Delete(key interface{}) {

    read, _ := m.read.Load().(readOnly)

    e, ok := read.m[key]

    if !ok && read.amended {

        m.mu.Lock()

        read, _ = m.read.Load().(readOnly)

        e, ok = read.m[key]

        if !ok && read.amended { // 说明可能在

            delete(m.dirty, key)

        }

        m.mu.Unlock()

    }

    if ok {

        e.delete()

    }

}

func (e *entry) delete() (hadValue bool) {

    for {

        p := atomic.LoadPointer(&e.p)

        if p == nil || p == expunged {

            return false

        }

        if atomic.CompareAndSwapPointer(&e.p, p, nil) {

            return true

        }

    }

}

补充说明一下,delete() 执行完之后,e.p 变成 nil,下次 Store 的时候,执行到 dirtyLocked() 这一步的时候,会被标记成 enpunged。因此在 read 中 nil 和 enpunged 都表示删除状态。

sync.map 总结

上面对源码粗略的梳理了一遍,最后在总结一下 sync.map 的实现思路:

  • 读写分离。读(更新)相关的操作尽量通过不加锁的 read 实现,写(新增)相关的操作通过 dirty 加锁实现。

  • 动态调整。新写入的 key 都只存在 dirty 中,如果 dirty 中的 key 被多次读取,dirty 就会上升成不需要加锁的 read。

  • 延迟删除。Delete 只是把被删除的 key 标记成 nil,新增 key-value 的时候,标记成 enpunged;dirty 上升成 read 的时候,标记删除的 key 被批量移出 map。这样的好处是 dirty 变成 read 之前,这些 key 都会命中 read,而 read 不需要加锁,无论是读还是更新,性能都很高。

总结了 sync.map 的设计思路后,我们就能理解官方文档推荐的 sync.map 的两种应用场景了。

三、总结

Go 内置的 map 使用起来很方便,但是在并发频繁的 Go 程序中很容易出现并发读写冲突导致的问题。本文介绍了三种常见的线程安全 map 的实现方式,分别是读写锁、分片锁和 sync.map。

较常使用的是前两种,而在特定的场景下,sync.map 的性能会有更优的表现。

相关文章:

golang中三种线程安全的MAP

一、map 是什么 map 是 Go 中用于存储 key-value 关系数据的数据结构&#xff0c;类似 C 中的 map&#xff0c;Python 中的 dict。Go 中 map 的使用很简单&#xff0c;但是对于初学者&#xff0c;经常会犯两个错误&#xff1a;没有初始化&#xff0c;并发读写。 1、未初始化的…...

C++笔试强训day16

目录 1.字符串替换 2.神奇数 3.DNA序列 1.字符串替换 链接 简单的遍历替换即可&#xff1a; class Solution { public:string formatString(string str, vector<char>& arg) {string ret;int k 0;for (int i 0; i < str.size(); i){if (str[i] %){ret arg…...

spsr 的恢复出错,导致 thumb 指令集的 it 条件运行指令运行异常,清晰的调试思路帮助快速解决问题

记一次调试过程 这是一个在 arm 架构上的 RTOS 上的调试过程。问题现象为使用 thumb 指令集的 libgcc 库的情况下&#xff0c;浮点运算随机出错。经过一番追踪调试&#xff0c;逐步缩小问题范围&#xff0c;最后定位问题&#xff0c;成功解决。 场景 在某款的国产 RTOS 上&a…...

mysql binlog 如何区分db

binlog不是InnoDB存储引擎特有的日志文件&#xff0c;是属于mysql server自己的日志文件。 提交事务的时候&#xff0c;同时会写入binlog 在MySQL中&#xff0c;Binary Log&#xff08;binlog&#xff09;记录了数据库更改操作的所有细节&#xff0c;对于实现数据复制、恢复以…...

ESP32 IDF linux下开发环境搭建

文章目录 介绍升级Python环境下载Python包配置编译环境及安装Python设置环境变量 ESPIDF环境搭建下载esp-idf 代码编译等待下载烧录成功查看串口打印 介绍 esp32 官方文档给的不是特别详细 参考多方资料 最后才完成开发 主要问题在于github下载的很慢本教程适用于ubuntu deban…...

光伏电站智能管理平台功能全面介绍

一、介绍 光伏电站智能管理平台专门为了光伏电站服务的融合了项目沟通、在线设计、施工管理、运维工单等多智能光伏管理系统&#xff0c;可以满足光伏电站建设前期沟通、中期建设和后续维护的一体化智能平台&#xff0c;同时通过组织架构对企业员工进行线上管理和数据同步&…...

SSL证书 购买流程

在购买SSL证书之前&#xff0c;需要知道一点相关的知识&#xff0c;通常包括以下几个环节&#xff1a; 一、确定需求 1、根据需要保护的域名数量&#xff0c;在以下三类中选择合适的证书类型&#xff1a; 单域名证书&#xff0c;只对一个域名&#xff08;例如abc.com&#x…...

C++|二叉搜索树

一、二叉搜索树的概念 二叉搜索树又称为二叉排序树&#xff0c;它或者是一颗空树&#xff0c;或者是具有以下性质的二叉树&#xff1a; 若它的左子树不为空&#xff0c;则左子树上所有节点的值小于根节点的值若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于根结…...

网页html版面分析-- BeauifulSoup(python 文档解析提取)

介绍 BeauifulSoup 是一个可以从HTML或XML 文件中提取数据的python库&#xff1b;它能通过转换器实现惯用的文档导航、查找、修改文档的方式。 BeauifulSoup是一个基于re开发的解析库&#xff0c;可以提供一些强大的解析功能&#xff1b;使用BeauifulSoup 能够提高提取数据的效…...

第五十八节 Java设计模式 - 适配器模式

Java设计模式 - 适配器模式 我们在现实生活中使用适配器很多。例如&#xff0c;我们使用存储卡适配器连接存储卡和计算机&#xff0c;因为计算机仅支持一种类型的存储卡&#xff0c;并且我们的卡与计算机不兼容。 适配器是两个不兼容实体之间的转换器。适配器模式是一种结构模…...

程序员的归宿。。

大家好&#xff0c;我是瑶琴呀。 相信每个进入职场的人都考虑过自己的职业生涯规划&#xff0c;在不同的年龄段可能面临不同挑战&#xff0c;这点对于 35 的人应该更为感同身受。 对于程序员来说&#xff0c;大部分人的职业道路主要是下面三种&#xff1a;第一条&#xff0c;…...

ROS服务器通信

目录 一、角色 二、流程 注意 三、例子描述 四、srv文件 编译配置文件 vscode配置 五、Server.cpp编写例子 编写CMakeList 六、观察server的效果 七、Client编写例子 编写CMakeList 八、观察Client的结果 九、Client优化&#xff08;动态输入&#xff09; 了解argc…...

双向带头循环链表(图解)

文章目录 头节点(哨兵位)双向循环结构头插尾插头删尾删在指定位置之前插入数据删除指定位置之前的数据销毁链表 全部代码结语 单链表地址 头节点(哨兵位) 什么是头节点呢?头节点也叫哨兵节点,他在链表中进行不了任何操作,只是用来放哨用的,在单链表中我们当我们尾插的时候我们…...

富文本编辑器 iOS

https://gitee.com/klkxxy/WGEditor-mobile#wgeditor-mobile 采用iOS系统浏览器做的一款富文本编辑器工具。 原理就是使用WKWebView加载一个本地的一个html文件&#xff0c;从而达到编辑器功能的效果&#xff01; 由于浏览器的一些特性等&#xff0c;富文本编辑器手机端很难做…...

【OceanBase诊断调优】—— checksum error ret=-4103 问题排查

适用版本 OceanBase 数据库所有版本。 什么是 checksum data checksum&#xff1a;一个 SSTable 中所有宏块内存二进制计算出来的 checksum 值。反映了宏块中的数据和数据分布情况。如果宏块中数据一致但是数据分布不一致&#xff0c;计算出来的 checksum 也不相等。 column…...

融合Transformer与CNN,实现各任务性能巅峰,可训练参数减少80%

论文er看过来&#xff0c;今天给各位推荐一个热门创新方向&#xff1a;CNNTransformer。 众所周知&#xff0c;CNN通过多层卷积自动学习空间层级特征&#xff0c;能够有效提取图像局部特征。而Transformer通过自注意力机制全局建模&#xff0c;能够有效处理长距离依赖关系。 …...

K8s 多租户管理

一、K8s 多租户管理 多租户是指在同一集群中隔离多个用户或团队&#xff0c;以避免他们之间的资源冲突和误操作。在K8s中&#xff0c;多租户管理的核心目标是在保证安全性的同时&#xff0c;提高资源利用率和运营效率。 在K8s中&#xff0c;该操作可以通过命名空间&#xff0…...

Java面试题:Synchronized和Lock的对比

Synchronized和Lock对比 语法层面 Synchronized是关键字,源码在jvm中,用c语言实现 使用时,退出同步代码块时会自动释放 Lock是接口,源码由jdk提供,用java语言实现 使用时,需要手动调用unlock方法进行释放 功能层面 都属于悲观锁,具备基本的互斥,同步,锁重入功能 但Lock…...

VPN方案和特点

VPN方案和特点 VPN&#xff0c;或者称为虚拟专用网络&#xff0c;是一种保护你的在线安全和隐私的技术。它可以创建一个加密的连接&#xff0c;使你的在线活动对其他人不可见。以下是一些常见的VPN协议和它们的特点&#xff1a; 开放VPN (OpenVPN)&#xff1a;这是一种极为可…...

力扣HOT100 - 84. 柱状图中最大的矩形

解题思路&#xff1a; 单调栈 对于一个高度height[ i ]&#xff0c;找左右两边均严格小于它的值。 class Solution {public int largestRectangleArea(int[] heights) {int n heights.length;int[] left new int[n];int[] right new int[n];Deque<Integer> mono_st…...

Java数据校验:确保数据完整性和正确性

在软件开发中&#xff0c;数据校验是确保应用程序数据完整性和正确性的关键步骤。Java 提供了多种方式来实现数据校验&#xff0c;从简单的条件检查到复杂的框架支持。在这篇博客中&#xff0c;我们将探讨 Java 中数据校验的重要性、常用的校验注解以及如何整合校验框架来提高代…...

前端八股之JS的原型链

1.原型的定义 每一个对象从被创建开始就和另一个对象关联&#xff0c;从另一个对象上继承其属性&#xff0c;这个另一个对象就是 原型。 当访问一个对象的属性时&#xff0c;先在对象的本身找&#xff0c;找不到就去对象的原型上找&#xff0c;如果还是找不到&#xff0c;就去…...

Linux编程:2、进程基础知识

一、进程基本概念 1、进程与程序的区别 程序&#xff1a;静态的可执行文件&#xff08;如电脑中的vs2022安装程序&#xff09;。进程&#xff1a;程序的动态执行过程&#xff08;如启动后的vs2022实例&#xff09;&#xff0c;是操作系统分配资源的单位&#xff08;如 CPU 时…...

Docke启动Ktransformers部署Qwen3MOE模型实战与性能测试

docker运行Ktransformers部署Qwen3MOE模型实战及 性能测试 最开始拉取ktransformers:v0.3.1-AVX512版本&#xff0c;发现无论如何都启动不了大模型&#xff0c;后来发现是cpu不支持avx512指令集。 由于本地cpu不支持amx指令集&#xff0c;因此下载avx2版本镜像&#xff1a; …...

Python爬虫实战:研究Hyper 相关技术

一、项目概述 本项目展示了如何结合 Python 的异步编程技术与 Hyper 框架开发一个高性能、可扩展的网络爬虫系统。该系统不仅能够高效地爬取网页内容,还提供了 RESTful API 接口,方便用户通过 API 控制爬虫的运行状态和获取爬取结果。 二、系统架构设计 1. 整体架构 系统采…...

Python训练营-Day22-Titanic - Machine Learning from Disaster

Description linkkeyboard_arrow_up &#x1f44b;&#x1f6f3;️ Ahoy, welcome to Kaggle! You’re in the right place. This is the legendary Titanic ML competition – the best, first challenge for you to dive into ML competitions and familiarize yourself w…...

DeepSeek 赋能智能养老:情感陪伴机器人的温暖革新

目录 一、引言二、智能养老情感陪伴机器人的市场现状与需求2.1 市场现状2.2 老年人情感陪伴需求分析 三、DeepSeek 技术详解3.1 DeepSeek 的技术特点3.2 与其他类似技术的对比优势 四、DeepSeek 在智能养老情感陪伴机器人中的具体应用4.1 自然语言处理与对话交互4.2 情感识别与…...

【软件工具】批量OCR指定区域图片自动识别内容重命名软件使用教程及注意事项

批量OCR指定区域图片自动识别内容重命名软件使用教程及注意事项 1、操作步骤1-5&#xff1a; 安装与启动&#xff1a;安装成功后&#xff0c;在桌面或开始菜单找到软件图标&#xff0c;双击启动。 导入图片&#xff1a;进入软件主界面&#xff0c;点击 “导入图片” 按钮&a…...

前端杂货铺——TodoList

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…...

【PCIe总线】 -- PCI、PCIe相关实现

PCI、PCIe相关概念和知识点 【PCIe总线】-- PCI、PCIe基础知识点整理 【PCIe】非常适合初学的pcie博客(PCIe知识整理) PCIe具体实现 【PCIe】如何获取PCIe的BAR空间大小&#xff1f;...