一、两个问题
1、同步执行问题
package main
import (
"fmt"
"time"
)
func main() {
go fun1()
go fun2()
fmt.Println("main函数等待")
time.Sleep(time.Second * 1)
fmt.Println("main函数结束")
}
func fun1() {
fmt.Println("fun1函数执行")
}
func fun2() {
fmt.Println("fun2函数执行")
}
主线程为了等待所有的子goroutine都运行完毕,不得不在程序中使用time.Sleep() 来睡眠一段时间,等待其他线程充分运行。这种方式耗费时间,显然是不够优雅的。
2、临界资源问题
临界资源: 指并发环境中多个进程/线程/协程共享的资源。并发编程中对临界资源的处理不当, 往往会导致数据不一致的问题。
如果多个goroutine在访问同一个数据资源(临界资源)的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的。
举个例子,我们通过并发来实现火车站售票这个程序。一共有10张票,3个售票口同时出售。
package main
import (
"fmt"
"math/rand"
"time"
)
//全局变量票数
var tickets = 10
func main() {
//三个goroutine 模拟售票窗口
go saleTickets("售票口1")
go saleTickets("售票口2")
go saleTickets("售票口3")
//为了保证3个goroutine协程正常工作,先将主线程睡眠5秒
time.Sleep(5 * time.Second)
}
func saleTickets(name string) {
//随机数种子
rand.Seed(time.Now().UnixNano())
for {
if tickets > 0 {
//随机睡眠1~1000ms
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
fmt.Println(name, "余票:", tickets)
tickets--
} else {
fmt.Println(name, "售罄,已无票。。")
break
}
}
}
运行结果
售票口3 余票: 10
售票口2 余票: 10
售票口1 余票: 10
售票口3 余票: 7
售票口1 余票: 7
售票口3 余票: 5
售票口2 余票: 4
售票口3 余票: 3
售票口2 余票: 3
售票口1 余票: 3
售票口1 售罄,已无票。。
售票口2 余票: 0
售票口2 售罄,已无票。。
售票口3 余票: -1
售票口3 售罄,已无票。。
在以上的代码中,使用三个并发运行的go协程模拟了三个售票窗口同时售票,而由于全局变量tickets会被三个协程在一段时间内同时访问,因此tickets就是我们所说的“临界资源”。
我们可以发现:
在开始时,三个窗口同时读到信息:tickets=10,从而随机都输出了余票=10
而在结尾时,竟然出现了余票为负数的情况,其产生的原因在于,票数快要卖完时,当售票口1余票1,并且售完这一张票后,在这个时间段内,售票口2已经进入了if tickets > 0满足条件的代码块内,然而售票口1此时将最后一张票售出,tickets 由1变为0售票口2打印出来了不应该出现的结果:余票0,同理售票口3打印了不该出现的结果:余票-1。
多goroutine【多任务】,有共享资源,且多goroutine修改共享资源,出现数据不安全问题【数据错误】,保证数据安全一致,需要goroutine同步
goroutine同步方式:
- channel 【csp模型】
- sync包提供的方法
二、sync同步等待组WaitGroup
使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务。
等待组的方法:
方法名 | 功能 |
---|---|
(wg *WaitGroup)Add(delta int) | 等待组的计数器+1 |
(wg *WaitGroup)Done() | 等待组的计数器-1 |
(wg *WaitGroup)Wait() | 当等待组计数器不等于0时阻塞,直到为0 |
代码示例:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
wg.Add(1)
go fun1()
wg.Add(1)
go fun2()
fmt.Println("main函数等待")
wg.Wait()
fmt.Println("main函数结束")
}
func fun1() {
fmt.Println("fun1函数执行")
wg.Done()
}
func fun2() {
fmt.Println("fun2函数执行")
wg.Done()
}
运行结果
main函数等待
fun1函数执行
fun2函数执行
main函数结束
三、sync互斥锁Mutex
加锁成功则操作资源,加锁失败则等待直至锁加锁成功——所有的goroutine互斥,一个得到锁其他全部等待。
互斥锁被称为Mutex,它有2个函数,Lock()和Unlock()分别是获取锁和释放锁,如下:
type Mutex
func (m *Mutex) Lock(){}
func (m *Mutex) Unlock(){}
修改上面售票代码,解决临界资源安全问题
示例代码:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
//全局变量票数
var tickets = 10
var mutex sync.Mutex
var wg sync.WaitGroup
func main() {
//三个goroutine 模拟售票窗口
wg.Add(1)
go saleTickets("售票口1")
wg.Add(1)
go saleTickets("售票口2")
wg.Add(1)
go saleTickets("售票口3")
wg.Wait()
}
func saleTickets(name string) {
//随机数种子
rand.Seed(time.Now().UnixNano())
for {
//上锁
mutex.Lock()
if tickets > 0 {
//随机睡眠1~1000ms
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
fmt.Println(name, "余票:", tickets)
tickets--
} else {
mutex.Unlock()
fmt.Println(name, "售罄,已无票。。")
break
}
//解锁
mutex.Unlock()
}
wg.Done()
}
运行结果
售票口3 余票: 10
售票口3 余票: 9
售票口1 余票: 8
售票口2 余票: 7
售票口3 余票: 6
售票口1 余票: 5
售票口2 余票: 4
售票口3 余票: 3
售票口1 余票: 2
售票口2 余票: 1
售票口1 售罄,已无票。。
售票口2 售罄,已无票。。
售票口3 售罄,已无票。。
四、sync读写锁RWMutex
读写锁要达到的效果是同一时间可以允许多个协程读数据,但只能有且只有1个协程写数据。也就是说,读和写是互斥的,写和写也是互斥的,但读和读并不互斥。
简单来说:
- (1)可以随便读,多个goroutine同时读。读的时候不能写。
- (2)写的时候,啥也不能干。不能读也不能写。
读写锁是RWMutex,它有5个函数:
- Lock()和Unlock()是给写操作用的。
- RLock()和RUnlock()是给读操作用的。
- RLocker()能获取读锁,然后传递给其他协程使用。使用较少。
type RWMutex
func (rw *RWMutex) Lock(){}
func (rw *RWMutex) RLock(){}
func (rw *RWMutex) RLocker() Locker{}
func (rw *RWMutex) RUnlock(){}
func (rw *RWMutex) Unlock(){}
举个例子,学生信息录入系统,录入学生信息是写操作,读取学生信息是读操作。可以使用读写锁:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
// Student 学生信息系统
type Student struct {
// 读写锁
sync.RWMutex
// 存储信息 姓名-年龄
data map[string]int
}
// Add 增加学生信息
func (s *Student) Add(name string, age int) {
defer wg.Done()
s.Lock()
defer s.Unlock()
if _, ok := s.data[name]; !ok {
s.data[name] = age
}
}
// Query 读取学生信息
func (s *Student) Query(name string) {
defer wg.Done()
s.RLock()
defer s.RUnlock()
if v, ok := s.data[name]; ok {
fmt.Printf("姓名:%s\t年龄:%d\n", name, v)
} else {
fmt.Println("学生信息不存在!")
}
}
func main() {
s := &Student{
data: make(map[string]int),
}
wg.Add(4)
s.Add("jack", 20)
s.Add("tom", 23)
s.Add("lili", 18)
s.Add("lili", 20)
nameList := []string{"jack", "tom", "lili", "xiaohua"}
for _, v := range nameList {
wg.Add(1)
go s.Query(v)
}
wg.Wait()
}
运行结果
学生信息不存在!
姓名:jack 年龄:20
姓名:lili 年龄:18
姓名:tom 年龄:23
五、sync单次执行Once
sync.Once 是 Golang package 中使方法只执行一次的对象实现,作用与 init 函数类似。但也有所不同:
- init 函数是在文件包首次被加载的时候执行,且只执行一次
- sync.Once 是在代码运行中需要的时候执行,且只执行一次
当一个函数不希望程序在一开始的时候就被执行的时候,我们可以使用 sync.Once 。
sync.Once是让函数方法只被调用执行一次的实现,其最常应用于单例模式之下,例如初始化系统配置、保持数据库唯一连接等。
代码示例
package main
import (
"sync"
)
var configs map[string]string
func loadConfig() {
configs = map[string]string{
"url": "https://www.jianshu.com",
"id": "cd41c8c3645c",
"email": "everydawn@jianshu.com",
}
}
// Config1 被多个goroutine调用时不是并发安全的
// 比如有两个线程都在调用Config1函数,线程A在执行到if configs==nil后
// cpu切换到线程B执行,直到线程B运行完,这时configs已经被实例化,
// 当cpu在切回到线程A继续执行的时候,对configs又执行实例化操作,
// 这时内存中已有configs的两个实例,违背了单例定义。
func Config1(name string) string {
if configs == nil {
loadConfig()
}
return configs[name]
}
var loadConfigOnce sync.Once
// Config2 是并发安全的
func Config2(name string) string {
loadConfigOnce.Do(loadConfig)
return configs[name]
}
func main() {
}
有疑问加站长微信联系(非本文作者)