协程的基本概念
注意 协程的概念 每个语言都有所不同,这里只说下go的概念
简单来说 go的协程 具有下面几个特点
轻量级,比线程要小很多。
非抢占式
啥叫非抢占式?
线程是抢占的,因为线程的调度 会有操作系统的干预,而go的协程 并不属于os的资源,os是没办法调度的,只有go的编程者自己有能力来处理,如果你不处理 就不会干预
多个协程 有可能在 一个或者多个线程上运行
go的协程是一个 编译器 层面的概念,大家不用深究他的原理,初学者只要掌握其用法就好
举个例子:
func print(i int) {
for {
fmt.Printf("i=%d\n", i)
}
}
func main() {
for i := 0; i < 10; i++ {
go print(i)
}
time.Sleep(10 * time.Millisecond)
}
这段程序的输出 显然是 会有很多种 i=0 一直到i=9 在打印
channel
下面就是一个简单的channel 例子
func chanDemo() {
c := make(chan int)
go func() {
for {
n := <-c
fmt.Println(n)
}
}()
c <- 1
c <- 2
time.Sleep(time.Millisecond)
}
func main() {
chanDemo()
}
往c 里面写了1和2 然后在另外一个goroutine里面 打印
接下来构建一个稍微复杂的程序,构建10个chan,分别打印自己的信息
func worker(id int, c chan int) {
for {
fmt.Println("worker ", id, "received ", string(rune(<-c)))
}
}
func chanDemo() {
var chanArray [10]chan int
for i := 0; i < 10; i++ {
chanArray[i] = make(chan int)
go worker(i, chanArray[i])
}
for i := 0; i < 10; i++ {
chanArray[i] <- 'a' + i
}
time.Sleep(time.Millisecond)
}
这里可能会有人问 我发的顺序是abcdefg 为啥接收的时候 顺序就乱掉了?
其实这里其实是因为 打印语句 本身是个io操作,这个io操作是资源共享的,会存在资源竞争的问题
所以最终出来的结果就是乱序的,因为谁能抢到这个io资源 并不确定,这是随机的。
更明确的chan 写法
上述的代码我们了解到,chan 可以收也可以发,但是有时候 我们作为 chan的提供者 希望明确告诉使用者你的chan到底是发还是收的时候 应该怎么办?
// 创建一个chan 这个chan 只能用于发数据
func createWorker(id int) chan<- int {
c := make(chan int)
go func() {
for {
// 但是在内部 我们仍旧可以用这个chan 来收数据
fmt.Println("worker ", id, "received ", string(rune(<-c)))
}
}()
return c
}
func chanDemo() {
//这里定的chan 就是一个只能发输出的chan了
var chanArray [10]chan<- int
for i := 0; i < 10; i++ {
chanArray[i] = createWorker(i)
//go worker(i, chanArray[i])
}
for i := 0; i < 10; i++ {
chanArray[i] <- 'a' + i
}
time.Sleep(time.Millisecond)
}
func main() {
chanDemo()
}
bufferchannel
func bufferChan() {
c := make(chan int)
c <- 1
}
func main() {
//chanDemo()
bufferChan()
time.Sleep(time.Millisecond)
}
这里程序执行会错误,因为你这个c 只有发送 没有收,在go中 这样操作是不被允许的
但是有时候我们在创建chan的时候 发前面几组数据的时候并不需要接收 这个时候 就可以利用buffer 来处理了
func bufferChan() {
c := make(chan int, 3)
c <- 1
c <- 2
c <- 3
}
比如说 这样就完全没有问题,因为我们发送的数据 先到了chan
关闭chan
func bufferChan() {
c := make(chan int, 3)
go func() {
for {
fmt.Println("received ", <-c)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
}
可以看下 这里的运行结果,
这里就很奇怪了,为啥我们close 以后 还会收到 0 这个数据? 其实这是go的特性,你关闭了chan以后 chan还会不停的发送 chan类型的默认值,我们这里类型是int 那自然默认值就是0了,
func bufferChan() {
c := make(chan int, 3)
go func() {
for {
v, ok := <-c
if !ok {
fmt.Println("closed")
break
}
fmt.Println("received ", v)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
}
当然我们还有一种更加简单的写法:
func bufferChan2() {
c := make(chan int, 3)
go func() {
for v := range c {
fmt.Println("received ", v)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
}
不要通过共享内存来通信,通过通信来共享内存
看看如何来理解这个go语言并发的设计哲学
前面的例子 我们能看出来 我们是通过time sleep 来控制 让程序不要退出的。 这个十分的粗暴,我们想做到的是 当你全部打印结束的时候 程序自动退出就可以了。这应该怎么做?
// 首先定义一个 worker
// in 代表 输入
// done 代表其实就是输出了 我要告诉外面 我的活干完了
type Worker struct {
in chan int
done chan bool
}
看看怎么告诉外面我们活干完了? 其实就是打印结束以后 向done里面发送一个数据
func doWorker(worker *Worker) {
go func() {
for {
fmt.Println("receive:", <-worker.in)
worker.done <- true
}
}()
}
再看看 如何创建worker?
func createWorker(id int) Worker {
w := Worker{
in: make(chan int),
done: make(chan bool),
}
doWorker(&w)
return w
}
创建的过程比较简单
最后看下主函数
func main() {
var chanArray [10]Worker
// 先创建10个worker
for i := 0; i < 10; i++ {
chanArray[i] = createWorker(i)
}
// 再向worker里面写入数据
for i := 0; i < 10; i++ {
chanArray[i].in <- i
<-chanArray[i].done
}
}
先看下执行结果:
这一次我们没有在main函数里面执行 time。sleep 但是依旧 执行了每个打印语句
但是这个打印语句有问题吧?
为啥变成顺序的了?
你这顺序执行等于没并发能力啊
仔细看啊 我们for循环中 这里 发送完数据以后 有一个获取done这个chan的操作, 这个操作 不执行完 程序不会继续执行循环的, 所以等于是一个顺序的操作了, 这里我们再次换一个写法:
func main() {
var chanArray [10]Worker
// 先创建10个worker
for i := 0; i < 10; i++ {
chanArray[i] = createWorker(i)
}
// 再向worker里面写入数据
for i := 0; i < 10; i++ {
chanArray[i].in <- i
}
for _, w := range chanArray {
<-w.done
}
}
这次就变成了 我先发, 发完了以后 我再弄个循环去接受:
这次这个执行结果就正确了。
再修改一下 提高一下难度
我们这次 发完10个数据以后 想接着再发下一个10数据, 要两次发送的数据 都print结束以后 程序才能退出
怎么处理?
func main() {
var chanArray [10]Worker
// 先创建10个worker
for i := 0; i < 10; i++ {
chanArray[i] = createWorker(i)
}
// 再向worker里面写入数据
for i := 0; i < 10; i++ {
chanArray[i].in <- i
}
// 再向worker里面写入数据 这次写的是10-19
for i := 0; i < 10; i++ {
chanArray[i].in <- i + 10
}
for _, w := range chanArray {
<-w.done
<-w.done
}
}
看样子好像没错呀, 连续发送2次,然后接收端 收到2个消息才能 结束
但是run起来以后 程序报错了,为啥?
因为我们第一次0-9的数据发送完毕以后 我们对每个worker触发了一次done的写入, 但是这次done触发写入以后 没有地方去收这个done的写入了, 然后 我们马上又开始对worker进行10-19的 写入了
针对于这个问题呢,有一种简单的修复写法:
func doWorker(worker *Worker) {
go func() {
for {
fmt.Println("receive:", <-worker.in)
go func() {
worker.done <- true
}()
}
}()
}
这里为啥正确呢
因为你重新开了一个 goroutine 去 写入数据 不会卡死 之前的 routine的循环了。
更简单的等待写法
type Worker struct {
in chan int
wg *sync.WaitGroup
}
func createWorker(id int, group *sync.WaitGroup) Worker {
w := Worker{
in: make(chan int),
wg: group,
}
doWorker(&w)
return w
}
func doWorker(worker *Worker) {
go func() {
for {
fmt.Println("receive:", <-worker.in)
worker.wg.Done()
}
}()
}
其实这里就是利用官方提供的 waitgroup来做等待
func main() {
var wg sync.WaitGroup
var chanArray [10]Worker
// 先创建10个worker
for i := 0; i < 10; i++ {
chanArray[i] = createWorker(i, &wg)
}
wg.Add(20)
// 再向worker里面写入数据
for i := 0; i < 10; i++ {
chanArray[i].in <- i
}
// 再向worker里面写入数据 这次写的是10-19
for i := 0; i < 10; i++ {
chanArray[i].in <- i + 10
}
wg.Wait()
}
Add 代表 有20个任务进去了, wait 就代表等待 等待这20个任务 全部done以后 就结束。