基本概念
并发和并行
并发和并行都是指处理机处理多个任务的情况。对于多核心CPU的情况而言,并发指的是多个程序交替占用其中一个核心或多个核心去执行任务,并不一定非要占用多核心去执行任务。但是对于并行而言,程序一定是在CPU多个核心上同时运行。
所以,并行是并发的一个子概念。
协程goroutines
协程是比操作系统线程更加轻量的一个概念。写成可以运行在一个或多个线程之内。go 语言会自动管理用户创建的的协程,自动释放资源等。
go 中,通过go 关键字加上一个函数名*(可以是匿名函数)*去创建一个协程。
管道channel
管道是协程之间交换数据的一个类型化的消息队列。在管道上的写操作和读操作已提前实现了互斥,并不需要显示的去给管道加锁*(在访问共享数据时,也不应该去用加锁的方式。这会严重降低性能。)*
可以创建任意类型的管道,包括空接口类型。方法是ch:=make(chan dataType) 。
协程
创建协程
使用go 关键字可以去创建一个协程。但是需要注意的是,创建的协程会随着main函数的结束而结束,所以假如创建携程后main 函数就执行结束了,可能所有协程都得不到运行。
为了解决这个问题,在学习时可以让main 函数sleep 一段时间再结束运行。例如:
func main(){
for i:=0;i<5;i++{
go print(i)
}
time.Sleep(1*time.Second)
}
func print(n int){
for i:=0;i<20;i++{
fmt.Print("(",n,i,")")
}
fmt.Println()
}
可以看出,协程执行的顺序和创建的顺序并不一定相同。同时,一个携程也并不是从头到尾一次性执行完毕,他可能在函数体内部任意一条语句后就转去执行别的协程。因此,创建的五个协程内的5个fmt.Println() 并不是每20个输出后换行,在最极端的情况下,可能出现输出连续输出100个元素后换行5次的情况。
但在实际开发中,肯定不会使用sleep 的方法让协程完成运行。应该使用sync 包下的WaitGroup 结构体中的Add() Done() Wait() 三个函数去解决问题。
通道
创建
和切片、map 一样,通道也需要使用make 创建。所以通道有下面两种创建方法:
var ch1 chan int
ch1=make(chan int)
ch2:=make(chan int)
发送和接收数据
在向通道发送和接收数据时,都要用到<- 操作符,该操作符的箭头指向为数据的流向。
当发送数据时,使用msg:=<-ch 来将通道中的数据发送到msg 中。数据一旦从通道中取出,那么通道中就不会再存在这个数据了。向通道发送msg 数据的方法为 ch<-msg 。例子:
func main(){
ch1:=make(chan int)
go print(ch1)
go send(ch1)
time.Sleep(1*time.Second)
}
func send(ch chan int){
for i:=0;i<10;i++{
ch<-i
}
}
func print(ch chan int){
for i:=0;i<10;i++{
fmt.Print(<-ch," ")
}
}
但是需要注意的是,上面创建的通道都是缓冲为0的通道。这个意思是,如果有一个协程发送数据到通道时没有被其他协程立即接收,那么这个数据将会丢失。所以做法是make通道时增加他的缓冲。如:make(chan int,1) 即创建一个缓冲为1的通道。下面这个程序将会出现死锁:
func main(){
ch1:=make(chan int)
ch1<-999
go print(ch1)
time.Sleep(1*time.Second)
}
func print(ch chan int){
fmt.Println(<-ch)
}
如果将上述程序第二行的ch1:=make(chan int) 改为ch1:=make(chan int,1) 那么程序就会正常执行,输出999。
迭代和关闭通道
关闭通道
go 中,使用close(ch) 来关闭一个通道,释放资源。但即使是对一个已关闭的通道进行接收操作,也不会报错,这时就需要用到<-ch 的第二个参数了。msg,ok:=<-ch 才是一个通道正确的读取方法,如果通道已经被关闭,那么ok 将会是false ,否则为true 。例子:
func main(){
ch1:=make(chan int)
go print(ch1)
go send(ch1)
time.Sleep(1*time.Second)
}
func send(ch chan int){
for i:=0;i<5;i++{
ch<-i
}
close(ch)
}
func print(ch chan int){
for i:=0;i<20;i++{
v,ok:=<-ch
fmt.Print("(",v,ok,")")
}
}
可以看出只有前五个数据是正确的。
迭代读取通道
如果使用for-range 模式读取通道,则会自动检测通道是否被关闭。例如将上述例子的print 函数替换为下面,即可自动检测通道是否被关闭了:
func print(ch chan int){
for i:=range ch{
fmt.Print(i," ")
}
}
select
select 的结构类似于switch-case 结构,只不过他的case 是通道。select 语句选择一个已经准备好了的通道去执行,或是一直处于阻塞状态。例子:
func main(){
ch1:=make(chan string)
ch2:=make(chan string)
go choose(ch1,ch2)
rand.Seed(time.Now().UnixNano())
for i:=0;i<5;i++{
c:=rand.Intn(2)
if c==0{
go func1(ch1)
}else{
go func2(ch2)
}
}
time.Sleep(1*time.Second)
}
func func1(ch chan string){
ch<-"11111111"
}
func func2(ch chan string){
ch<-"22222222"
}
func choose(ch1,ch2 chan string){
for{
select {
case v:=<-ch1:
fmt.Println(v)
case v:=<-ch2:
fmt.Println(v)
}
}
}
上述程序一定会输出五条记录,因为不管是哪个通道准备好了数据,都会进行输出操作。如果choose 函数写成下面这种,最极端的情况下一条数据都不会输出:
func choose(ch1,ch2 chan string){
for{
fmt.Println(<-ch1)
fmt.Println(<-ch2)
}
}
222 11111111 22222299 */
上述程序一定会输出五条记录,因为不管是哪个通道准备好了数据,都会进行输出操作。如果`choose`函数写成下面这种,最极端的情况下一条数据都不会输出:
~~~go
func choose(ch1,ch2 chan string){
for{
fmt.Println(<-ch1)
fmt.Println(<-ch2)
}
}
|