go 并发 chan :以生产者消费者问题为实例
本文及并发系列博客资料来源:udemy 上《working with concurrency in go》,Trevor Sawler老师讲课非常细致易懂,感兴趣可以去支持一下他
以一个披萨店的生产者消费者模型来讲解chan的运用,英语注释为老师的注释原文
package main
import (
"fmt"
"math/rand"
"time"
"github.com/fatih/color"
)
const NumberOfPizzas = 10
var pizzasMade, pizzasFailed, total int
// Producer is a type for structs that holds two channels: one for pizzas, with all
// information for a given pizza order including whether it was made
// successfully, and another to handle end of processing (when we quit the channel)
type Producer struct {
data chan PizzaOrder
quit chan chan error
}
// PizzaOrder is a type for structs that describes a given pizza order. It has the order
// number, a message indicating what happened to the order, and a boolean
// indicating if the order was successfully completed.
type PizzaOrder struct {
pizzaNumber int
message string
success bool
}
// Close is simply a method of closing the channel when we are done with it (i.e.
// something is pushed to the quit channel)
func (p *Producer) Close() error {
ch := make(chan error)
p.quit <- ch
return <-ch
}
// makePizza attempts to make a pizza. We generate a random number from 1-12,
// and put in two cases where we can't make the pizza in time. Otherwise,
// we make the pizza without issue. To make things interesting, each pizza
// will take a different length of time to produce (some pizzas are harder than others).
func makePizza(pizzaNumber int) *PizzaOrder {
pizzaNumber++
if pizzaNumber <= NumberOfPizzas {
delay := rand.Intn(5) + 1
fmt.Printf("Received order #%d!\n", pizzaNumber)
rnd := rand.Intn(12) + 1
msg := ""
success := false
if rnd < 5 {
pizzasFailed++
} else {
pizzasMade++
}
total++
fmt.Printf("Making pizza #%d. It will take %d seconds....\n", pizzaNumber, delay)
// delay for a bit
time.Sleep(time.Duration(delay) * time.Second)
if rnd <=2 {
msg = fmt.Sprintf("*** We ran out of ingredients for pizza #%d!", pizzaNumber)
} else if rnd <= 4 {
msg = fmt.Sprintf("*** The cook quit while making pizza #%d!", pizzaNumber)
} else {
success = true
msg = fmt.Sprintf("Pizza order #%d is ready!", pizzaNumber)
}
p := PizzaOrder{
pizzaNumber: pizzaNumber,
message: msg,
success: success,
}
return &p
}
return &PizzaOrder{
pizzaNumber: pizzaNumber,
}
}
// pizzeria is a goroutine that runs in the background and
// calls makePizza to try to make one order each time it iterates through
// the for loop. It executes until it receives something on the quit
// channel. The quit channel does not receive anything until the consumer
// sends it (when the number of orders is greater than or equal to the
// constant NumberOfPizzas).
func pizzeria(pizzaMaker *Producer) {
// keep track of which pizza we are making
var i = 0
// this loop will continue to execute, trying to make pizzas,
// until the quit channel receives something.
for {
currentPizza := makePizza(i)
if currentPizza != nil {
i = currentPizza.pizzaNumber
select {
// we tried to make a pizza (we send something to the data channel -- a chan PizzaOrder)
case pizzaMaker.data <- *currentPizza:
// we want to quit, so send pizzMaker.quit to the quitChan (a chan error)
case quitChan := <-pizzaMaker.quit:
// close channels
close(pizzaMaker.data)
close(quitChan)
return
}
}
}
}
func main() {
// seed the random number generator
rand.Seed(time.Now().UnixNano())
// print out a message
color.Cyan("The Pizzeria is open for business!")
color.Cyan("----------------------------------")
// create a producer
pizzaJob := &Producer{
data: make(chan PizzaOrder),
quit: make(chan chan error),
}
// run the producer in the background
go pizzeria(pizzaJob)
// create and run consumer
for i := range pizzaJob.data {
if i.pizzaNumber <= NumberOfPizzas {
if i.success {
color.Green(i.message)
color.Green("Order #%d is out for delivery!", i.pizzaNumber)
} else {
color.Red(i.message)
color.Red("The customer is really mad!")
}
} else {
color.Cyan("Done making pizzas...")
err := pizzaJob.Close()
if err != nil {
color.Red("*** Error closing channel!", err)
}
}
}
// print out the ending message
color.Cyan("-----------------")
color.Cyan("Done for the day.")
color.Cyan("We made %d pizzas, but failed to make %d, with %d attempts in total.", pizzasMade, pizzasFailed, total)
switch {
case pizzasFailed > 9:
color.Red("It was an awful day...")
case pizzasFailed >= 6:
color.Red("It was not a very good day...")
case pizzasFailed >= 4:
color.Yellow("It was an okay day....")
case pizzasFailed >= 2:
color.Yellow("It was a pretty good day!")
default:
color.Green("It was a great day!")
}
}
我们从main看起,先rand.seed初始化了随机数相关的种子,因为后面要用到随机数生成函数。
// seed the random number generator
rand.Seed(time.Now().UnixNano())
color包是打印多彩字体用的,不用管
// print out a message
color.Cyan("The Pizzeria is open for business!")
color.Cyan("----------------------------------")
创建生产者,这里用到了一个自定义的类型Producer的指针类型(&表示取地址)
// create a producer
pizzaJob := &Producer{
data: make(chan PizzaOrder),
quit: make(chan chan error),
}
我们来看下这个类型的定义,producer包含data和quit两个变量,data是一个接收自定义的类型pizzaorder的chan,这个pizzaorder包含三个变量,分别是订单号、信息、成功判断。这个变量的作用是接收后面做的披萨的信息
quit就比较有意思了,类型为接收chan error 的chan,我搜了下好像普罗米修斯也有类似的写法,这种写法的目的是为了让我们能够决定信息所传递的位置,可以看到下面的代码给producer添加了一个closer方法。这个变量的用处是判断producer的状态
// Producer is a type for structs that holds two channels: one for pizzas, with all
// information for a given pizza order including whether it was made
// successfully, and another to handle end of processing (when we quit the channel)
type Producer struct {
data chan PizzaOrder
quit chan chan error
}
// PizzaOrder is a type for structs that describes a given pizza order. It has the order
// number, a message indicating what happened to the order, and a boolean
// indicating if the order was successfully completed.
type PizzaOrder struct {
pizzaNumber int
message string
success bool
}
// Close is simply a method of closing the channel when we are done with it (i.e.
// something is pushed to the quit channel)
func (p *Producer) Close() error {
ch := make(chan error)
p.quit <- ch
return <-ch
}
接下来,用协程运行pizzera,并用一个循环来获取并判断上面定义的pizzajob里面的data并根据data做出响应的输出
我们先来看pizzera,他的参数为一个pizzaMaker *Producer,定义了一个数作为订单数并调用了makepizza()
来看这个makepizza,他接收一个整数,然后根据随机数来判断一个披萨是否制作成功,调用sleep假装制作,最后返回一个包含有这个披萨信息的pizzaorder类型的指针。如果超过了设定的最大披萨数,他会返回一个只包含订单号的&pizzaorder
来继续看pizzera,他用一个currentPizza来接住makepizza返回的指针,如果返回的这个pizzaorder里面有数据,把他发给job的data里面,但是如果job.quit有消息,那么就关闭channel并退出这个函数
然后来看消费者,取job.data这个channel里面的数据,如果没有超过披萨上限,那么根据披萨的状态来打印对应的值,否则,超出上限代表着所有的披萨都做完了,那么调用job.Close(),给quit发消息,这时正在运行的协程发现这点之后就会关闭channel并退出
// run the producer in the background
go pizzeria(pizzaJob)
// create and run consumer
for i := range pizzaJob.data {
if i.pizzaNumber <= NumberOfPizzas {
if i.success {
color.Green(i.message)
color.Green("Order #%d is out for delivery!", i.pizzaNumber)
} else {
color.Red(i.message)
color.Red("The customer is really mad!")
}
} else {
color.Cyan("Done making pizzas...")
err := pizzaJob.Close()
if err != nil {
color.Red("*** Error closing channel!", err)
}
}
}
// pizzeria is a goroutine that runs in the background and
// calls makePizza to try to make one order each time it iterates through
// the for loop. It executes until it receives something on the quit
// channel. The quit channel does not receive anything until the consumer
// sends it (when the number of orders is greater than or equal to the
// constant NumberOfPizzas).
func pizzeria(pizzaMaker *Producer) {
// keep track of which pizza we are making
var i = 0
// this loop will continue to execute, trying to make pizzas,
// until the quit channel receives something.
for {
currentPizza := makePizza(i)
if currentPizza != nil {
i = currentPizza.pizzaNumber
select {
// we tried to make a pizza (we send something to the data channel -- a chan PizzaOrder)
case pizzaMaker.data <- *currentPizza:
// we want to quit, so send pizzMaker.quit to the quitChan (a chan error)
case quitChan := <-pizzaMaker.quit:
// close channels
close(pizzaMaker.data)
close(quitChan)
return
}
}
}
}
// makePizza attempts to make a pizza. We generate a random number from 1-12,
// and put in two cases where we can't make the pizza in time. Otherwise,
// we make the pizza without issue. To make things interesting, each pizza
// will take a different length of time to produce (some pizzas are harder than others).
func makePizza(pizzaNumber int) *PizzaOrder {
pizzaNumber++
if pizzaNumber <= NumberOfPizzas {
delay := rand.Intn(5) + 1
fmt.Printf("Received order #%d!\n", pizzaNumber)
rnd := rand.Intn(12) + 1
msg := ""
success := false
if rnd < 5 {
pizzasFailed++
} else {
pizzasMade++
}
total++
fmt.Printf("Making pizza #%d. It will take %d seconds....\n", pizzaNumber, delay)
// delay for a bit
time.Sleep(time.Duration(delay) * time.Second)
if rnd <= 2 {
msg = fmt.Sprintf("*** We ran out of ingredients for pizza #%d!", pizzaNumber)
} else if rnd <= 4 {
msg = fmt.Sprintf("*** The cook quit while making pizza #%d!", pizzaNumber)
} else {
success = true
msg = fmt.Sprintf("Pizza order #%d is ready!", pizzaNumber)
}
p := PizzaOrder{
pizzaNumber: pizzaNumber,
message: msg,
success: success,
}
return &p
}
return &PizzaOrder{
pizzaNumber: pizzaNumber,
}
}
整个案例,使用go协程来运行生产者,消费者用for循环监听在生产者的channel,并控制何时给生产者发送停止信号
一次运行结果如下
go run .
The Pizzeria is open for business!
----------------------------------
Received order #1!
Making pizza #1. It will take 5 seconds....
Received order #2!
Making pizza #2. It will take 2 seconds....
*** We ran out of ingredients for pizza #1!
The customer is really mad!
Received order #3!
Making pizza #3. It will take 5 seconds....
*** We ran out of ingredients for pizza #2!
The customer is really mad!
Received order #4!
Making pizza #4. It will take 1 seconds....
Pizza order #3 is ready!
Order #3 is out for delivery!
Received order #5!
Making pizza #5. It will take 1 seconds....
Pizza order #4 is ready!
Order #4 is out for delivery!
Received order #6!
Making pizza #6. It will take 3 seconds....
Pizza order #5 is ready!
Order #5 is out for delivery!
Received order #7!
Making pizza #7. It will take 5 seconds....
*** We ran out of ingredients for pizza #6!
The customer is really mad!
Received order #8!
Making pizza #8. It will take 3 seconds....
Pizza order #7 is ready!
Order #7 is out for delivery!
Received order #9!
Making pizza #9. It will take 3 seconds....
Pizza order #8 is ready!
Order #8 is out for delivery!
Received order #10!
Making pizza #10. It will take 3 seconds....
Pizza order #9 is ready!
Order #9 is out for delivery!
Pizza order #10 is ready!
Order #10 is out for delivery!
Done making pizzas...
-----------------
Done for the day.
We made 7 pizzas, but failed to make 3, with 10 attempts in total.
It was a pretty good day!
评论已关闭