如何理解go语言的并发编程(一)

4 篇文章 0 订阅
4 篇文章 0 订阅

go语言使用goroutine来实现并发编程,goroutine类似于协程,是非抢占式的,即一个goroutine交出控制权后,另一个goroutine才会执行。相比于抢占式的线程来说更加轻量。

比较

  • 普通函数中调用另一个函数是单向的,如main函数调用FuncA函数,要等到FuncA函数执行完,才能继续执行main函数中的部分。
  • goroutine和goroutine之间是有一个双向的通道,不仅可以交换数据,还可以交换控制权。比如在main函数中开了一个goroutine,执行到goroutine的时候,main函数还会继续往下进行,goroutine也在执行(并不是严格意义上的同时,中间会切换控制权,看似是在同时进行),有可能goroutine正在执行着,main函数就已经执行完毕,程序退出了。

语法

创建goroutine的语法就是在调用普通函数前加一个go

//调用已经定义好的函数
func FuncA(){
	for i:=0;i<5;i++{
		fmt.Println("这是A函数")
	}
}

func main(){
	go FuncA()
	fmt.Println("这是main函数")
}

//创建一个匿名函数,开一个goroutine
func main(){
	go func(){
		for i:=0;i<5;i++{
			fmt.Println("这是A函数")
		}
	}
	fmt.Println("这是main函数")
}
//这种情况下有可能直接输出"这是main函数",然后程序退出,也有可能"这是main函数"在几句"这是A函数"之间

之前说goroutine是非抢占式的,但FuncA还没执行完,就执行了fmt.Println(“这是main函数”)语句,这是因为存在调度器,会自动切换控制权,这里调用了fmt.Println就起到了切换控制权的作用。
还可以通过runtime.Sched()来主动交出控制权.

其他的一些调度器(并不是全部,还有很多地方可能会造成控制权的切换)

  • channel
  • select
  • I/O
  • 某些函数的调用

channel

channel是goroutine之间的通道。go语言的思想是通过通信来共享数据,而不是通过共享数据来通信。其他语言共享数据,需要加锁,解锁,保证数据的一致。而go语言的通信是通过channel收发数据来实现,保证了数据的一致性。有发送必须有接收,有接收也必须有发送。如果发送了没有接收,或者接收时没有发送,就会锁住,不会向下进行。

func main(){
	c := make(chan int)//也可以使用var c chan int来声明,但声明的是个空的chan,不能使用
	c <- 1//将1发送给c
	c <- 2//这里就会报错了,因为将1发送给c后没有接收
}

这时可以开一个goroutine来一直接收c的数据,这样只要给c发送数据,就会有人接收,就不会锁住了

func main(){
	c := make(chan int)
	go func(){
		for {
			<-c
		}
	}()
	c <- 1
	c <- 2
	c <- 3
}
//这样就不会锁住了,因为每向c发送一个数据,就会被goroutine接收,就能继续向c发送数据了

特殊用法

可以规定一个channel只能发送数据或只能接收数据

//创建了一个只能发送数据的channel
a := make(chan<- int)
//创建了一个只能接收数据的channel
b := make(<-chan int)
channel是go语言的一等公民,所以既能作为参数,也能作为返回值
//该函数需要一个只能发送数据的channel
func FuncA(c chan<- int){
	reutrn
}
//该函数返回一个只能接收数据的channel
func FuncA() <-chan int{
	out := make(chan int)
	go func(){
		for {
			time.Sleep(time.Second)
			out <- 1
		}
	}()
	return out
}

## 生成器
实际上不会直接创建一个chan,而是创建一个chan的生成器,生成器中一直向chan发送数据,调用这个生成器之后获得一个chan,就可以一直从chan中获取数据了。这样的一个生成器就类似于一个服务和任务,调用它就可以一直获取返回信息
```go
func createChan() chan int{
	out := make(chan int)
	go func(){
		for {
			//每隔五秒向out发送1
			time.Sleep(5 * time.Second)
			out <- 1
		}
	}()
	return out
}
//这样就创建了一个生成器,调用它返回一个chan,可以从这个chan中获取数据

func main(){
	c := createChan()
	for {
		fmt.Println(<-c)
	}
}

深入理解

为了更好的理解goroutine和channel的关系,可以试一下开多个goroutine

首先创建一个要开goroutine的函数

func doWork(c chan int){
	//这里还要传一遍参,是因为goroutine是一直在运行的,如果使用外部的自由变量,会由于自由变量值的变化造成一些错误
	//比如如果在goroutine外有一个遍历数组,当i等于数组长度时跳出循环,但由于goroutine不会因为跳出循环就终止了,导致使用了i,就造成了数组越界
	//又或者遍历了channel数组,对每个channel进行收发数据,但因为前面遍历与goroutine是并行,遍历走完了,goroutine才开始执行,这时goroutine使用的就全是数组的最后一个channel
	go func(c chan int){
		for{
			fmt.Println(<-c)
		}
	}(c)
}

然后在main函数中创建一个chan数组,每个chan都调用一次上面这个函数,这样对于每个channel来说都有一个对应的goroutine来接收数据了

func main(){
	var chans [10]chan int
	for i:=0;i<10;i++{
		chans[i] = make(chan int)
	}
	for i:=0;i<len(chans);i++{
		//其实在这里也可以直接go一个匿名函数,但是为了体现goroutine参数的问题,直接创建个函数
		doWork(chans[i])
	}
	
	//分别向每个channel发送两个数据
	for i:=0;i<len(chans);i++{
		chans[i] <- 'a' + i
	}
	
	for i:=0;i<len(chans);i++{
		chans[i] <- 'A' + i
	}
}

这样10个goroutine同步进行,都会接收2个数据并输出,输出是乱序的,但可以发现输出中,同一个字母的小写是在大写上面的,尽管中间可能隔着一些其他字母。这是因为对于某一个goroutine来说,它是先接收的小写字母,然后又接收了大写字母.

等待任务结束

上面会有一个问题,对每个goroutine发送完2个数据后就没有再发送了,但goroutine却一直还再接收数据,如果这时goroutine还没有接受完数据,主程序就执行完退出了,就会导致最后几个数据没有打印。所以我们需要知道goroutine是否接收完数据,如果接收完了,再退出。可以再goroutine接收完数据后发送数据到一个channel中,然后主程序从这个channel中接收数据,接收到了就代表goroutine接收完数据了,就可以退出了。

func doWork(w worker){	
	for{
		fmt.Println(<-w.In)
		w.Done <- true
	}	
}

type worker struct{
	In chan int
	Done chan bool
}

func main(){
	var workers [10]worker
	for i:=0;i<10;i++{
		workers[i].In = make(chan int)
		workers[i].Done = make(chan int)
	}
	
	for i:=0;i<len(workers);i++{
		//这里在调用的时候开goroutine
		go doWork(workers[i])
	}
	
	//分别向每个channel发送两个数据
	for i:=0;i<len(workers);i++{
		workers[i].In <- 'a' + i
		<-workers[i].Done
	}
	
	for i:=0;i<len(chans);i++{
		workers[i].In <- 'A' + i
		<-workers[i].Done
	}
}

像上面这种方式,会在接收到foroutine返回信息后才继续循环,才能向下一个goroutine发送数据,所以最后输出的数据就是发送的数据的顺序,这样开goroutine就没有意义了,因为正常情况下就可以顺序打印。可以发完所有的数据之后,再接收返回信息.

func main(){
	var workers [10]worker
	for i:=0;i<10;i++{
		workers[i].In = make(chan int)
		workers[i].Done = make(chan int)
	}
	
	for i:=0;i<len(workers);i++{
		//这里在调用的时候开goroutine
		go doWork(workers[i])
	}
	
	//分别向每个channel发送两个数据
	for i:=0;i<len(workers);i++{
		workers[i].In <- 'a' + i
	}
	
	//对每一个goroutine都要接收返回信息
	for i:=0;i<len(workers);i++{
		<-workers[i].Done
	}
	
	for i:=0;i<len(chans);i++{
		workers[i].In <- 'A' + i
	}
	
	for i:=0;i<len(workers);i++{
		<-workers[i].Done
	}
}

这样做还有一些缺点,会先打印乱序的小写字母,再打印乱序的大写字母,因为我们是再接受完返回信息后,又向每一个goroutine发送数据,为了解决这个问题,可以在最后的时候再接收返回数据,但直接这样做会有问题,因为在每个goroutine中,接收完数据后,就发送了返回数据,如果没有人接收返回数据而且继续发送数据,这时goroutine就没办法接收数据了,就会造成死锁。可以在doWork函数中再开一个gorotine发送返回数据,这样即时没有人接收,还可以继续接收传过来的数据。在main函数中最后接收返回数据,保证了goroutine执行完再退出.

func doWork(w worker){	
	for{
		fmt.Println(<-w.In)
		go func(w worker){
			w.Done <- true
		}(w)
	}	
}

func main(){
	var workers [10]worker
	for i:=0;i<10;i++{
		workers[i].In = make(chan int)
		workers[i].Done = make(chan int)
	}
	
	for i:=0;i<len(workers);i++{
		//这里在调用的时候开goroutine
		go doWork(workers[i])
	}
	
	//分别向每个channel发送两个数据
	for i:=0;i<len(workers);i++{
		workers[i].In <- 'a' + i
	}

	for i:=0;i<len(chans);i++{
		workers[i].In <- 'A' + i
	}
	
	for i:=0;i<len(workers);i++{
		//每个goroutine都发送了两次返回数据
		<-workers[i].Done
		<-workers[i].Done
	}
}

实际上这个问题也可以通过sync.WaitGroup来解决

//将上面的Done换为*sync.WaitGroup
func doWork(w worker){	
	for{
		fmt.Println(<-w.In)
		w.Wg.Done()
	}	
}

type worker struct{
	In chan int
	Wg *sync.WaitGroup
}

func main(){
	var workers [10]worker
	var wg sync.WaitGroup
	for i:=0;i<10;i++{
		workers[i].In = make(chan int)
		workers[i].Wg = &wg
	}
	
	wg.Add(20)
	
	for i:=0;i<len(workers);i++{
		//这里在调用的时候开goroutine
		go doWork(workers[i])
	}
	
	//分别向每个channel发送两个数据
	for i:=0;i<len(workers);i++{
		workers[i].In <- 'a' + i
		<-workers[i].Done
	}
	
	wg.Wait()
}

结语

channel是go语言中比较重要的一个部分,体现了go语言通过通信来共享数据而不是通过共享数据来通信的思想。
这篇问题只是简单介绍了channel的一些基本语法,以及简单的应用。但其实还有很多涉及到与其他调度器配合的内容没有在这里提到。
比如并发模式,这里只是提到了顺序的向goroutine发送数据,如果同时由两个goroutine发送数据,想要谁发送的快就接受哪个,而不是接收完一个必须等待接收完另一个才能接收第一个。
比如超时机制,如果超过一定时间没有接收到数据如何处理
这些都涉及到与select调度器的配合,在之后讲解select调度的文章中会结合起来,补充未讲的这部分。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值