Go语言中的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为goroutine时,Go会将其视为一个独立的工作单元。
操作系统会在物理处理器上调度线程来运行,而Go语言运行时会在逻辑处理器上调度 goroutine 来运行。每个逻辑处理器都分别绑定到单个操作系统线程。如果创建一个 goroutine 并运行,那么这个 goroutine 就会被放到调度器的全局运行队列中,之后调度器就会将这些队列中的 goroutine 分配给一个逻辑处理器,并将其放到这个逻辑处理器对应的本地运行队列中。本地运行队列中的 goroutine 会一直等待直到自己被分配的逻辑处理器执行。
goroutine 协程
我们通过一个在逻辑处理器上运行的例子来理解调度器的行为与如何管理 goroutine。
func main() {
//分配一个逻辑处理器给调度器使用
runtime.GOMAXPROCS(1)
//wg 用来等待线程完成
var wg sync.WaitGroup
//Add(2)表示要等待两个goroutine
wg.Add(2)
fmt.Println("start goroutines")
//声明匿名函数,并创建一个goroutine
go func() {
//在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done()
for i := 0;i<10;i++{
fmt.Println("func1: ",i)
}
}()
//声明匿名函数,并创建一个goroutine
go func() {
//在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done()
for i := 20;i<30;i++{
fmt.Println("func2: ",i)
}
}()
fmt.Println("Wating to finish")
//等待所以 goroutine 结束
//这里如果不设置就有可能会导致main函数在运行两个goroutine完成之前提前退出,这样程序就有可能提前终止
wg.Wait()
}
函数说明:
runtime.GOMAXPROCS(num):允许程序更改调度器可以使用的逻辑处理器的数量,如: runtime.GOMAXPROCS(runtime.NumCPU()),为每个可用的物理处理器创建一个逻辑处理器。
sync.WaitGroup:是一个计数信号量,可以用来记录并维护运行 goroutine 。如果WaitGroup的值大于0,就会导致 wg.Wait() 被阻塞,每执行一次wg.Done(),WaitGroup的值就会减一,最终为0.
基于调度器的内部算法,一个正在运行的 goroutine 可能会被停止并重新调度,这样的目的是为了防止某个goroutine 长时间占用逻辑处理器。如果多个goroutine 在没有互相同步的情况下访问某个共享的资源,那么就可能产生竞态。
竞态检测器
go build -race //竞态检测器
./example //运行程序
来看一下竞态的小示例
var(
counter int
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go IncCounter(1)
go IncCounter(2)
wg.Wait()
fmt.Println(counter)
}
func IncCounter(id int) {
defer wg.Done()
for count:=0;count<2;count++{
value := counter
//让出对处理器的占用
runtime.Gosched()
value ++
counter = value
}
}
输出结果并不一定等于4,这是因为每个 goroutine 都会覆盖掉原来 goroutine 的工作内容,也就是说当第一个goroutine 进入到IncCounter时,运行到 runtime.Gosched()时,会让出对处理器的占用,然后让第二个 goroutine 进入到 IncCounter执行,这样就会导致原来的 counter 被覆盖掉,最终结果就会出现错误。
同步工具
①原子函数:从底层的加锁机制来同步访问整型变量和指针
将上面的函数改为原子函数操作
func IncCounter(id int) {
defer wg.Done()
for count:=0;count<2;count++{
atomic.AddInt64(&counter,1)
//放弃对处理器的占用,回到队列中,相当于java中的yeild
runtime.Gosched()
}
}
②互斥锁
互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码。
func IncCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
mutex.Lock()
{
value := counter
runtime.Gosched()
value ++
counter = value
}
mutex.Unlock()
}
}
③通道
除了上面两个方式进行同步消除竞态以外,还可以使用通道来解决竞争问题。
当一个资源需要在 goroutine 中被共享时,通道会在goroutine之间建立一个管道,并提供同步交换数据的机制。声明通道时,需要指定被共享的数据类型。通道分为无缓冲的通道与有缓冲的通道。向通道发送数据需要用到<-操作符
court := make(chan int) //无缓冲通道
court := make(chan int,10)//有缓冲通道
court<- 1 //向通道发送整数 1
value,ok := <-court //从通道中接收数据,value:接收的通道数据,ok:通道是否被关闭,true表示正常开启
无缓冲的通道是指接收前没有能力保存任何值的通道,这种类型的通道要求发送 goroutine 与接收 goroutine 要同时准备好,如果没有同时准备好,那么先发送或者先接收的 goroutine 就会进行阻塞等待。
//无缓冲通道示例
var wg1 sync.WaitGroup
//init初始化包,Go语言运行时会在其他代码执行前优先执行这个
func init() {
//设置随机数种子
rand.Seed(time.Now().UnixNano())
}
func main(){
court := make(chan int)
wg1.Add(2)
go player("A",court)
go player("B",court)
//开始发球
court<- 1
}
func player(name string,court chan int) {
defer wg1.Done()
for {
ball,ok := <-court //ok:表示接收到的值有效
if !ok{
//说明通道被关闭
fmt.Printf("name: %v is win\n",name)
return
}
n := rand.Intn(100)
if n%13 == 0{
fmt.Printf("Player %v is missed\n",name)
//通道已被关闭
close(court)
return
}
fmt.Printf("Player %v Hit %v\n",name,ball)
ball++
//将球打回对方
court <- ball
}
}
有缓冲的通道是指在通道数据被接收前有能力保存一个或多个的值,这种类型的通道并不要求发送 goroutine 与接收 goroutine 要同时准备好。只有在通道中没有多余的缓存空间保存值的时候,发送 goroutine 的动作才会进行阻塞,同理只有在通道中没有要接收的值时,接收动作才会被阻塞。如果缓冲区已满,再往通道发送数据时就会报错
func main() {
ch := make(chan int, 1)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
----output----
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
D:/GoDemo/src/MyGo/Demo_03.go:8 +0x7a
与无缓冲通道相比不同点:无缓冲通道能保证通道中发送与接收动作是同时进行的,而有缓冲的通道则不会保证。
通道关闭:
在有缓存的通道执行通道关闭( close(court))后,goroutine依旧可以从通道中接收数据,这样有利于将缓存中的所有数据都接收,从而不会使得数据丢失,但是并不能往通道中发送数据。只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有值需要发送的时候才有必要关闭,例如终止一个 range 循环。