本文及并发系列博客资料来源: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!

标签: none

评论已关闭