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

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日

相关文章

  • idea设置转大写快捷键

    以下是“IDEA设置转大写快捷键的完整攻略”的详细讲解,过程中包含两个示例说明的标准Markdown格式文本: IDEA设置转大写快捷键的完整攻略 在IDEA中我们可以设置快捷键来实现一些常用的操作。本文将介绍如何设置快捷键来实现将选中的文本转为大写的操作。 1. 打开设置窗口 要设置快捷键,我们需要打开IDEA的设置窗口。我们可以通过以下两种方式打开设置口…

    other 2023年5月10日
    00
  • idea怎么设置代理

    IDEA怎么设置代理 在使用IntelliJ IDEA这样的开发工具时,我们可能需要通过代理服务器来访问外网资源。那么,IDEA如何设置代理呢?下面是具体操作步骤。 设置HTTP代理 我们可以通过以下步骤设置HTTP代理: 打开IDEA,并选择菜单栏中的”File” -> “Settings”。 在弹出的”Settings”对话框中,选择”Appear…

    其他 2023年3月28日
    00
  • scala的unit

    以下是关于“Scala的Unit”的完整攻略: 什么是Unit 在Scala中,Unit是一个特殊的类型,表示不返回任何有用的值。类似于Java中的void,但是Unit是一个真正的类型,而不是一个关键字。 在Scala中,如果一个函数不返回任何有用的值,可以将返回类型设置为Unit。例如: def printHello(): Unit = { printl…

    other 2023年5月7日
    00
  • 在Python中使用gRPC的方法示例

    那么让我们开始“在Python中使用gRPC的方法示例”的完整攻略。 什么是gRPC gRPC是一个快速、高效、开源和通用的远程过程调用(RPC)框架。它最初由Google开发,支持多种编程语言。 gRPC使用ProtoBuf作为默认的数据序列化机制,这使得它可以高效地跨语言和平台之间进行通信。 gRPC的工作原理 gRPC使用Protocol Buffer…

    other 2023年6月27日
    00
  • DELL电脑大小写切换问题(窃取焦点)的解决办法

    DELL电脑大小写切换问题(窃取焦点)的解决办法攻略 问题描述 在使用DELL电脑时,有时会遇到大小写切换问题,即键盘在输入时会窃取焦点,导致大小写切换失败。这可能会给用户带来不便和困扰。 解决办法 为了解决这个问题,可以尝试以下两种方法: 方法一:禁用快捷键 打开“控制面板”。 在控制面板中,找到并点击“区域和语言”选项。 在“区域和语言”窗口中,点击“键…

    other 2023年8月16日
    00
  • vi/vim编辑、搜索、查找、定位

    使用vi/vim编辑、搜索、查找、定位 Vi和Vim是Unix和类Unix操作系统中最常用的文本编辑器之一。它们通常被用来编辑代码,但也可以用来编辑任何形式的文本文件。在这篇文章中,我们将介绍如何使用Vi/Vim进行编辑、搜索、查找和定位。 编辑文件 要使用Vi/Vim编辑文件,请使用以下命令: vi filename 这将打开一个Vi编辑器,其中filen…

    其他 2023年3月28日
    00
  • java 多线程死锁详解及简单实例

    Java多线程死锁详解及简单实例 定义 多线程死锁指的是两个或者多个线程在等待对方释放所持有的锁,从而进入了死锁状态,无法继续执行,也无法退出。 死锁产生的条件 多线程死锁产生的条件如下: 互斥:至少有一个资源是被独占的,如一个文件、一张表或一个锁等。 持有和等待:至少有一个进程正持有一个资源,并等待其他的资源。 非抢占性:资源不能被抢占,只有持有资源的进程…

    other 2023年6月27日
    00
  • Win10系统下去掉右键新建菜单中bmp图像选项的操作步骤

    以下是Win10系统下去掉右键新建菜单中bmp图像选项的操作步骤: 步骤一:打开注册表编辑器 为了能够操作注册表,我们需要打开注册表编辑器。具体操作步骤如下:1.按下“Win+R”组合键,打开运行窗口;2.在运行窗口中输入“regedit”,然后点击“确定”按钮。 步骤二:定位菜单项 接下来,我们需要找到“bmp”菜单项的命令位置,具体操作步骤如下:1.在注…

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