Go 是并发式语言,而不是并行式语言。在讨论 Go 如何处理并发之前,我们必须理解何为并发,以及并发与并行的区别。
并发是指立即处理多个任务的能力。一个CPU的情况下<意指看上去像是同时运行,其中有io的阻塞态等待的时间慢而已。
我们可以想象一个人正在跑步。假如在他晨跑时,鞋带突然松了。于是他停下来,系一下鞋带,接下来继续跑。这个例子就是典型的并发。这个人能够一下搞定跑步和系鞋带两件事,即立即处理多个任务。
解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三:在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群
并行是指同时处理多个任务。这听起来和并发差不多,但其实完全不同。
我们同样用这个跑步的例子来帮助理解。假如这个人在慢跑时,还在用他的 iPod 听着音乐。在这里,他是在跑步的同时听音乐,也就是同时处理多个任务。这称之为并行。
Go 编程语言原生支持并发。Go 使用 Go 协程(Goroutine) 和信道(Channel)来处理并发。在接下来的教程里,我们还会详细介绍它们。
Go 协程是与其他函数或方法一起并发运行的函数或方法。Go 协程可以看作是轻量级线程。与线程相比,创建一个 Go 协程的成本很小。因此在 Go 应用中,常常会看到有数以千计的 Go 协程并发地运行。
调用函数或者方法时,在前面加上关键字 go
,可以让一个新的 Go 协程并发地运行。
例子:
package main import ( "fmt" "time" ) // go协程 func index() { fmt.Println("hello world") } func main() { go index() go index() go index() go index() fmt.Println("") time.Sleep(time.Second*3) }
协程的书写直接在函数前加关键字“go”即可开启协程,运行是从上至下,遇到io阻塞态时等待自行的切换处理。各个协程之间做数据的通信,开多个协程就是这么简单的实现。
time模块的时间传参,点击second查看源码:
信道可以想像成 Go 协程之间通信的管道。如同管道中的水会从一端流到另一端,通过使用信道,数据也可以从一端发送,在另一端接收。
所有信道都关联了一个类型。信道只能运输这种类型的数据,而运输其他类型的数据都是非法的。
chan T
表示 T
类型的信道。信道是一个值类型,可以作为参数传递。
信道的零值为 nil
。信道的零值没有什么用,应该像对 map 和切片所做的那样,用 make
来定义信道。
下面编写代码,声明一个信道。
package main import ( "fmt" "time" ) //信道、管道 通道 channel // func main() { // //信道也是个变量 // //信道运输的类型是int类型 // //空值 是nil 引用类型 // var a chan int // fmt.Println(a) // //初始化 var a chan int =make(chan int) fmt.Println(a) // //重点 :放值和取值 // //箭头向信道变量,就是往信道中放值 a <-1 // //箭头向外,表示从信道中取值 <-a // //信道默认情况下,取值和赋值都是阻塞的 ******重要**** // }
简短声明通常也是一种定义信道的简洁有效的方法。
a := make(chan int)
这一行代码同样定义了一个 int 类型的信道 a,类型可以是其他数据结构bool string int...
如下所示,该语法通过信道发送和接收数据。
data := <- a // 读取信道 a a <- data // 写入信道 a
信道旁的箭头方向指定了是发送数据还是接收数据。
在第一行,箭头对于 a
来说是向外指的,因此我们读取了信道 a
的值,并把该值存储到变量 data
。
在第二行,箭头指向了 a
,因此我们在把数据写入信道 a
。
package main import ( "fmt" "time" ) // 信道,管道,通道 channel func main() { // 定义信道 // 信道也是个变量 // 控制nil 引用的类型时int //var a chan int //fmt.Println(a) // 初始化 var a chan int =make(chan int ) fmt.Println(a) // 重点:放值和取值 // 箭头向通道变量 就是往信道中值 a<-1 // 阻塞态 不在往下运行 等待取值 // 箭头向外,表示信道中取值 b:=<-a fmt.Println(b) time.Sleep(time.Second*1) //信道默认情况下,取值和赋值都是阻塞的 ******重要**** }
信道默认情况下,取值和赋值都是阻塞的,存和取都需要通过信道之间的通信进行传递参数,不然会造成死锁现象。
发送与接收默认是阻塞的。这是什么意思?当把数据发送到信道时,程序控制会在发送数据的语句处发生阻塞,直到有其它 Go 协程从信道读取到数据,才会解除阻塞。与此类似,当读取信道的数据时,如果没有其它的协程把数据写入到这个信道,那么读取过程就会一直阻塞着。
信道的这种特性能够帮助 Go 协程之间进行高效的通信,不需要用到其他编程语言常见的显式锁或条件变量。
接下来写点代码,看看协程之间通过信道是怎么通信的吧。
package main import ( "fmt" "time" ) // go协程 信道之间的通信传递 func index1(a chan bool) { fmt.Println("hello go") // 一旦执行完毕,往信道中存放一个值 time.Sleep(time.Second*3) a<-true fmt.Println("Just do IT") } func main() { // 必须初始化 var a chan bool = make(chan bool) go index1(a) // 从信道中取值 <-a //b:=<-a //fmt.Println(b) time.Sleep(time.Second*3) }
加入time.sleep模拟IO阻塞态,来更好的理解go协程中使用信道通信的传递。
我们再编写一个程序来更好地理解信道。该程序会计算一个数中每一位的平方和与立方和,然后把平方和与立方和相加并打印出来。
例如,如果输出是 123,该程序会如下计算输出:
squares = (1 * 1) + (2 * 2) + (3 * 3) cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3) output = squares + cubes = 50
我们会这样去构建程序:在一个单独的 Go 协程计算平方和,而在另一个协程计算立方和,最后在 Go 主协程把平方和与立方和相加。
package main import "fmt" // 例子 //number =456 func calcSquares(number int, squareop chan int) { sum := 0 for number != 0 { digit := number % 10 sum += digit * digit number /= 10 } squareop <- sum } func calcCubes(number int, a chan int) { sum := 0 for number != 0 { digit := number % 10 sum += digit * digit*digit number /= 10 } a <- sum } func main() { //运输平方和 var a chan int=make(chan int) //运输立方和 var b chan int=make(chan int) number:=123
//开启两个go协程 go calcSquares(number,a) go calcCubes(number,b) sum1:=<-a sum2:=<-b // 开两个协程分别进行,最后计算总和即可,速度很快 fmt.Println(sum1+sum2) }
协程分任务执行,速度很快。
使用信道需要考虑的一个重要的是死锁,当GO协程给一个信道发送数据时,照理说会有其他G协程来接收数据,
如果没有的话,程序会在运行时触发panic,形成死锁现象。
同理,当有 Go 协程等着从一个信道接收数据时,我们期望其他的 Go 协程会向该信道写入数据,要不然程序就会触发 panic。
package main func main() { ch := make(chan int) ch <- 5 }
没有接收信道的值会报错:
我们目前讨论的信道都是双向信道,即通过信道既能发送数据,又能接收数据。其实也可以创建单向信道,这种信道只能发送或者接收数据。
package main import "fmt" //单向信道 只能放或者只能取 func sendData(sendch chan<- int) { sendch <- 10 } func main() { // 只写信道 sendch := make(chan int) go sendData(sendch) fmt.Println(<-sendch) }
...>>> 10
数据发送方可以关闭信道,通知接收方这个信道不再有数据发送过来
当从信道接收数据时,接收方可以多用一个变量来检查信道是否已经关闭。
v, ok := <- ch
上面的语句里,如果成功接收信道所发送的数据,那么 ok
等于 true。而如果 ok
等于 false,说明我们试图读取一个关闭的通道。从关闭的信道读取到的值会是该信道类型的零值。例如,当信道是一个 int
类型的信道时,那么从关闭的信道读取的值将会是 0
。
package main import "fmt" //关闭信道和使用 for range 遍历信道 func producer(chnl chan int) { for i := 0; i < 10; i++ { chnl <- i } close(chnl) } func main() { ch := make(chan int) go producer(ch) for { v, ok := <-ch // 如果信道关闭,ok就是false //如果没关闭,就是true if ok == false { break } fmt.Println("Received ", v, ok) } }
主函数用的是for循环,会打印出0-9,然后关闭通道,使用变量 ok,
检查信道是否已经关闭。如果 ok
等于 false,说明信道已经关闭,于是退出 for 循环。如果 ok
等于 true,会打印出接收到的值和 ok
的值。
用range来循环读取数据,range会自动判断,取完值后直接关闭,比for更好一些
package main import ( "fmt" ) func producer(chnl chan int) { for i := 0; i < 10; i++ { chnl <- i } close(chnl) } func main() { ch := make(chan int) go producer(ch) for v := range ch { fmt.Println("Received ",v) } }
for range 循环从信道 ch
接收数据,直到该信道关闭。一旦关闭了 ch
,循环会自动结束。该程序会输出:
Received 0 Received 1 Received 2 Received 3 Received 4 Received 5 Received 6 Received 7 Received 8 Received 9
package main import "fmt" //优化例子 //number=456 func digits(number int, dchnl chan int) { for number != 0 { digit := number % 10 dchnl <- digit number /= 10 } close(dchnl) } func calcSquares1(number int, squareop chan int) { sum := 0 dch := make(chan int) go digits(number, dch) for digit := range dch { sum += digit * digit } squareop <- sum } func calcCubes1(number int, cubeop chan int) { sum := 0 dch := make(chan int) go digits(number, dch) for digit := range dch { sum += digit * digit * digit } cubeop <- sum } func main() { number := 123 sqrch := make(chan int) cubech := make(chan int) go calcSquares1(number, sqrch) go calcCubes1(number, cubech) squares:= <-sqrch cubes :=<-cubech fmt.Println("Final output", squares+cubes) }
Go-并发和并行-协程-信道-缓冲信道-select-mutex-读写文件
原文:https://www.cnblogs.com/Gaimo/p/12037018.html