IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 【Go】并发编程之 sync:Mutex、RWMutex、WaitGroup、Once -> 正文阅读

[Java知识库]【Go】并发编程之 sync:Mutex、RWMutex、WaitGroup、Once


一、sync 包简介

Go 语言在 sync 包中提供了用于同步的一些基本原语,包括常见的 互斥锁 Mutex读写互斥锁 RWMutex 以及 OnceWaitGroup

Go sync包提供了:sync.Mutex,sync.RMutex,sync.Once,sync.Cond,sync.Waitgroup,sync.atomic等

sync 包含一个 Locker interface:

type Locker interface {
        Lock()
        Unlock()
} 

该接口只有两个方法,Lock() 和 Unlock()。整个sync包都是围绕该接口实现


二、互斥锁 Mutex

1. 引言 - 并发安全与锁

有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。

举个例子:

package main

import (
	"fmt"
	"sync"
)

var x int64
var wg sync.WaitGroup

func add() {
	for i := 0; i < 5000; i++ {
		x = x + 1
	}
	wg.Done()
}
func main() {
	wg.Add(2) //将计数器的值设为2,以下开启了两个协程
	go add()
	go add()
	wg.Wait()  //两个协程都结束之前一直阻塞,直到两个协程都运行结束才继续执行
	fmt.Println(x)
}

输出结果:

7773

上面的代码中我们开启了两个 goroutine 去累加变量 x 的值,这两个 goroutine 在访问和修改 x 变量的时候就会存在数据竞争,导致最后的结果与期待的不符。

为了避免这种情况,互斥锁是一种常用的 控制共享资源访问 的方法,它能够保证 同时只有一个 goroutine 可以访问共享资源。Go语言中使用 sync 包的 Mutex 类型来实现互斥锁。

2. 源码:Mutex 结构体

Go 语言中的互斥锁在 sync 包中,它由两个字段 state 和 sema 组成,state 表示当前互斥锁的状态,而 sema 真正用于控制锁状态的信号量,这两个加起来只占 8 个字节空间的结构体就表示了 Go 语言中的互斥锁。

type Mutex struct {
    state int32   //状态
    sema  uint32  //信号量
}

3. 互斥锁的三个状态

互斥锁的状态是用 int32 来表示的,但是锁的状态并不是互斥的,它的最低三位分别表示 mutexLockedmutexWokenmutexStarving,剩下的位置都用来表示当前有多少个 Goroutine 等待互斥锁被释放。

内部实现时把该变量(state int32)分成四份,用于记录Mutex的四种状态。下图展示Mutex的内存布局:
在这里插入图片描述

  • Locked: 表示该Mutex是否已被锁定,0:没有锁定 1:已被锁定。
  • Woken: 表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中。
  • Starving:表示该 Mutex 是否处理饥饿状态, 0:没有饥饿 1:饥饿状态,说明有协程阻塞了超过1ms。
  • Waiter: 表示阻塞等待该锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。

互斥锁在被创建出来时,所有的状态位的默认值都是 0,当互斥锁被锁定时 mutexLocked 就会被置成 1、当互斥锁被在正常模式下被唤醒时 mutexWoken 就会被被置成 1、mutexStarving 用于表示当前的互斥锁是否进入饥饿状态,最后的几位是在当前互斥锁上等待的 Goroutine 个数。

协程之间 抢锁 实际上是抢给 Locked 赋值的权利,能给Locked域置1,就说明抢锁成功。抢不到的话就阻塞等待Mutex.sema 信号量,一旦持有锁的协程解锁,等待的协程会依次被唤醒。

Woken 和 Starving 主要用于控制协程间的抢锁过程,后面再进行了解。

4. 互斥锁上的方法

互斥锁 Mutex 是 Locker 的一种具体实现,有两个方法:

func (m *Mutex) Lock()
func (m *Mutex) Unlock()

一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁,之后再重新争抢对互斥锁的锁定。

对一个未锁定的互斥锁解锁将会产生运行时错误。

实例 1:

使用互斥锁来修复引言中的代码的问题:

package main

import (
	"fmt"
	"sync"
)

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
	for i := 0; i < 5000; i++ {
		lock.Lock() // 加锁
		x = x + 1      //每次只有一个 goroutine 能对 x 进行 +1 操作
		lock.Unlock() // 解锁
	}
	wg.Done()
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

输出结果:

10000

使用互斥锁能够保证 同一时间有且只有一个 goroutine 进入临界区(临界区是加锁和解锁之间的区域),其他的 goroutine 则在等待锁;当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区,多个 goroutine 同时等待一个锁时,唤醒的策略是随机的。

实例 2:

package main

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

func main() {
	ch := make(chan struct{}, 2)

	var l sync.Mutex
	go func() { //开启一个 goroutine1
		l.Lock()
		defer l.Unlock()
		fmt.Println("goroutine1: 锁定大概 2s")
		time.Sleep(time.Second * 2)
		fmt.Println("goroutine1: 解锁了,可以开抢了!")
		ch <- struct{}{}
	}()

	go func() {
		fmt.Println("groutine2: 等待解锁")
		l.Lock() //goroutine1解锁之后,groutine2才可以重新锁定
		defer l.Unlock()
		fmt.Println("goroutine2: 我锁定了")
		ch <- struct{}{}
	}()

	// 等待 goroutine 执行结束
	for i := 0; i < 2; i++ {
		<-ch
	}
}

输出结果:

groutine2: 等待解锁
goroutine1: 锁定大概 2s
goroutine1: 解锁了,可以开抢了!
goroutine2: 我锁定了

三、读写锁 RWMutex

1. 引言 - 多读不互斥

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用 sync 包中的 RWMutex 类型。

读写互斥锁也是 Go 语言 sync 包为我们提供的接口之一。一个常见的服务对资源的读写比例会非常高,如果大多数的请求都是读请求,它们之间不会相互影响,那么我们为什么不能将对资源读和写操作分离呢?这也就是 RWMutex 读写互斥锁解决的问题,不限制对资源的并发读,但是读写写写操作无法并行执行(读读是可以并行的)。

读写锁分为两种:读锁和写锁。当一个 goroutine 获取读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。

2. 源码:RWMutex 结构体

读写互斥锁在 Go 语言中的实现是 RWMutex,其中不仅包含一个互斥锁,还持有两个信号量,分别用于写等待读和读等待写:

type RWMutex struct {
    w           Mutex
    writerSem   uint32
    readerSem   uint32
    readerCount int32   //当前正在执行的读操作的数量
    readerWait  int32   //当写操作被阻塞时等待的读操作个数
}

readerCount 存储了当前正在执行的读操作的数量,最后的 readerWait 表示当写操作被阻塞时等待的读操作个数。

3. 读写锁上的方法

读写锁是 针对读写操作的互斥锁(简单理解就是读和写无法同时进行;而且,根据我们的知识知道,写写也不能同时;只有读读可以同时),读写锁与互斥锁最大的不同就是可以分别对读、写进行锁定。一般用在大量读操作、少量写操作的情况:

func (rw *RWMutex) Lock()
func (rw *RWMutex) Unlock()

func (rw *RWMutex) RLock()
func (rw *RWMutex) RUnlock()

由于这里需要区分读写锁定,读写锁这样定义:

  • 读锁定(RLock),对读操作进行锁定

  • 读解锁(RUnlock),对读锁定进行解锁

  • 写锁定(Lock),对写操作进行锁定

  • 写解锁(Unlock),对写锁定进行解锁

不要混用锁定和解锁,如:Lock 和 RUnlock、RLock 和 Unlock。因为对未读锁定的读写锁进行读解锁或对未写锁定的读写锁进行写解锁将会引起运行时错误。

如何理解读写锁呢?

  • 同时只能有一个 goroutine 能够获得写锁定。

  • 同时可以有任意多个 gorouinte 获得读锁定。

  • 同时只能存在写锁定或读锁定(读和写互斥)。

也就是说,当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,需等它们读(读解锁)完再来写(写锁定)。

4. 实例

package main

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

var (
	x      int64
	wg     sync.WaitGroup
	lock   sync.Mutex
	rwlock sync.RWMutex
)

func write() {
	// lock.Lock()   // 加互斥锁
	rwlock.Lock() // 加写锁
	x = x + 1
	time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
	rwlock.Unlock()                   // 解写锁
	// lock.Unlock()                     // 解互斥锁
	wg.Done()
}

func read() {
	// lock.Lock()                  // 加互斥锁
	rwlock.RLock()               // 加读锁
	time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
	rwlock.RUnlock()             // 解读锁
	// lock.Unlock()                // 解互斥锁
	wg.Done()
}

func main() {
	start := time.Now()
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go read()
	}

	wg.Wait()
	end := time.Now()
	fmt.Println(end.Sub(start))
}

输出结果:

169.1675ms

5. 一个要点

需要注意的是 读写锁 非常适合 读多写少 的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。

读写锁的优势就是不限制大家同时读!!!这比 “顺序读” 快多了。


四、等待组 - sync.WaitGroup

1. 引言

经常会看到以下代码:

package main

import (
    "fmt"
    "time"
)

func main(){
    for i := 0; i < 100 ; i++{
        go fmt.Println(i)
    }
    time.Sleep(time.Second)
}

主协程为了等待 goroutine 都运行完毕,不得不在程序的末尾使用 time.Sleep() 来睡眠一段时间,等待其他线程充分运行。对于简单的代码,100个 for 循环可以在1秒之内运行完毕,time.Sleep() 也可以达到想要的效果。

但是对于实际生活的大多数场景来说,1秒是不够的,并且大部分时候我们都无法预知 for 循环内代码运行时间的长短。这时候就不能使用 time.Sleep() 来完成等待操作了。

可以考虑使用 管道 来完成上述操作:

func main() {
    c := make(chan bool, 100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            fmt.Println(i)
            c <- true
        }(i)
    }

    for i := 0; i < 100; i++ {
        <-c
    }
}

首先可以肯定的是使用管道是能达到我们的目的的,而且不但能达到目的,还能十分完美的达到目的。

但是管道在这里显得有些大材小用,因为它被设计出来不仅仅只是在这里用作简单的同步处理,在这里使用管道实际上是不合适的。而且假设我们有一万、十万甚至更多的for循环,也要申请同样数量大小的管道出来,对 内存 也是不小的开销。

对于这种情况,go语言中有一个其他的工具 sync.WaitGroup 能更加方便的帮助我们达到这个目的。

WaitGroup 对象维护着一个计数器,计数器的值可以增加和减少。最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。例如当我们启动了N 个并发任务时,就用 Add(n) 把计数器设置为 n ,每个任务完成时通过调用Done()方法将计数器减1,通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成,在任务完成之前,wait() 会阻塞代码的运行,直到计数器的值减为0。

使用WaitGroup 将上述代码可以修改为:

func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

这里首先把wg 计数设置为100, 每个for循环运行完毕都把计数器减一,主函数中使用Wait() 一直阻塞,直到wg为零——也就是所有的100个for循环都运行完毕。相对于使用管道来说,WaitGroup 轻巧了许多。

再看一个简单的实例:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func hello() {
	defer wg.Done()
	fmt.Println("Hello Goroutine!")
}
func main() {
	wg.Add(1)
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
	wg.Wait()
}

输出结果:

main goroutine done!
Hello Goroutine!

这个实例体现出了等待组的优势:

我们之前介绍 goroutine 的时候介绍过,如果主 goroutine 运行结束,那么所有的协程将陪葬。
如果我们要等创建的协程运行完的话,之前的方法是让主 goroutine “sleep()” 一会儿,但是 sleep() 是要指定时间的,而我们往往不能准确预估协程需要多长时间能运行完。所以用这个方法其实很笨拙。
现在我们有了 sync.WaitGroup 就方便太多啦!用 wg.Wait() 在主 goroutine 里等着,等所有 goroutine 全部运行完就直接返回。nice!

现在你对 sync.WaitGroup 有了一个大概的认知了吧?接下来详细介绍。

2. WaitGroup 的作用

Go语言中除了可以使用通道(channel)和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步,等待组可以保证在并发环境中 完成指定数量的任务

官方文档对 WaitGroup 的描述是:一个 WaitGroup 对象可以等待一组协程结束。使用方法是:

  • main协程通过调用 wg.Add(delta int) 设置 worker 协程的个数,然后创建 worker 协程;
  • worker协程执行结束以后,都要调用 wg.Done();
  • main协程调用 wg.Wait() 且被block,直到所有worker协程全部执行结束后返回。

3. 三个方法

以上提到了三个方法,可以改变 WaitGroup 对象的值:

在 sync.WaitGroup(等待组)类型中,每个 sync.WaitGroup 值在内部维护着一个计数,此计数的初始默认值为零。等待组内部拥有一个计数器,计数器的值可以通过方法调用实现计数器的增加和减少。当我们添加了 N 个并发任务进行工作时,就将等待组的计数器值增加 N。每个任务完成时,这个值减 1。同时,在另外一个 goroutine 中等待这个 等待组的计数器值为 0 时,表示所有任务已经完成

对于一个可寻址的 sync.WaitGroup 值 wg:

  • 我们可以使用方法调用 wg.Add(delta) 来增加值 wg 维护的计数。
  • 方法调用 wg.Done() 和 wg.Add(-1) 是完全等价的。(Done() 方法底层就是通过 Add(-1) 实现的。)
  • 计数器不能为负值:如果一个 wg.Add(delta) 或者 wg.Done() 调用将 wg 维护的计数更改成一个负数,一个恐慌将产生。所以我们 不能 使用 Add() 给 wg 设置一个负值或者使用Done() 把计数器减成负数。
  • 当一个协程调用了 wg.Wait() 时,如果此时 wg 维护的计数为零,则此 wg.Wait() 执行一个空操作(noop);否则(也就是计数为一个正整数),此协程将进入阻塞状态。当以后其它某个协程将此计数更改至 0 时(一般通过调用 wg.Done()),此协程将重新进入运行状态(即 wg.Wait() 将返回)。

总结:

WaitGroup 用于 等待一组 goroutine 结束,用法比较简单。它有三个方法:

func (wg *WaitGroup) Add(delta int)   //Add函数主要为WaitGroup的等待数+1或者+n
func (wg *WaitGroup) Done()           //Done函数调用的也是Add函数,主要用于-1操作
func (wg *WaitGroup) Wait()           //Wait函数是指阻塞当前协程,直到等待数归为0才继续向下执行

其中,Add 用来添加 goroutine 的个数。Done 执行一次数量减 1。Wait 用来等待结束。

4. 源码:WaitGroup 结构体

type WaitGroup struct {
  noCopy noCopy
  state1 [3]uint32
}

WaitGroup 结构体里有 noCopy 和 state1 两个字段。

编译代码时,go vet 工具会检查 noCopy 字段,避免 WaitGroup 对象被拷贝。

state1 字段比较秀,在逻辑上它包含了 worker计数器、waiter计数器和信号量。

5. 一个注意

需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针。

WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址:

func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go f(i, &wg)
    }
    wg.Wait()
}

// 一定要通过指针传值,不然进程会进入死锁状态
func f(i int, wg *sync.WaitGroup) { 
    fmt.Println(i)
    wg.Done()
}

五、sync.Once

1. 引言

在编程的很多场景下我们需要确保某些操作在高并发的场景下 只执行一次,例如只加载一次配置文件、只关闭一次通道等。

Go语言中的sync包中提供了一个针对只执行一次场景的解决方案 – sync.Once。

2. sync.Once 介绍

sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,例如初始化配置、保持数据库连接等。作用与 init 函数类似 ,但有区别:

  • init 函数是在文件包首次被加载的时候执行,且只执行一次
  • sync.Onc 是在代码运行中需要的时候执行,且只执行一次

当一个函数不希望程序在一开始的时候就被执行的时候,我们可以使用 sync.Once 。

init 函数是当所在的 package 首次被加载时执行,若迟迟未被使用,则既浪费了内存,又延长了程序加载时间。
sync.Once 可以在代码的任意位置初始化和调用 ,因此可以延迟到使用时再执行,并发场景下是线程安全的。

在多数情况下,sync.Once 被用于控制变量的初始化,这个变量的读写满足如下三个条件:

  • 当且仅当第一次访问某个变量时,进行初始化(写);
  • 变量初始化过程中,所有读都被阻塞,直到初始化完成;
  • 变量仅初始化一次,初始化完成后驻留在内存里。

sync.Once 其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

3. Do 方法

sync.Once 仅提供了一个方法 Do,参数 f 是对象初始化函数。

func (o *Once) Do(f func())

4. 实例

sync.Once.Do(f func()) 能保证 sync.Once 只执行一次,无论你是否更换 Once.Do(xx) 里面的方法 xx,这个 sync.Once 块只会执行一次。

package main

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

var once sync.Once

func main() {

	for i, v := range make([]string, 10) {
		once.Do(onces)                         //执行一次
		fmt.Println("count:", v, "---", i)
	}
	for i := 0; i < 10; i++ {

		go func() {
			once.Do(onced)                    //不会被执行
			fmt.Println("213")
		}()
		
	}
	time.Sleep(4000)
}
func onces() {
	fmt.Println("onces")
}
func onced() {
	fmt.Println("onced")
}

输出结果:

onces
count:  --- 0
count:  --- 1
count:  --- 2
count:  --- 3
count:  --- 4
count:  --- 5
count:  --- 6
count:  --- 7
count:  --- 8
count:  --- 9
213
213
213
213
213
213
213
213
213
213

六、sync.Map

1. 引言

工作中,经常会碰到并发读写 map 而造成 panic 的情况,为什么在并发读写的时候,会 panic 呢?因为在并发读写的情况下,map 里的数据会被写乱,之后就是 Garbage in, garbage out,还不如直接 panic 了。

Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。

下面来看下并发情况下读写 map 时会出现的问题,代码如下:

package main

func main() {

	// 创建一个int到int的映射
	m := make(map[int]int)
	// 开启一段并发代码
	go func() {
		// 不停地对map进行写入
		for {
			m[1] = 1
		}
	}()
	// 开启一段并发代码
	go func() {
		// 不停地对map进行读取
		for {
			_ = m[1]
		}
	}()
	// 无限循环, 让并发程序在后台执行
	for {

	}

}

报错:

fatal error: concurrent map read and map write

错误信息显示,并发的 map 读和 map 写,也就是说使用了两个并发函数不断地对 map 进行读和写而发生了竞态问题,map 内部会对这种并发操作进行检查并提前发现。

所以说,Go 语言原生 map 并不是线程安全的,对它进行并发读写操作的时候,需要加锁。而 sync.map 则是一种 并发安全 的 map,在 Go 1.9 引入。

  • sync.map 是线程安全的,读取,插入,删除也都保持着常数级的时间复杂度。
  • sync.map 的零值是有效的,并且零值是一个空的 map。在第一次使用之后,不允许被拷贝。

一般情况下解决并发读写 map 的思路是加一把大锁,或者把一个 map 分成若干个小 map,对 key 进行哈希,只操作相应的小 map。前者锁的粒度比较大,影响效率;后者实现起来比较复杂,容易出错。而使用 sync.map 之后,对 map 的读写,不需要加锁。并且它通过空间换时间的方式,使用 read 和 dirty 两个 map 来进行读写分离,降低锁时间来提高效率。

2. 源码:sync.Map 数据结构

type Map struct {
    mu Mutex
    read atomic.Value // readOnly
    dirty map[interface{}]*entry
    misses int
}

互斥量 mu 保护 read 和 dirty。

read 是 atomic.Value 类型,可以并发地读。但如果需要更新 read,则需要加锁保护。

dirty 是一个非线程安全的原始 map。包含新写入的 key,并且包含 read 中的所有未被删除的 key。这样,可以快速地将 dirty 提升为 read 对外提供服务。如果 dirty 为 nil,那么下一次写入时,会新建一个新的 dirty,这个初始的 dirty 是 read 的一个拷贝,但除掉了其中已被删除的 key。

每当从 read 中读取失败,都会将 misses 的计数值加 1,当加到一定阈值以后,需要将 dirty 提升为 read,以期减少 miss 的情形。

3. sync.Map

需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。

sync.Map 有以下特性:

  • 无须初始化,直接声明即可。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。

3. 实例

实例 1

并发安全的 sync.Map 演示代码如下:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var scene sync.Map
	// 将键值对保存到sync.Map
	scene.Store("greece", 97)
	scene.Store("london", 100)
	scene.Store("egypt", 200)
	// 从sync.Map中根据键取值
	fmt.Println(scene.Load("london"))
	// 根据键删除对应的键值对
	scene.Delete("london")
	// 遍历所有sync.Map中的键值对
	scene.Range(func(k, v interface{}) bool {
		fmt.Println("iterate:", k, v)
		return true
	})
}

输出结果:

100 true
iterate: egypt 200
iterate: greece 97

代码说明如下:

  • 声明 scene,类型为 sync.Map,注意,sync.Map 不能使用 make 创建
  • 将一系列键值对保存到 sync.Map 中,sync.Map 将键和值以 interface{} 类型进行保存。
  • 提供一个 sync.Map 的键给 scene.Load() 方法后将查询到键对应的值返回。
  • sync.Map 的 Delete 可以使用指定的键将对应的键值对删除。
  • 第 24 行,Range() 方法可以遍历 sync.Map,遍历需要提供一个匿名函数,参数为 k、v,类型为 interface{},每次 Range() 在遍历一个元素时,都会调用这个匿名函数把结果返回。

实例 2

package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map
	// 1. 写入
	m.Store("qcrao", 18)
	m.Store("stefno", 20)

	// 2. 读取
	age, _ := m.Load("qcrao")
	fmt.Println(age.(int))

	// 3. 遍历
	m.Range(func(key, value interface{}) bool {
		name := key.(string)
		age := value.(int)
		fmt.Println(name, age)
		return true
	})

	// 4. 删除
	m.Delete("qcrao")
	age, ok := m.Load("qcrao")
	fmt.Println(age, ok)

	// 5. 读取或写入
	m.LoadOrStore("stefno", 100)
	age, _ = m.Load("stefno")
	fmt.Println(age)
}

输出结果:

18
qcrao 18
stefno 20
<nil> false
20

说明:

  • 第 1 步,写入两个 k-v 对;
  • 第 2 步,使用 Load 方法读取其中的一个 key;
  • 第 3 步,遍历所有的 k-v 对,并打印出来;
  • 第 4 步,删除其中的一个 key,再读这个 key,得到的就是 nil;
  • 第 5 步,使用 LoadOrStore,尝试读取或写入 “Stefno”,因为这个 key 已经存在,因此写入不成功,并且读出原值。

4. 两个说明

  1. sync.map 适用于读多写少的场景。对于写多的场景,会导致 read map 缓存失效,需要加锁,导致冲突变多;而且由于未命中 read map 次数过多,导致 dirty map 提升为 read map,这是一个 O(N) 的操作,会进一步降低性能。
  2. sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

参考链接

  1. Go语言等待组(sync.WaitGroup)
  2. Golang 并发编程之同步原语
  3. Go语言基础-sync包
  4. GO 互斥锁(Mutex)原理
  5. Go sync.WaitGroup的用法
  6. 并发安全和锁
  7. Sync
  8. go语言:sync.Once的用法
  9. Go语言sync.Map(在并发环境中使用的map)
  10. 深度解密Go语言之sync.map
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-02-28 15:14:49  更:2022-02-28 15:19:13 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 11:41:28-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码