Golang goroutine与channel

-beyond · · 682 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

  使用goroutine的方法很简单,直接在语句前面加go关键字即可,如果是多核处理器的电脑,使用gorountine,就会在另外一个CPU上执行goroutine,子协程不一定会和主协程在一个CPU上执行。

  这里有两个注意的地方,使用go关键字的进程称之为子协程,而没有使用go关键字的进程称之为主协程,在多CPU的机器上,如果有多个协程,那么这些协程的执行顺序以及执行完成的顺序都是不确定的,但有一点,如果主协程结束,那么整个进程就结束了,不论子协程是否结束,整个进程都结束了,也就看不到子协程的运行结果了。

示例1

package main
import "fmt"
func main() {
	go fmt.Println("hello")  //子协程
	fmt.Println("world")   //主协程
}

  上面的代码执行之后,会有三种结果:

  1、hello world      2、world       3、world hello

  上面这三个结果都是正确的。

  对于第一种,子协程在一个CPU上面执行输出hello之后,再轮到主协程中执行输出world。

  对于第二种,子协程在一个CPU上还没来得及执行,或者说没有执行完成,但是主协程已经执行完成了,此时整个进程就运行完毕了,所以就看不到子协程的运行结果了。

  对于第三种,主协程中语句还没执行完成的时候,子协程已经运行完毕,几乎同时完成,所以会出现着这种情况。

  可以参考下面这个图来理解:

      

 

创建channel

  channel的零值是nil

chi := make(chan int)
chs := make(chan string)
chif := make(chan interface{}) 
//因为可以认为所有的类型都实现了空接口
//所以这个chif的chan接收任何类型的chan

  

发送和接收channel

chi := make(chan int)
chi <- 3	//发送
i := <-chi	//接收,保存值
<-chi		//接收,不保存值

  

关闭channel

chi := make(chan int)
chi <- 3   //发送
i := <-chi //接收,保存值
<-chi      //接收,不保存值
close(chi) //关闭channel

  对一个已经关闭的channel发送数据,都会导致panic异常。对于一个已经关闭的channel,可以继续接收这个已关闭的channel中的数据,如果没有数据的话,接收到的就是chan类型的零值。

 

无缓存channel(同步channel)

  一个基于无缓存的channel:

  1、发送操作将导致发送者的gorountine阻塞,直到有两外一个goroutine在相同的channel上执行接收操作之后,发送者的阻塞才会解开。

  2、同理,如果接收操作先发生,那么接受者goroutine也将阻塞,直到另一个goroutine在相同的channel上执行发送操作,接受者的阻塞才会解开。

  基于无缓存的channel的发送和接收操作将导致两个goroutine做一次同步操作,所以也叫作同步channel。

  向一个无缓冲的channel中发送多次数据,即多个goroutine都向同一个goroutine中发送数据,并且只有一个协程来接收数据,这个时候会后什么问题吗?会发生覆盖?或者是只有第一个发送的值才会保留,后面发送的数据都无效?和有缓存channel有什么区别?

  请看下面的示例:

package main
import "fmt"
func main() {
	chi := make(chan int) //无缓冲
	go func() {
		chi <- 10
		chi <- 30
		chi <- 40
	}()
	//	fmt.Println(<-chi)		//10
	//	fmt.Println(<-chi, <-chi) 	//10 30
	//fmt.Println(<-chi, <-chi, <-chi)//10 30 40
	fmt.Println(<-chi, <-chi, <-chi, <-chi) //deadlock
}

  从上面的例子中可以看到,有一个gorountine向一个无缓冲的channel中发送三个数据,此时,分这几种情况:

  0、chi还没有来得及向channel中发送数据时,主协程阻塞,子协程可以向其中发送一个数据。

  1、 主协程只从channel中接收一次数据:子协程向chi中发送了一个数据,此时,自协程阻塞,等待chi中的数据被消费之后,才继续运行。 那么,主协程接收的第一个数据就是第一次向这个channel中发送的数据10,之后,channel空闲,发送者可以发送第二个数据30,子协程再次阻塞,等待主协程接收数据,但是此时主协程不接受数据,主协程已经执行完成了,那么整个进程就完成了,所以最终只输出10这一个数据。此时子协程也会跟着被结束。

  2、 主协程只从channel中接收两次数据:接着上一步,此时主协程第二次接收数据的话,接收到的是30;之后,chi空闲,子协程再次向chi中发送数据40,然后又阻塞,但是此时主协程已经完成,所以子协程再次被结束。

  3、主协程从channel中接收三次数据:接着上一步,此时主协程接收到的数据应该是40,之后,chi空闲,子协程也不会像其中发送数据了,同时,子协程也已经运行结束了,所以,主协程中输出10 30 40。

  4、主协程从channel中接收四次数据:接着上一步,因为chi已经空闲,且子协程已经结束,表示没有协程向chi中发送数据了,但是chi并没有关闭,那么主协程中第四次的接收数据就会一直等待,最终造成死锁。解决这个问题,可以在子协程中发送第三次数据40之后,执行close(chi),之后,主协程第四次接收数据便不会引起死锁,接收到的数据时0(chi的类型零值)

   

使用range遍历channel

package main
import (
	"fmt"
)
func main() {
	num := make(chan int)
	go func() {
		num <- 30
		num <- 40
		close(num)
	}()
	for x := range num {
		fmt.Println(x)
	}
	//输出
	//30
	//40
}

  使用range遍历channel时,从channel中接收数据,如果从channel中获取到了数据,那么返回的结果就是获取到的值;如果channel已经关闭,那么对于无缓冲的channel而言,就没有数据可获取了,但是range的结果是channel的类型零值。

串联channel

  下面是一个串联的channel,实现

package main
import (
	"fmt"
	"time"
)
func main() {
	num := make(chan int)
	sqrt := make(chan int)
	go func() {
		for x := 0; ; x++ {
			num <- x
			time.Sleep(time.Second)
		}
	}()
	go func() {
		for {
			x := <-num
			sqrt <- x * x
		}
	}()
	for {
		fmt.Println(<-sqrt)
	}
}

  上面这个代码中,存在问题:如果第一个子协程不再往num中发送数据,那么,第二个子协程就会阻塞,同理,主协程sqrt取数据也会阻塞,最终引发死锁。

  解决方法:

package main
import (
	"fmt"
	"time"
)
func main() {
	num := make(chan int)
	sqrt := make(chan int)
	go func() {
		for x := 0; x < 10; x++ {
			num <- x
			time.Sleep(time.Second)
		}
		//不再向num中发送数据时,关闭channel,防止死锁
		close(num)
	}()
	go func() {
		for x := range num {
			sqrt <- x * x
		}
		//num已经关闭了,所以也要关闭sqrt
		close(sqrt)
	}()
	for x := range sqrt {
		fmt.Println(x)
	}
}

  

 单向channel

  使用channel作为参数进行通信时,在函数内部,有的channel只接受数据,有的channel只发送数据,这是可以使用单向的channel表示。

  双向的channel:因为关闭channel操作 一般是 发送方通知接收方,发送方不再向channel中发送新的数据了,所以通常只有在发送者所在的goroutine才会调用close函数,在接收channel数据的goroutine中关闭channel显然是不合理的,很有可能会引起错误,因为发送数据的goroutine可能不知道channel已经关闭了,所以他一旦向channle中发送数据,就会引发错误。

  单向channel:只有发送者的goroutine中可以调用close函数来关闭单项channel。如果一个goroutine中,对一个只接受的channel调用close函数,或引发编译错误。

  任何双向的channel向单向的channel赋值都将导致隐式转换,但是不能反向转换。

  声明单向channle  

chi := make(<-chan int) //只能被读数据的channel
chi := make(chan <- int) //只接收数据的channel

  

有缓冲的channel

chi := make(chan int)		//无缓冲
chii := make(chan int,10)	//有缓存channel,缓存空间可存10个值

  定义了一个有缓存的channel之后:

  在往channel中发数据的时候,只有当channel的缓冲区数量不为0时,数据才能写入,写数据的goroutine才不会阻塞。

  在从channel中读数据的时候,只有当channel不为空的时候,读channel的goroutine才不会阻塞。

package main

import (
	"fmt"
	"time"
)

func WriteData(chi chan int) {
	go func() {
		for x := 0; ; x++ {
			chi <- x
			time.Sleep(2 * time.Second)
		}
	}()
	go func() {
		for x := 0; ; x++ {
			chi <- x
			time.Sleep(time.Second)
		}
	}()
	go func() {
		for x := 0; ; x++ {
			chi <- x
			time.Sleep(2 * time.Second)
		}
	}()
}
func main() {
	chi := make(chan int, 3)
	WriteData(chi)
	for x := range chi {
		fmt.Println(x)
	}
}

  此例中,如果写数据时channel已满,那么写数据的goroutine会阻塞。如果读channel时,channel为空,那么读数据的goroutine会阻塞。

 

并发中的循环

package main

import "fmt"

func loop() {
	for x := 0; x < 10; x++ {
		go func() {
			fmt.Println(x)
		}()
	}
}
func main() {
	loop()
}

  上面这个代码执行的时候,可能有以下问题:

  1、没有输出   2、只输出一部分值   3、输出很多相同的值   4、输出的值全相同

  对于1、2、3的问题,是因为主协程执行速度太快,各个goroutine还没来得及执行,主协程就结束了,于是整个进程就结束了,也就看不到输出了。

  解决方法:在主协程中加一个休眠,给各个协程时间去执行。

package main

import (
	"fmt"
	"time"
)

func loop() {
	for x := 0; x < 10; x++ {
		go func() {
			fmt.Println(x)
		}()
	}
}
func main() {
	loop()
	time.Sleep(time.Second)
}

  对于第四个问题,是因为loop中的循环太快,而各个goroutine太慢,于是所有goroutine在执行前,循环已经完毕,此时i为10,于是全部都打印10了。

  解决方法:将i作为参数(闭包)

package main

import (
	"fmt"
	"time"
)

func loop() {
	for x := 0; x < 10; x++ {
		go func(i int) {
			//i为形参
			fmt.Println(i)
		}(x) //x为实参
	}
}
func main() {
	loop()
	time.Sleep(time.Second)
}

  

 select接收channel

package main

import (
	"fmt"
)

func loop(ch chan int, chi chan int) {
	go func() {
		for {
			ch <- 9
		}
	}()
	go func() {
		for {
			chi <- 8
		}
	}()
}
func main() {
	ch, chi := make(chan int), make(chan int)
	loop(ch, chi)
	select {
	case <-ch:
		fmt.Println("接收到ch中的数据")
	case <-chi:
		fmt.Println("接收到chi中的数据")
	}
}

  select类似于if  else,每一个case表示接收到channel的数据,执行对应的操作。只不过上面这个例子中,只会接收一次,要么接收的是ch,要么是chi,之后就不会再从channel中读数据了。

  如果要一直等待接收多个channel,即,要接收多个channel中的数据,可以使用for + select即可:

package main
import (
	"fmt"
	"time"
)
func loop(ch chan int, chi chan int) {
	go func() {
		for {
			ch <- 9
			time.Sleep(2 * time.Second)
		}
	}()
	go func() {
		for {
			chi <- 8
			time.Sleep(3 * time.Second)
		}
	}()
}
func main() {
	ch, chi := make(chan int), make(chan int)
	loop(ch, chi)
	for {
		select {
		case <-ch:
			fmt.Println("接收到ch中的数据")
		case <-chi:
			fmt.Println("接收到chi中的数据")
		}
	}
}

  

并发的退出 

package main

import (
	"fmt"
	"time"
)

func main() {
	exit := make(chan bool)
	for x := 1; x < 5; x++ {
		go func(i int) {
			for {
				select {
				case <-exit:
					fmt.Println("第", i, "个goroutine结束运行")
					return
				default:
					fmt.Println("第", i, "个goroutine正在运行")
				}
				time.Sleep(time.Second)
			}
		}(x)
	}
	time.Sleep(time.Second * 5)  //各个goroutine运行
	close(exit)  //广播结束消息
	time.Sleep(time.Second * 1)  //等待各个goroutine打印结束信息
}

  

 

 

 

 


有疑问加站长微信联系(非本文作者)

本文来自:博客园

感谢作者:-beyond

查看原文:Golang goroutine与channel

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

682 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传