Go语言中map使用和并发安全详解

yizhihongxing

Go语言中map使用和并发安全详解

概述

在Go语言中,map是一种集合类型,它可以关联一个键和一个值。map是一种引用类型,可以使用 make 函数来创建。map 的底层实现是 hash 表,因此 map 的键是无序的,但是在迭代过程中,Go语言会自动对其进行排序。

map 的基本使用方法是:使用键访问值,如果键不存在,则会返回初始值。map 与 slice、struct 一样,可以使用 range 迭代器。同时,map 也支持删除元素,可以使用 delete 函数将指定元素从 map 中移除。

同时,由于 map 在并发条件下并不是线程安全的,因此在并发场景下使用 map 时需要使用加锁机制。

在本篇文章中,我们将详细讲解 Go 语言中 map 的基本使用方法以及在并发场景下如何保证 map 的安全。

map 实现与基本使用

创建 map 的语法为 make(map[keyType]valueType)。其中,keyType 是键的类型,valueType 是值的类型,例如:

// 创建键为 string 类型,值为 int 类型的 map
m := make(map[string]int)

向 map 中添加元素同样简单:

// 向 map 中添加元素
m["one"] = 1
m["two"] = 2

通过键访问值:

// 通过键访问值
fmt.Println(m["one"])
fmt.Println(m["two"])

输出:

1
2

当访问一个不存在的键时,会返回 map 中值类型的初始值:

fmt.Println(m["three"])

输出:

0

可以使用 format 包中提供的 %v 格式化输出 map 的元素:

// 使用迭代器遍历 map
for k, v := range m {
  fmt.Printf("key=%v, value=%v\n", k, v)
}

输出:

key=one, value=1
key=two, value=2

删除元素:

delete(m, "one")

map 也支持长度操作:

len(m)

map 的值可以是任意类型,它还可以包含另一个 map,不过 map 不可以自己包含自己,否则会引起死循环。

并发安全

由于 map 在并发条件下并不是线程安全的,因此在并发场景下使用 map 时需要使用加锁机制,以防止不同的 goroutine 并发访问同一个 map 导致的数据竞争和内存问题。

下面是一个使用 sync 包的方法:

import "sync"

// 创建互斥锁
var mutex sync.Mutex

// 安全的访问 map
func SafeAccessMap() {
  // 加锁
  mutex.Lock()
  defer mutex.Unlock()

  m["one"] = 1
  m["two"] = 2
  delete(m, "two")
}

也可以使用 sync 包中的 RWMutex 实现读写锁:

import "sync"

// 创建读写锁
var rwMutex sync.RWMutex

// 安全的访问 map
func SafeAccessMap() {
  // 写锁
  rwMutex.Lock()
  defer rwMutex.Unlock()

  m["one"] = 1
  m["two"] = 2
  delete(m, "two")
}

// 安全的迭代 map
func SafeIterateMap() {
  // 读锁
  rwMutex.RLock()
  defer rwMutex.RUnlock()

  for k, v := range m {
    fmt.Printf("key=%v, value=%v\n", k, v)
  }
}

以上是两个简单的示例,通过使用 mutex 或 RWMutex 实现了对 map 的访问和迭代的互斥操作,保证了 map 在并发访问时的安全性。

示例

示例 1:安全的并发计数器

下面是一个使用 sync.Mutex 实现的安全的并发计数器的示例:

import (
  "fmt"
  "sync"
  "time"
)

// 创建互斥锁
var mutex sync.Mutex

// 计数变量
var count int

// 安全的增加计数器的函数
func SafeIncrement() {
  // 加锁
  mutex.Lock()
  defer mutex.Unlock()

  count++
}

// 返回计数器的函数
func GetCount() int {
  // 加锁
  mutex.Lock()
  defer mutex.Unlock()

  return count
}

func main() {
  // 并发执行的次数
  num := 1000

  // 创建 WaitGroup
  var wg sync.WaitGroup

  // 增加计数器
  for i := 0; i < num; i++ {
    wg.Add(1)

    go func() {
      defer wg.Done()

      SafeIncrement()
    }()
  }

  // 等待所有 goroutine 执行完毕
  wg.Wait()

  // 输出计数器值
  fmt.Println("count:", GetCount())
}

输出:

count: 1000

示例 2:并发安全的 LRU Cache

下面是一个使用 sync.Mutex 实现的并发安全的 LRU Cache 的示例:

import (
  "container/list"
  "sync"
)

// 缓存项
type LRUCacheItem struct {
  key   interface{}
  value interface{}
}

// LRU Cache
type LRUCache struct {
  // 最大存储空间
  maxBytes int

  // 当前已使用空间
  usedBytes int

  // 存储缓存项的 hashmap
  items map[interface{}]*list.Element

  // 双向链表,存储缓存项
  list *list.List

  // 互斥锁
  mutex sync.Mutex
}

// 构造函数
func NewLRUCache(maxBytes int) *LRUCache {
  return &LRUCache{
    maxBytes: maxBytes,
    items:    make(map[interface{}]*list.Element),
    list:     list.New(),
  }
}

// 添加缓存项
func (c *LRUCache) Add(key, value interface{}, bytes int) {
  // 加锁
  c.mutex.Lock()
  defer c.mutex.Unlock()

  // 如果缓存项已存在,则更新值和链表顺序
  if ele, ok := c.items[key]; ok {
    c.usedBytes += bytes - ele.Value.(*LRUCacheItem).value.(int)
    ele.Value.(*LRUCacheItem).value = value
    c.list.MoveToFront(ele)
    return
  }

  // 创建缓存项
  item := &LRUCacheItem{key, value}

  // 在链表头部插入缓存项
  ele := c.list.PushFront(item)

  // 在 hashmap 中存储缓存项,并将 hashmap 中删除超出存储空间的第一个缓存项
  c.items[key] = ele
  c.usedBytes += bytes
  for c.usedBytes > c.maxBytes && c.list.Len() > 0 {
    c.removeOldestItem()
  }
}

// 获取缓存项
func (c *LRUCache) Get(key interface{}) interface{} {
  // 加锁
  c.mutex.Lock()
  defer c.mutex.Unlock()

  // 如果缓存项已存在,则更新链表顺序,并返回值
  if ele, ok := c.items[key]; ok {
    c.list.MoveToFront(ele)
    return ele.Value.(*LRUCacheItem).value
  }

  // 如果缓存项不存在,则返回 nil
  return nil
}

// 删除缓存项
func (c *LRUCache) Remove(key interface{}) {
  // 加锁
  c.mutex.Lock()
  defer c.mutex.Unlock()

  // 如果缓存项已存在,则删除缓存项
  if ele, ok := c.items[key]; ok {
    c.removeElement(ele)
  }
}

// 删除最老的缓存项
func (c *LRUCache) removeOldestItem() {
  ele := c.list.Back()
  if ele != nil {
    c.removeElement(ele)
  }
}

// 删除链表节点和 hashmap 中对应的缓存项
func (c *LRUCache) removeElement(ele *list.Element) {
  c.list.Remove(ele)

  item := ele.Value.(*LRUCacheItem)
  c.usedBytes -= item.value.(int)

  delete(c.items, item.key)
}

以上示例实现了一个线程安全的 LRU Cache。其基于哈希表和双向链表实现,可以进行常用的添加、获取和删除操作。同时,为了保证线程安全,使用 sync.Mutex 实现了对 Cache 的互斥访问。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Go语言中map使用和并发安全详解 - Python技术站

(0)
上一篇 2023年6月26日
下一篇 2023年6月26日

相关文章

  • js随机生成26个大小写字母

    当使用JavaScript编写代码时,可以使用Math.random()函数生成一个0到1之间的随机数。为了生成26个大小写字母,可以使用ASCII码表中的对应值。大写字母的ASCII码值范围是65到90,小写字母的ASCII码值范围是97到122。 以下是生成26个大小写字母的JavaScript代码示例: // 生成随机的大写字母 var upperca…

    other 2023年8月17日
    00
  • vim中进行列编辑的方法

    以下是关于“vim中进行列编辑的方法”的完整攻略,包括基本概念、解决方法、示例说明和注意事项。 基本概念 在Vim中,列编辑是指多行文本中对某一列进行编辑的操作。这种操作可以大大提高编辑效率特别是在需要对多行文本进行相同操作时。 解决方法 以下是Vim中进行列编辑的解决方法: 使用Ctrl + v进行列选择 使用Ctrl + v进入列选择模式。 使用上下键选…

    other 2023年5月7日
    00
  • 魔兽世界6.0熊德全面攻略 熊T最黑暗的时代来临

    魔兽世界6.0熊德全面攻略 熊T最黑暗的时代来临 简介 《魔兽世界》(World of Warcraft)游戏中的熊德(Bear Druid)是一种坦克职业,拥有强大的肉盾能力和高度的可持续性。随着6.0版本的到来,熊德面临着一系列的变化和挑战,需要玩家们通过深入研究和不断实践来掌握。 在本攻略中,我们将为您提供全面的熊德攻略,包括天赋选择、技能打法、装备选…

    other 2023年6月27日
    00
  • vue 2.0 开发实践总结之疑难篇

    Vue 2.0 开发实践总结之疑难篇的完整攻略 Vue 2.0 是一款流行的前端框架,但在实践中,我们可能会遇到一些疑难问题。本文将为您提供一份详细的 Vue 2.0 开发实践总结之疑难篇的完整攻略,包括两个示例说明。 示例1:如何在 Vue 中使用第三方库? 在 Vue 中使用第三方库可能会遇到一些问题,例如无法正确引入库、无法正确使用库等。可以按照以下步…

    other 2023年5月5日
    00
  • VSCODE添加open with code实现右键打开文件夹

    下面是“VSCODE添加open with code实现右键打开文件夹”的完整攻略: 步骤一:安装open with code插件 首先,我们需要安装一个叫做“open with code”的插件,该插件可以在右键菜单中添加一个“Open with Code”的选项。我们可以在VSCODE的插件市场中搜索“open with code”插件,然后进行安装。 …

    other 2023年6月27日
    00
  • Win10系统右键菜单没有”图形属性”和”图形选项”的还原方法图文教程

    下面是详细的攻略。 问题描述 在Win10系统中,右键菜单中的“图形属性”和“图形选项”不见了,无法直接进入显卡设置界面。这给用户带来了很大的不便,因此需要通过还原的方法让这两个选项重新出现在右键菜单中。 解决步骤 下面详细介绍还原右键菜单中的“图形属性”和“图形选项”的步骤: 步骤一:打开注册表 Win10系统的右键菜单中的“图形属性”和“图形选项”都是通…

    other 2023年6月27日
    00
  • C++编译器无法捕捉到的8种错误实例分析

    下面我将详细讲解“C++编译器无法捕捉到的8种错误实例分析”的完整攻略。 1. 程序逻辑错误 在编写程序时,有时候可能会出现程序逻辑错误,例如程序进入了不该进入的分支,或者是某个变量值不符合预期等情况。这些错误不会直接导致编译错误,但会导致程序运行出现异常。 示例代码: #include <iostream> using namespace st…

    other 2023年6月26日
    00
  • CentOS7.x卸载与安装MySQL5.7的操作过程及编码格式的修改方法

    下面是详细的“CentOS7.x卸载与安装MySQL5.7的操作过程及编码格式的修改方法”的完整攻略。 卸载MySQL 在卸载MySQL之前,应该先备份你的MySQL数据,以下是卸载MySQL的步骤: 停止MySQL服务 sudo systemctl stop mysqld.service 卸载MySQL软件 sudo yum remove mysql* 删…

    other 2023年6月20日
    00
合作推广
合作推广
分享本页
返回顶部