GO并发实例

129 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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协程执行完之后,子协程也结束,所以子协程可能不会执行完毕就退出。 我们有几种方法:

  1. main协程Sleep一段时间,如下:
go gopher1.MakeCoffee("Latte")
go gopher2.MakeCoffee("Luksusowa")
time.Sleep(10 * time.Second)

如果等了10s后两个子协程还没执行完,main协程会往下执行,然后结束掉,导致子协程提前结束,我们在测试的时候可以按情况调整睡眠时间。但这不是一种好的方法,因为睡眠的代价能大,我们可以考虑其它方法。

  1. 使用select{}阻塞main协程,这样main协程一直不会退出,但这样程序一直不会退出。
  2. 使用信道来做一次阻塞
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{}{},一直不会有信道接收,所以会导致死锁。

  1. 使用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