首页>>后端>>Golang->Golang 基础之并发基本同步原语(一)

Golang 基础之并发基本同步原语(一)

时间:2023-12-01 本站 点击:0

大家好,今天将梳理出的 Go语言并发知识内容,分享给大家。 请多多指教,谢谢。

本章节内容

WaitGroup

Mutex

Locker

RWMutex

Go在内存访问同步基元的基础上构建了一组新的并发基元,并为使用者提供扩展的内容。 Go sync标准库,主要包含对低级别内存访问同步最有用的并发原语。 如果你使用的是主要通过内存访问同步处理并发的语言,那么这些类型是不错的选择。

WaitGroup

WaitGroup 类型原型

type WaitGroup struct {  // contains filtered or unexported fields}func (wg *WaitGroup) Add(delta int)func (wg *WaitGroup) Done()func (wg *WaitGroup) Wait()

可以把 WaitGroup 视作一个安全的并发计数器:调用 Add() 增加计数,调用 Done() 减少计数。调用 Wait() 会阻塞并等待至计数器归零。

请注意,Add() 的调用是在 goroutine 之外完成的。 如果没有这样做,我们会引入一个数据竞争条件,因为我们没有对 goroutine 做任何调度顺序上的保证; 我们可能在任何一个 goroutine 开始前触发 Wait() 调用。 如果 Add() 的调用被放置在 goroutine 的闭包中,对 Wait() 的调用可能完全没有阻塞地返回,因为 Add() 没有被执行。

WaitGroup 使用

package mainimport (  "fmt"  "sync")var wg sync.WaitGroupfunc main() {  hello := func(wg *sync.WaitGroup, id int) {    defer wg.Done()    fmt.Printf("id: %d\n", id)  }  wg.Add(5)  for i := 0; i < 5; i++ {    go hello(&wg, i+1)  }  wg.Wait()}

通常情况下,尽可能与要跟踪的 goroutine 就近且成对的调用 Add(),但有时候会一次性调用 Add() 来跟踪一组 goroutine。

Mutex

之前文章中已经简单介绍了 Mutex 类型,可以参考 Golang 基础之并发知识 (三) 文章。

Mutex 很容易理解,代表 “mutual exclusion(互斥)”。互斥提供了一种并发安全的方式来表示对共享资源访问的独占。

可以理解为在代码块设置临界区,在同一时刻只能由一个 goroutine 去操作。

Mutex 类型原型

type Mutex struct {  // contains filtered or unexported fields}func (m *Mutex) Lock()func (m *Mutex) TryLock() boolfunc (m *Mutex) Unlock()

Lock()方法: 锁定

TryLock()方法: 尝试锁定并报告 (很少使用)

Unlock()方法: 解锁

Mutex 使用

举例:两个 goroutine,它们试图增加和减少一个公共值,并使用 Mutex 来同步访问。

// 并发修改一个公共值package mainimport (  "fmt"  "sync")var Count intvar Lock sync.Mutexvar wg sync.WaitGroupfunc main() {  // 增加  for i := 0; i <= 5; i++ {    wg.Add(1)    go func() {      defer wg.Done()      increment()    }()  }   // 减少  for i := 0; i <= 5; i++ {    wg.Add(1)    go func() {      defer wg.Done()      decrement()    }()  }   wg.Wait()}func increment() {  Lock.Lock()  defer Lock.Unlock()  Count++  fmt.Printf("Incrementing: %d\n", Count)}func decrement() {  Lock.Lock()  defer Lock.Unlock()  Count--  fmt.Printf("Decrementing: %d\n", Count)}

这里,count变量由互斥锁保护

输出

Decrementing: -1Incrementing: 0Incrementing: 1Incrementing: 2Incrementing: 3Incrementing: 4Incrementing: 5Decrementing: 4Decrementing: 3Decrementing: 2Decrementing: 1Decrementing: 0

这里因为goroutine调度机制原因,在大家各自设备编码后结果会发生变化。

注意,被锁定部分是程序的性能瓶颈,进入和退出锁定的成本有点高,因此通常尽量减少锁定涉及的范围。

Locker

Locker 接口原型

type Locker interface {  Lock()  Unlock()}

Locker接口中定义了锁定和解锁的方法。

RWMutex

RWMutex 是读写互斥锁,锁可以由任意数量的读或单个写持有。RWMutex 的零值是一个未锁定的mutex。

RWMutexMutex 在概念上是一样的:它保护对内存的访问;不过,RWMutex可以给你更多地控制方式。 你可以请求锁定进行读取,在这种情况下,你将被授予读取权限,除非锁定正在进行写入操作。 这意味着,只要没有别的东西占用写操作,任意数量的读取者就可以进行读取操作。

常见的服务对资源的读写比列会非常高,如果大多数的请求都是读请求,它们之间不会互相影响,那么就可以将资源的操作进行读和写分离,出于这样的考虑,可以使用RWMutex。

读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但多个读操作之间不存在互斥关系。读写锁可以在大大降低因使用锁而造成的性能损耗,完成对共享资源的访问控制。

RWMutex 类型原型

type RWMutex struct {  // contains filtered or unexported fields}func (rw *RWMutex) Lock()func (rw *RWMutex) RLock()func (rw *RWMutex) RLocker() Lockerfunc (rw *RWMutex) RUnlock()func (rw *RWMutex) TryLock() boolfunc (rw *RWMutex) TryRLock() boolfunc (rw *RWMutex) Unlock()

Lock()方法: 用于写入的锁定;如果锁已被锁定用于读取或写入,则锁定会一直锁定,直到锁可用。

RLock()方法: 用于读取的锁定;它不应用于递归读取锁定;被阻止的锁调用会阻止新读取获取锁。

RLocker()方法: RLocker返回一个Locker接口,该接口通过调用rw来实现Lock和Unlock方法。

RUnlock()方法: RUnlock撤销一个RLock调用;它不会影响其他同时读取的goroutine。如果rw在进入RUnlock时未被锁定读取,则为运行时错误。

TryLock()方法: TryLock试图锁定rw进行写入,并报告是否成功。 (很少使用)

TryRLock()方法: TryRLock尝试锁定rw进行读取,并报告是否成功。 (很少使用)

Unlock()方法: 解锁用于写入的rw。如果rw未被锁定以写入要解锁的条目,则这是一个运行时错误。

与 Mutex 一样,RWMutex的互斥体与特定的 goroutine 没有关联。一个 goroutine 可以重新锁定(锁定),然后安排另一个 goroutine 运行锁定(解锁)。

RWMutex 使用

举例:读写锁的使用

package mainimport (  "fmt"  "sync"  "time")func main() {  var rwm sync.RWMutex  for i := 0; i < 3; i++ {    go func(i int) {      fmt.Printf("执行读锁: %d\n", i)      rwm.RLock()      fmt.Printf("读锁: %d\n", i)      time.Sleep(time.Second * 2)      fmt.Printf("执行取消读锁: %d\n", i)      rwm.RUnlock()      fmt.Printf("取消读锁: %d\n", i)    }(i)  }  time.Sleep(time.Millisecond * 100)  fmt.Println("执行写锁...")  rwm.Lock()  fmt.Println("写锁")}

输出

执行读锁: 0读锁: 0执行读锁: 1读锁: 1执行读锁: 2读锁: 2执行写锁...执行取消读锁: 1取消读锁: 1执行取消读锁: 0取消读锁: 0执行取消读锁: 2取消读锁: 2写锁

启用了 3 个 goroutine 用于读写锁 rwm 的读锁定和读解锁操作

读解锁操作会延迟 2s 进行模拟真是的情况

先让主 goroutine 睡眠 100ms,让 3个 goroutine先有足够时间执行

之后 rwm 的写锁定操作让主 goroutine 阻塞,因为此时 3个 goroutine读锁定还未进行读解锁操作

当 3个 goroutine读解锁完成后,main函数写锁定才会完成

可以通过这个例子看到 RWMutex 在大量级上相对于 Mutex 是有性能优势。 建议在逻辑上合理的情况下使用 RWMutex 而不是 Mutex

package mainimport (  "os"  "fmt"  "sync"  "time"  "math"  "text/tabwriter")var wg sync.WaitGroupfunc main() {  tw := tabwriter.NewWriter(os.Stdout, 0, 1, 2, ' ', 0)  defer tw.Flush()  var m sync.RWMutex  fmt.Fprintf(tw, "Readers\tRWMutex\tMutex\n")  for i := 0; i < 20; i++ {    count := int(math.Pow(2, float64(i)))    fmt.Fprintf(      tw, "%d\t%v\t%v\n", count,      test(count, &m, m.RLocker()),      test(count, &m, &m),    )  }}func test(count int, mutex, rwMutex sync.Locker) time.Duration {  wg.Add(count + 1)  beginTime := time.Now()  go producer(&wg, mutex)  for i := count; i > 0; i-- {    go observer(&wg, rwMutex)  }  wg.Wait()  return time.Since(beginTime)}func producer(wg *sync.WaitGroup, l sync.Locker) { // 1  defer wg.Done()  for i := 5; i > 0; i-- {    l.Lock()    l.Unlock()    time.Sleep(1) // 2  }}func observer(wg *sync.WaitGroup, l sync.Locker) {  defer wg.Done()  l.Lock()  defer l.Unlock()}

输出

package mainimport (  "fmt"  "sync")var wg sync.WaitGroupfunc main() {  hello := func(wg *sync.WaitGroup, id int) {    defer wg.Done()    fmt.Printf("id: %d\n", id)  }  wg.Add(5)  for i := 0; i < 5; i++ {    go hello(&wg, i+1)  }  wg.Wait()}0

producer 函数的第二个参数是 sync.Locker 类型。 该接口有两种方法,锁定和解锁,MutexRWMutex 类型都适用。

让 producer 休眠1秒

技术文章持续更新,请大家多多关注呀~~

搜索微信公众号,关注我【 帽儿山的枪手 】

参考材料

[1]   《Go并发编程实战》书籍

[2]   《Concurrency in Go》书籍

[3]  https://pkg.go.dev/sync sync标准库

原文:https://juejin.cn/post/7097902611017236488


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Golang/5920.html