开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第13天,点击查看活动详情
并发是指在一段时间内能够有多个协程访问同一个资源,微观上它们还是串行执行的。例如我们一个咖啡机,在8:00-8:10分有10个人排队并使用它制造、取出、并喝掉咖啡,那我们可以认为在这段时间内有10个人同时使用了它,因为最终的效果是大家都喝到了咖啡。并行就是我们有多台咖啡机,在同一时刻能够有不同的人同时使用咖啡机。
并发示例
我们可以抽象一个咖啡机、员工,每个员工能用咖啡机磨不同的咖啡,然后从咖啡机中取出咖啡、喝掉咖啡。
coffeemachine.go
type CoffeeMachine struct {
CoffeeName string //当前正在碾磨的咖啡名称
Gopher //使用咖啡机的员工
Mlock sync.Mutex //锁,保证同一时刻只能有一个员工占用它,直到取出咖啡才释放
}
gopher.go
type Gopher struct {
GopherName string //员工名称
GopherId int //员工ID
CoffeeName string //要研磨咖啡的名称
}
我们应当在gopher.go中对Gopher结构体实现磨咖啡、取咖啡、喝咖啡的动作。
下面是文件结构 -onecore -coffeemachine.go -gopher.go go.mod main.go
coffeemachine.go
package onecore
import "sync"
type CoffeeMachine struct {
CoffeeName string
Gopher
Mlock sync.Mutex
}
gopher.go
package onecore
import (
"fmt"
)
type Gopher struct {
GopherName string
GopherId int
CoffeeName string
}
//实现MakeCoffee方法
//实现TakeCoffee方法
//实现DrinkCoffee方法
var coffeeMachine = CoffeeMachine{} //全局变量
func (g *Gopher) MakeCoffee(CoffeeName string) {
//实现MakeCoffee方法
//没有coffee才能MakeCoffee
coffeeMachine.Mlock.Lock()
if coffeeMachine.CoffeeName == "" {
coffeeMachine.CoffeeName = CoffeeName
coffeeMachine.Gopher = *g
fmt.Printf("%v is Making %v Coffee, his id is %v\n", g.GopherName, CoffeeName, g.GopherId)
}
g.TakeCoffee()
coffeeMachine.Mlock.Unlock()
g.DrinkCoffee()
}
func (g *Gopher) TakeCoffee() {
//实现TakeCoffee方法
//有coffee才能TakeCoffee
if coffeeMachine.CoffeeName != "" {
g.CoffeeName = coffeeMachine.CoffeeName
fmt.Printf("name=%v id=%v is taking %v Coffee\n", g.GopherId, g.GopherName, g.CoffeeName)
coffeeMachine.CoffeeName = "" //取完coffee后,清空coffee
} else {
fmt.Printf("%v is Waiting for Coffee\n", g.GopherName)
}
}
func (g *Gopher) DrinkCoffee() {
//实现DrinkCoffee方法
//有coffee才能DrinkCoffee
if g.CoffeeName != "" {
fmt.Printf("name=%v id=%v is Drinking %v Coffee\n", g.GopherId, g.GopherName, g.CoffeeName)
} else {
fmt.Printf("%v has no coffee to drink\n", g.GopherName)
}
}
main.go
package main
import (
onecore "Concurrency/oneCore"
"runtime"
"sync"
"time"
)
func main() {
runtime.GOMAXPROCS(1) // 1 CPU core
gopher1 := onecore.Gopher{GopherName: "gopher1", GopherId: 1}
gopher2 := onecore.Gopher{GopherName: "gopher2", GopherId: 2}
//go gopher1.MakeCoffee("Latte")
//go gopher2.MakeCoffee("Luksusowa")
//time.Sleep(10 * time.Second)
//如果不Sleep,main的go程会立即退出,导致子go程也退出
//当父协程是main协程时,父协程退出,父协程下的所有子协程也会跟着退出;当父协程不是main协程时,父协程退出,父协程下的所有子协程并不会跟着退出
//由于Sleep的开销很大,我们一般用waitGroup方法来解决
var wg sync.WaitGroup
wg.Add(2)
go func() {
go gopher1.MakeCoffee("Latte")
wg.Done() //这个wg.Done()也可以放在MakeCoffee()的最后一行,这样就不需要匿名函数了,但是MakeCoffee得加上wg *sync.WaitGroup参数
}()
go func() {
go gopher2.MakeCoffee("Luksusowa")
wg.Done()
}()
wg.Wait()
}
注意一下main协程和子协程的关系: 当父协程是main协程时,父协程退出,父协程下的所有子协程也会跟着退出;当父协程不是main协程时,父协程退出,父协程下的所有子协程并不会跟着退出。当 go gopher1.MakeCoffee 时,就会启动一个子协程,main协程和子协程会并发执行,当main协程执行完之后,子协程也结束,所以子协程可能不会执行完毕就退出。 我们有几种方法:
- main协程Sleep一段时间,如下:
go gopher1.MakeCoffee("Latte")
go gopher2.MakeCoffee("Luksusowa")
time.Sleep(10 * time.Second)
如果等了10s后两个子协程还没执行完,main协程会往下执行,然后结束掉,导致子协程提前结束,我们在测试的时候可以按情况调整睡眠时间。但这不是一种好的方法,因为睡眠的代价能大,我们可以考虑其它方法。
- 使用select{}阻塞main协程,这样main协程一直不会退出,但这样程序一直不会退出。
- 使用信道来做一次阻塞
var c = make(chan struct{})
go func() {
gopher1.MakeCoffee("Latte")
gopher2.MakeCoffee("Luksusowa")
c <- struct{}{}
}()
<-c
这段代码开启一个go routine,它执行一个匿名函数(现在两个员工冲咖啡的顺序是确定的),另外main协程会到<-c这里执行,这是信道接收数据的操作,如果它一直没有收到,则会等待,所以main协程一直不会往下执行,只有当两个MakeCoffee完成之后,运行到c <- struct{}{} 时才会往下执行。另外,使用空结构体可以减少空间占用 。struct{}{}是类型struct{}的实例{}。
如果我们将代码改为:
var c = make(chan struct{})
func() {
go gopher1.MakeCoffee("Latte")
go gopher2.MakeCoffee("Luksusowa")
c <- struct{}{}
}()
<-c
程序会产生死锁,使用匿名函数运行两个子协程时,main协程不会运行到<-c这一步,等到两个子go程结束时,运行到c <- struct{}{},一直不会有信道接收,所以会导致死锁。
- 使用waitGroup,这种方法在实际中使用比较常见。
使用方法
wg.Add(2)
执行完一个go协程后就wg.Done()
执行完一个go协程后就wg.Done()
main协程会阻塞在wg.Wait()上,直到运行了两个wg.Done()
具体使用方法
package main
import (
"sync"
)
func main() {
// Create a WaitGroup
var wg sync.WaitGroup
wg.Add(2)
// Launch two goroutines, each of which will run a function
go func() {
// Call Done to indicate that the goroutine is finished
defer wg.Done()
// code for the goroutine goes here
}()
go func() {
// Call Done to indicate that the goroutine is finished
defer wg.Done()
// code for the goroutine goes here
}()
// Wait for the goroutines to finish
wg.Wait()
// At this point, both goroutines have finished
}
上述咖啡实例的输出如下:
gopher1 is Making Latte Coffee, his id is 1
name=1 id=gopher1 is taking Latte Coffee
name=1 id=gopher1 is Drinking Latte Coffee
gopher2 is Making Luksusowa Coffee, his id is 2
name=2 id=gopher2 is taking Luksusowa Coffee
name=2 id=gopher2 is Drinking Luksusowa Coffee