竞态指多个goroutine按某些交错顺序执行时程序无法给出正确结果。
因为我们无法知道一个goroutine中的事件x和另一个goroutine中的事件y
的执行先后顺序。它们是并发的。
package main
import (
"fmt"
"time"
)
var myMoeny int
func give(money int) {
myMoeny += money
}
func look() int {
return myMoeny
}
func main() {
go func() {
give(100)
fmt.Println("1:", look())
}()
go func() {
give(200)
fmt.Println("2:", look())
}()
time.Sleep(time.Second * 2)
}
这个时候由于两次存钱同时进行(不知道先后),有可能就出现
1:300 , 2:200的情况。但这并不是我们的初衷,我们希望的是
1:100,2:300的情况。
上面的例子中出现了数据竞态的情况:
两个goroutine并发读写一个变量并且至少其中一个是写入时。
根据数据竞态的定义我们有如下解决办法:
1. 在其他goroutine创建之前,数据已经初始化且不在修改,
这样每个gouroutine都只读,就不会出现数据竞态。
2. 避免从多个goroutine访问同一个变量。
由于其他goroutine无法直接访问变量,它们需要使用
通道来发送查询请求或者更新变量。(即不要通过共享
内存来通信,通过通信来共享内存)。
package main
import (
"fmt"
"time"
)
var giveMoney = make(chan int)
var recvMoney = make(chan int)
func give(money int) {
giveMoney <-money
}
func look() int {
return <-recvMoney
}
func teller() {
var myMoney int
for {
select {
case m := <-giveMoney:
myMoney += m
case recvMoney<- myMoney:
}
}
}
func main() {
go teller()
give(100)
fmt.Println("1:", look())
give(200)
fmt.Println("2:", look())
time.Sleep(time.Second * 2)
}
3. 运行多个gotoutine访问一个变量,但同一时间只能有一个访问。
互斥锁:sync.Mutex
package main
import (
"fmt"
"sync"
"time"
)
var (
mu sync.Mutex //保护共享变量myMoney
myMoney int
)
func give(money int) {
mu.Lock()
defer mu.Unlock()
myMoney += money
}
func look() int {
mu.Lock()
defer mu.Unlock()
return myMoney
}
func main() {
give(200)
fmt.Println("1:", look())
go func() {
give(100)
fmt.Println("2:", look())
}()
time.Sleep(time.Microsecond * 1000)
}
通过Lock方法获取一个互斥锁,如果其他goroutine取走互斥锁,那么操作
会一直阻塞到其他goroutine调用Unlock之后。
读写互斥锁:sync.RWMutex
它允许读操作并发执行,写操作只能独享访问权限
var (
mu sync.RWMutex //保护共享变量myMoney
myMoney int
)
func give(money int) {
mu.Lock() //写锁
defer mu.Unlock()
myMoney += money
}
func look() int {
mu.RLock()//读锁
defer mu.RUnlock()
return myMoney
}
sync.RWMutex在符锁竞争激烈的时候才有优势,不激烈时比普通互斥锁慢。
//goroutine与线程
goroutine并不等同于操作系统中的线程
1.操作系统中每个线程都有一个固定大小的栈内存,用来保护正在执行或者
临时暂停的函数中的局部变量。
goroutine的栈时可增长的,最开始时很小,最大可到1GB
2.OS线程由OS内核调度。每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用
一个叫调度器的内核函数。
Go运行时包含一个自己的调度器,它可以调m个进程到n个OS线程。Go调度器
不是由硬件时钟定期触发的,是由Go语言结构出发的。消耗成本更低。
3.Go调度器使用GOMAXPROCS参数确定使用多少个OS线程来同时执行Go代码。
默认值是机器上的CPU数量。
4.goroutine没有标识符。OS线程都有独特的标识符。