8-并发编程之-sync包-锁

一 临界区

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

临界区:当程序并发地运行时,多个 [Go 协程]不应该同时访问那些修改共享资源的代码。这些修改共享资源的代码称为临界区。

package main

import (
	"fmt"
	"sync"
)

var x = 10
var wg sync.WaitGroup

func add() {
	for i := 0; i < 5000; i++ {
		/*
			获得 x 的当前值
			计算 x + 1
			将步骤 2 计算得到的值赋值给 x
		*/
		x = x + 1

	}
	wg.Done()
}
func main() {

	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)

}

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

二 互斥锁

Mutex 用于提供一种加锁机制(Locking Mechanism),可确保在某时刻只有一个协程在临界区运行,以防止出现竞态条件。

Mutex 可以在 [sync] 包内找到。[Mutex] 定义了两个方法:[Lock]和 [Unlock](。所有在 LockUnlock 之间的代码,都只能由一个 Go 协程执行,于是就可以避免竞态条件。

mutex.Lock()  
x = x + 1  
mutex.Unlock()

在上面的代码中,x = x + 1 只能由一个 Go 协程执行,因此避免了竞态条件。

如果有一个 Go 协程已经持有了锁(Lock),当其他协程试图获得该锁时,这些协程会被阻塞,直到 Mutex 解除锁定为止

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题

package main

import (
	"fmt"
	"sync"
)

var x = 10
var wg sync.WaitGroup
var lock sync.Mutex  // 值类型,不需要初始化
func add() {
	for i := 0; i < 5000; i++ {
		/*
			获得 x 的当前值
			计算 x + 1
			将步骤 2 计算得到的值赋值给 x
		*/
		lock.Lock()
		x = x + 1
		lock.Unlock()

	}
	wg.Done()
}
func main() {

	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)

}

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

三读写锁

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

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

package main

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

var x = 10
var wg sync.WaitGroup
//var rwlock sync.RWMutex  // 值类型,不需要初始化
//var lock sync.Mutex  // 值类型,不需要初始化
var lock   sync.Mutex
var rwlock sync.RWMutex

func write()  {
	//rwlock.Lock() // 写锁都用Lock
	lock.Lock()
	time.Sleep(1 * time.Millisecond) // 模拟写耗时1毫秒
	x=x+1
	//rwlock.Unlock()
	lock.Unlock()
	wg.Done()

}
func read()  {
	//rwlock.RLock() // 读锁用RLock
	lock.Lock()
	time.Sleep(time.Millisecond) // 模拟读耗时1毫秒
	//fmt.Printf("x 现在的值是:%d\n",x)
	//rwlock.RUnlock()
	lock.Unlock()
	wg.Done()

}
func main() {
	// 统计开始时间
	time1:=time.Now()
	// 开10个协程写
	for i := 0; i < 10 ; i++ {
		wg.Add(1)
		go write()
	}
	// 开1000个协程读
	for i := 0; i < 1000 ; i++ {
		wg.Add(1)
		go read()
	}

	wg.Wait()
	fmt.Println("x最终值为:",x)
	// 统计结束时间
	time2:=time.Now()
	fmt.Println(time2.Sub(time1))  // 结束时间-开始时间

	// 使用读写锁:15.387426ms
	// 使用互斥锁:1.26868106s
}

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

四 sync.WaitGroup

在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:

方法名 功能
(wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

我们利用sync.WaitGroup将上面的代码优化一下:

var wg sync.WaitGroup

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

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

五 sync.Once

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

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

sync.Once只有一个Do方法,其签名如下:

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

注意:如果要执行的函数f需要传递参数就需要搭配闭包来使用。

通过sync.Once实现一个单例

package main

import (
	"fmt"
	"sync"
)

// 实现单例

// 定义一个 sync.Once
var one sync.Once

// 定义一个animalSig的指针变量
var animalSig *Animal

// 定义一个结构体
type Animal struct {
	name string
	age  int
}

func getAnimalInstance() *Animal {
	one.Do(func() {
		fmt.Println("只会执行一次")
		animalSig = &Animal{"狗狗", 1}
	})
	return animalSig
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			res:=getAnimalInstance()
			fmt.Printf("单例animalSig地址为:%p\n",res)
			wg.Done()
		}()
	}
	wg.Wait()


}

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

六 sync.Map

Go语言中内置的map不是并发安全的。请看下面的示例:

package main

import (
	"fmt"
	"strconv"
	"sync"
)

// 定义一个map
var m1 = make(map[string]string)

func setMap(key, valeu string) {
	m1[key] = valeu
}
func getMap(key string) string {
	return m1[key]
}
func main() {
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(i)
			setMap(key, key)
			fmt.Println(getMap(key))
			wg.Done()
		}(i)

	}
	wg.Wait()
	//报错:fatal error: concurrent map writes

}

上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。

像这种场景下就需要为 map 加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版 map——sync.Map。开箱即用表示其不用像内置的 map 一样使用 make 函数初始化就能直接使用。同时sync.Map内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。

方法名 功能
func (m *Map) Store(key, value interface{}) 存储key-value数据
func (m *Map) Load(key interface{}) (value interface{}, ok bool) 查询key对应的value
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) 查询或存储key对应的value
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) 查询并删除key
func (m *Map) Delete(key interface{}) 删除key
func (m *Map) Range(f func(key, value interface{}) bool) 对map中的每个key-value依次调用f

下面的代码示例演示了并发读写sync.Map

package main

import (
	"fmt"
	"strconv"
	"sync"
)
var m1 sync.Map=sync.Map{}  // 要初始化
func main() {
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			m1.Store(key,n)
			res,_:=m1.Load(key)
			fmt.Printf("key为:%s,value为:%d\n",key,res)
			wg.Done()
		}(i)

	}
	wg.Wait()
	//报错:fatal error: concurrent map writes

}

posted @ 2022-03-12 15:48  刘清政  阅读(106)  评论(0编辑  收藏  举报