0
点赞
收藏
分享

微信扫一扫

go sync 处理同步需求详解


文章目录

  • ​​sync背景​​
  • ​​sync 包的使用方法​​
  • ​​Lock 锁​​
  • ​​sync.Mutex​​
  • ​​sync.RWMutex​​
  • ​​RLock() 和 RUnlock()​​
  • ​​Once​​
  • ​​WaitGroup​​

sync背景


golang 是一门语言级别支持并发的程序语言。golang 中使用 go 语句来开启一个新的协程。 goroutine 是非常轻量的,除了给它分配栈空间,它所占用的内存空间是微乎其微的。

但当多个 goroutine 同时进行处理的时候,就会遇到比如同时抢占一个资源,某个 goroutine 等待另一个 goroutine 处理完某一个步骤之后才能继续的需求。 在 golang 的官方文档上,作者明确指出,golang 并不希望依靠共享内存的方式进行进程的协同操作。而是希望通过管道 channel 的方式进行。 当然,golang 也提供了共享内存,锁,等机制进行协同操作的包。sync 包就是为了这个目的而出现的。

sync 包的使用方法

  • sync.Mutex
  • sync.RMutex
  • sync.Once
  • sync.Cond

Lock 锁

type Locker interface {
Lock()
Unlock()
}

并且创造了两个结构来实现 Locker 接口:​​Mutex 和 RWMutex​​。

Mutex 就是互斥锁,互斥锁代表着当数据被加锁了之后,除了加锁的程序,其他程序不能对数据进行读操作和写操作。 这个当然能解决并发程序对资源的操作。但是,效率上是个问题。当加锁后,其他程序要读取操作数据,就只能进行等待了。 这个时候就需要使用读写锁。

读写锁分为读锁和写锁,读数据的时候上读锁,写数据的时候上写锁。有写锁的时候,数据不可读不可写。有读锁的时候,数据可读,不可写。 互斥锁就不举例子。

sync.Mutex

sync.Mutex用于多个goroutine对共享资源的互斥访问。使用要点如下:
(1)使用Lock()加锁,Unlock()解锁;
(2)对未解锁的Mutex使用Lock()会阻塞;
(3)对未上锁的Mutex使用Unlock()会导致 panic 异常。

加锁和解锁示例

package main
import (
"time"
"fmt"
"sync"
)
func main() {
var mutex sync.Mutex
fmt.Println("Lock the lock")
mutex.Lock()
fmt.Println("The lock is locked")
channels := make([]chan int, 4)
for i := 0; i < 4; i++ {
channels[i] = make(chan int)
go func(i int, c chan int) {
fmt.Println("Not lock: ", i)
mutex.Lock()
fmt.Println("Locked: ", i)
time.Sleep(time.Second)
fmt.Println("Unlock the lock: ", i)
mutex.Unlock()
c <- i
}(i, channels[i])
}
time.Sleep(time.Second)
fmt.Println("Unlock the lock")
mutex.Unlock()
time.Sleep(time.Second)

for _, c := range channels {
<-c
}
}
程序输出:

Lock the lock
The lock is locked
Not lock: 1
Not lock: 2
Not lock: 0
Not lock: 3
Unlock the lock
Locked: 1
Unlock the lock: 1
Locked: 2
Unlock the lock: 2
Locked: 3
Unlock the lock: 3
Locked: 0
Unlock the lock: 0

在解锁之前加锁会导致死锁

package main
import (
"fmt"
"sync"
)
func main(){
var mutex sync.Mutex
mutex.Lock()
fmt.Println("Locked")
mutex.Lock()
}
程序输出:

Locked
fatal error: all goroutines are asleep - deadlock!

sync.RWMutex

sync.RWMutex用于读锁和写锁分开的情况。使用时注意如下几点:
(1)RWMutex是单写多读锁,该锁可以加多个读锁或者一个写锁;
(2)读锁占用的情况下会阻止写,不会阻止读,多个 goroutine 可以同时获取读锁;
(3)写锁会阻止其他 goroutine(无论读和写)进来,整个锁由该 goroutine 独占;
(4)适用于读多写少的场景。

多个goroutine对同一个全局变量读写需要加锁
rwmutex1.go

package main
import (
"fmt"
"sync"
)
func main() {
var intVar int
var wg sync.WaitGroup
var mutex sync.RWMutex
go func(){
defer wg.Done()
mutex.Lock()
intVar=4
mutex.Unlock()
fmt.Printf("first goroutine, intVar=%d\n",intVar)
}()

go func(){
defer wg.Done()
mutex.Lock()
intVar=5
mutex.Unlock()
fmt.Printf("second goroutine, intVar=%d\n",intVar)
}()

wg.Add(2)
wg.Wait()
fmt.Println("end main goroutine")
}

输出结果:

second goroutine, intVar=5
first goroutine, intVar=4
end main goroutine

//或
first goroutine, intVar=4
second goroutine, intVar=5
end main goroutine

RLock() 和 RUnlock()

(1)RLock() 加读锁,RUnlock() 解读锁;
(2)RLock() 加读锁时,如果存在写锁,则无法加读锁;当只有读锁或者没有锁时,可以加读锁,读锁可以加多个;
(3)RUnlock() 解读锁,RUnlock() 撤销单次 RLock() 调用,对于其他同时存在的读锁则没有效果;
(4)在没有读锁的情况下调用 RUnlock(),会导致 panic 错误;
(5)RUnlock() 的个数不得多于 RLock(),否则会导致 panic 错误。

rwmutex1.go

package main
import (
"sync"
"time"
)
var m *sync.RWMutex
var val = 0

func main() {
m = new(sync.RWMutex)
go read(1)
go write(2)
go read(3)
time.Sleep(5 * time.Second)
}

func read(i int) {
m.RLock()
time.Sleep(1 * time.Second)
println("val: ", val)
time.Sleep(1 * time.Second)
m.RUnlock()
}

func write(i int) {
m.Lock()
val = 10
time.Sleep(1 * time.Second)
m.Unlock()
}
输出:
val: 0
val: 10
但是如果我们把 read 中的 RLock RUnlock 两个函数给注释了,就返回了 :

val: 10
val: 10
这个就是由于读的时候没有加读锁,在准备读取 val 的时候,val write 函数进行修改了

Once

有的时候,我们多个 goroutine 都要过一个操作,但是这个操作我只希望被执行一次,这个时候 Once 就上场了。比如下面的例子 :

package main

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

func main() {
var once sync.Once
onceBody := func() {
fmt.Println("Only once")
}
for i := 0; i < 10; i++ {
go func() {
once.Do(onceBody)
}()
}
time.Sleep(3e9)
}
[root@localhost sync]# go run sync3.go
Only once
[root@localhost sync]#

WaitGroup

一个 goroutine 需要等待一批 goroutine 执行完毕以后才继续执行,那么这种多线程等待的问题就可以使用 WaitGroup 了。
假如没有时间等待for循环,会输出不确定。

package main
import (
"fmt"
//"time"
)

func main(){
for i := 0; i < 200 ; i++{
go fmt.Println(i)
}
//time.Sleep(time.Second)
}
[root@localhost sync]# go run sync5.go
12
0
1
[root@localhost sync]# go run sync5.go
19
0
1

有时间等待,会正常输出for循环,但偶尔给出的时间并不能保障足够。

package main
import (
"fmt"
"time"
)

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

[root@localhost sync]# go run sync5.go
0
1
2
3
...//100行
99

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

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

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

[root@localhost sync]# cat sync6.go
package main
import "fmt"
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
}
}

[root@localhost sync]# go run sync6.go
99
49
48
54
....//100行

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

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

WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为n ,Done() 每次把计数器-1 ,wait() 会阻塞代码的运行,直到计数器地值减为0。
注意:

  1. 计数器不能为负值
  2. WaitGroup对象不是一个引用类型

使用WaitGroup 将上述代码可以修改为:
sync7-1.go

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()
}
[root@localhost sync]# go run sync7-1.go
38
99
0
42
....//100行

sync7-2.go

package main
import (
"fmt"
"sync"
)
func main() {
wp := new(sync.WaitGroup)
wp.Add(10);

for i := 0; i < 10; i++ {
go func() {
fmt.Println("done ", i)
wp.Done()
}()
}

wp.Wait()
fmt.Println("wait end")
}
[root@localhost sync]# go run sync7-2.go
done 10
done 10
done 10
done 10
done 10
done 10
done 10
done 10
done 10
done 10
wait end

sync.waitGroup实战
获取同时开三个协程去请求网页

package main
import (
"fmt"
"sync"
"net/http"
"io/ioutil"
)
func main() {
var wg sync.WaitGroup
var urls = []string{
"http://www.baidu.com/",
"http://www.ailiyun.com/",
"http://www.bilibili.com/",
}

for _, url := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
r, err := http.Get(url)
if err != nil {
panic(err)
}
defer func() { _ = r.Body.Close() }()

body, _ := ioutil.ReadAll(r.Body)
fmt.Printf("%s", body)
}(url)
}
wg.Wait()
}
$ go run sync8.go
DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html;charset=utf-8"><meta http-equiv=
"X-UA-Compatible" content="IE=edge,chrome=1"><meta content="always" name="referrer"><meta name="theme-color" content="#2932e1"
><link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" /><link rel="search" type="application/opensearchdescriptio
n+xml" href="/content-search.xml" title="百度搜索" />" sizes="any" mask href="//www.baidu.com/img/baidu_85beaf5
496f291521eb75ba38eacbd87

参考链接:
​​​https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter16/16.01.html​​​​https://www.kancloud.cn/digest/batu-go/153535​​

举报

相关推荐

0 条评论