多线程与多进程本质的区别在于,多线程的内存空间是共享的,多进程是每一个进程都会独立开辟一块内存空间。如果我们运行的多个任务是完全独立的,那么在资源足够的情况下并发还是并行方案都无所谓了。但如果我们的多个任务之间有内在联系,那任务间的通信就是个问题了。由于groutine兼具多线程与多进程的特性,所以groutine也具备了多进程内存独立的特点。这时候我们就需要channel工具,来帮助groutine实现任务之间的通信。

以典型的生产者和消费者模型为例。一家寿司餐馆: 1个厨师,2个食客。厨师只负责生产菜品放到流水线上,2个食客自己从流水线拿取菜品。流水线比较短,没人取餐的话最多只能存放3个菜品

package main

import (
"fmt"
"sync"
"time"
)

// 声明WaitGroup,用于确保主groutine一定晚于子groutine结束
var wg sync.WaitGroup

// 将通道类型作为参数传递给Chef函数
func Chef(ch chan<- int) {
// defer函数的执行顺序是反向的,先出现的defer后运行
defer wg.Done()
// 函数结束前关闭通道的使用,避免因通道一直开启hold住代码
// 确定写完数据就关闭通道,通道只能被关闭一次
defer close(ch)
for i := 1; i < 6; i++ {
// 向通道中推送数据
ch <- i
fmt.Printf("厨师制作第 < %d > 盘菜品\n", i)
time.Sleep(time.Second * 1)
}
}

// 单向chan只允许取出数据
func Comsumer_1(ch <-chan int) {
defer wg.Done()
// 获取通道的内数据的方式一
for {
dish, ok := <-ch
// 当通道数据内没有数据时,则ok返回false
if !ok {
break
}
fmt.Printf("食客 [ %d ] 号吃了第 < %d > 盘菜品>\n", 1, dish)
}
}

func Comsumer_2(ch <-chan int) {
defer wg.Done()
// 获取通道的内数据的方式二
// 只返回一个通道重点数据值,当通道为空时自动结束for循环
for dish := range ch {
fmt.Printf("食客 [ %d ] 号 已经吃了第 < %d > 盘菜品>\n", 2, dish)
time.Sleep(time.Second * 2)
}
}

func main() {
// 声明一个可以缓存3个数据的channel, 允许传输的数据类型是int
ch := make(chan int, 3)

wg.Add(3)
go Chef(ch)
go Comsumer_1(ch)
go Comsumer_2(ch)

wg.Wait()

fmt.Println("程序执行结束")
}

执行结果,2个食客任务交替消费厨师任务推送进通道的数据。

% go run main.go
厨师制作第 < 1 > 盘菜品
食客 [ 2 ] 号 已经吃了第 < 1 > 盘菜品>
厨师制作第 < 2 > 盘菜品
食客 [ 1 ] 号吃了第 < 2 > 盘菜品>
厨师制作第 < 3 > 盘菜品
食客 [ 1 ] 号吃了第 < 3 > 盘菜品>
厨师制作第 < 4 > 盘菜品
食客 [ 2 ] 号 已经吃了第 < 4 > 盘菜品>
厨师制作第 < 5 > 盘菜品
食客 [ 1 ] 号吃了第 < 5 > 盘菜品>
程序执行结束

groutine池

虽然创建和销毁groutine的开销很小,但是我们依然希望可以在任务很多的时候,把groutine数量控制在一定范围内,循环使用避免无意义的性能开销。根据上面餐厅的案例,我们让厨师累计生产大量菜品(10个)放到流水线,而食客的数量保持与cpu内核数相等。代码如下:

package main

import (
"fmt"
"runtime"
"sync"
"time"
)

// 声明WaitGroup,用于确保主groutine一定晚于子groutine结束
var wg sync.WaitGroup

// 将通道类型作为参数传递给Chef函数
func Chef(ch chan<- int) {
defer close(ch)
// 厨师制作10盘磁盘,放入通道
for i := 1; i < 11; i++ {
// 向通道中推送数据
ch <- i
fmt.Printf("厨师制作第 < %d > 盘菜品\n", i)
time.Sleep(time.Second * 1)

}
}

// 单向chan只取出流水线上的菜品,吃完以后将结果放入result通道
func Comsumer(cost_num int, ch <-chan int, result chan<- string) {
defer wg.Done()
//defer close(result)
// 循环从通道中获取数据,直到取完
for {
dish, ok := <-ch
// 当通道数据内没有数据时,则ok返回false
if !ok {
break
}
msg := fmt.Sprintf("食客 [ %d ] 号吃了第 < %d > 盘菜品>\n", cost_num+1, dish)
// 吃完以后要记账,结果写到result通道去。
result <- msg
time.Sleep(time.Second * 2)
}
}

func main() {
// 声明一个可以缓存3个数据的channel, 允许传输的数据类型是int
ch := make(chan int, 3)
result := make(chan string, 10)

// 获取当前电脑的CPU内核数
cpu_num := runtime.NumCPU()
fmt.Printf("当前电脑CPU内核数为: %v\n", cpu_num)

// 主goroutine不能操作通道,必须使用子groutine才能读写通道
// 启动一个负责生产的子groutine
go Chef(ch)

// 启动groutine数目等于cpu内核数
wg.Add(cpu_num)

for num := 0; num < cpu_num; num++ {
// 启动多个食客groutine,并发从通道拿去食物。
// 通道取完后,才停止各个食客任务
go Comsumer(num, ch, result)

}

wg.Wait()

// result 通道必须等所有子groutine都关闭以后才可以关闭。
// 关闭早了其他子groutine还没用完,就会报错
close(result)

fmt.Println(" #### 主grouine开始从result通道获取并打印账单 ####")

// 在主groutine中读取result通道的内容
for ret := range result {
fmt.Println(ret)
}

fmt.Println("程序执行结束")
}

执行结果,最多8个食客争相从通道中消费了10个菜品。消费记录不在与厨师生产记录交替打印了。食客的账单是主groutine从result通道获取并打印机的

% go run main.go
当前电脑CPU内核数为: 8
厨师制作第 < 1 > 盘菜品
厨师制作第 < 2 > 盘菜品
厨师制作第 < 3 > 盘菜品
厨师制作第 < 4 > 盘菜品
厨师制作第 < 5 > 盘菜品
厨师制作第 < 6 > 盘菜品
厨师制作第 < 7 > 盘菜品
厨师制作第 < 8 > 盘菜品
厨师制作第 < 9 > 盘菜品
厨师制作第 < 10 > 盘菜品
#### 主grouine开始从result通道获取并打印账单 ####
食客 [ 8 ] 号吃了第 < 1 > 盘菜品>

食客 [ 4 ] 号吃了第 < 2 > 盘菜品>

食客 [ 5 ] 号吃了第 < 3 > 盘菜品>

食客 [ 6 ] 号吃了第 < 4 > 盘菜品>

食客 [ 7 ] 号吃了第 < 5 > 盘菜品>

食客 [ 2 ] 号吃了第 < 6 > 盘菜品>

食客 [ 3 ] 号吃了第 < 7 > 盘菜品>

食客 [ 1 ] 号吃了第 < 8 > 盘菜品>

食客 [ 8 ] 号吃了第 < 9 > 盘菜品>

食客 [ 4 ] 号吃了第 < 10 > 盘菜品>

程序执行结束


总结:

  • 通道只能被关闭一次,关闭的通道依然可以被其他任务获取数据。所以写操作完成就可以关闭隧道了。通道的关闭操作要谨慎,尤其多个groutine都涉及通道使用的时候。关闭早了其他还在运行写任务的子groutine就会报错了。
  •  Comsumer_1(ch <-chan int) 这种方式声明的是只能单向读取的通道,若声明为 Comsumer_1(ch chan int) 没有箭头指向的话,就是个双向通道,在函数中可以读写
  • Chef(ch chan<- int) 这种方式就是只能写数据单向通道,数据类型是int
  • ch <- i 是向通道中写数据。 i, ok <- ch 是从通道中读数据,通道为空的时候ok的值返回false
  • ch := make(chan int, 3) 生命都是带3个数据缓存的通道,即在通道未达到缓存上限之前,生产者把数据放到通道既可进行下一步运行。若声明为ch := make(chan int) 则表示声明的是无缓存的同步通道。即生产者把消息放入通道后,生产者流程会hold住,直到有消费者来取走消息。
  • 一个channel同时仅允许被一个goroutine读写。
  • 主的groutine尽量被安排用来做读取通道的操作,写操作容易hold住程序流程,建议放在子groutine中处理。