如果说Go有什么让人一见钟情的特性,那大概就是并行计算了吧。

做个题目

如果我们列出10以下所有能够被3或者5整除的自然数,那么我们得到的是3,5,6和9。这四个数的和是23。
那么请计算1000以下(不包括1000)的所有能够被3或者5整除的自然数的和。

这个题目的一个思路就是:

(1) 先计算1000以下所有能够被3整除的整数的和A,
(2) 然后计算1000以下所有能够被5整除的整数和B,
(3) 然后再计算1000以下所有能够被3和5整除的整数和C,
(4) 使用A+B-C就得到了最后的结果。

按照上面的方法,传统的方法当然就是一步一步计算,然后再到第(4)步汇总了。

但是一旦有了Go,我们就可以让前面三个步骤并行计算,然后再在第(4)步汇总。

并行计算涉及到一个新的数据类型chan和一个新的关键字go

先看例子:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func get_sum_of_divisible(num int, divider int, resultChan chan int) {
  7. sum :=
  8. for value := ; value < num; value++ {
  9. if value%divider == {
  10. sum += value
  11. }
  12. }
  13. resultChan <- sum
  14. }
  15. func main() {
  16. LIMIT :=
  17. resultChan := make(chan int, )
  18. t_start := time.Now()
  19. go get_sum_of_divisible(LIMIT, , resultChan)
  20. go get_sum_of_divisible(LIMIT, , resultChan)
  21. //这里其实那个是被3整除,哪个是被5整除看具体调度方法,不过由于是求和,所以没关系
  22. sum3, sum5 := <-resultChan, <-resultChan
  23. //单独算被15整除的
  24. go get_sum_of_divisible(LIMIT, , resultChan)
  25. sum15 := <-resultChan
  26. sum := sum3 + sum5 - sum15
  27. t_end := time.Now()
  28. fmt.Println(sum)
  29. fmt.Println(t_end.Sub(t_start))
  30. }

(1) 在上面的例子中,我们首先定义了一个普通的函数get_sum_of_divisible,这个函数的最后一个参数是一个整型chan类型,这种类型,你可以把它当作一个先进先出的队列。你可以向它写入数据,也可以从它读出数据。它所能接受的数据类型就是由chan关键字后面的类型所决定的。在上面的例子中,我们使用<-运算符将函数计算的结果写入channelchannel是go提供的用来协程之间通信的方式。本例中main是一个协程,三个get_sum_of_divisible调用是协程。要在这四个协程间通信,必须有一种可靠的手段。

(2) 在main函数中,我们使用go关键字来开启并行计算。并行计算是由goroutine来支持的,goroutine又叫做协程,你可以把它看作为比线程更轻量级的运算。开启一个协程很简单,就是go关键字后面跟上所要运行的函数

(3) 最后,我们要从channel中取出并行计算的结果。使用<-运算符从channel里面取出数据。

在本例中,我们为了演示go并行计算的速度,还引进了time包来计算程序执行时间。在同普通的顺序计算相比,并行计算的速度是非同凡响的。

好了,上面的例子看完,我们来详细讲解Go的并行计算。

Go Routine 协程

所谓协程,就是Go提供的轻量级的独立运算过程,比线程还轻。创建一个协程很简单,就是go关键字加上所要运行的函数。看个例子:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func list_elem(n int) {
  6. for i := ; i < n; i++ {
  7. fmt.Println(i)
  8. }
  9. }
  10. func main() {
  11. go list_elem()
  12. }

上面的例子是创建一个协程遍历一下元素。但是当你运行的时候,你会发现什么都没有输出为什么呢?
因为上面的main函数创建完协程后立刻退出了,所以协程没有来得及运行呢!修改一下:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func list_elem(n int) {
  6. for i := ; i < n; i++ {
  7. fmt.Println(i)
  8. }
  9. }
  10. func main() {
  11. go list_elem()
  12. var input string
  13. fmt.Scanln(&input)
  14. }

这里,我们在main函数创建协程后,要求用户输入任何数据后才退出,这样协程就有了运行的时间,故而输出结果:

  1.  

其实在开头的例子里面,我们的main函数事实上也被阻塞了,因为sum3, sum5, sum15 := <-resultChan, <-resultChan, <-resultChan这行代码在channel里面没有数据或者数据个数不符的时候,都会阻塞在那里,直到协程结束,写入结果。

不过既然是并行计算,我们还是得看看协程是否真的并行计算了。

  1. package main
  2. import (
  3. "fmt"
  4. "math/rand"
  5. "time"
  6. )
  7. func list_elem(n int, tag string) {
  8. for i := ; i < n; i++ {
  9. fmt.Println(tag, i)
  10. tick := time.Duration(rand.Intn())
  11. time.Sleep(time.Millisecond * tick)
  12. }
  13. }
  14. func main() {
  15. go list_elem(, "go_a")
  16. go list_elem(, "go_b")
  17. var input string
  18. fmt.Scanln(&input)
  19. }

输出结果

  1. go_a
  2. go_b
  3. go_a
  4. go_b
  5. go_a
  6. go_b
  7. go_b
  8. go_b
  9. go_a
  10. go_b
  11. go_b
  12. go_a
  13. go_a
  14. go_b
  15. go_a
  16. go_a
  17. go_b
  18. go_b
  19. go_a
  20. go_b
  21. go_b
  22. go_a
  23. go_b
  24. go_b
  25. go_b
  26. go_b
  27. go_b
  28. go_b
  29. go_b
  30. go_b

在上面的例子中,我们让两个协程在每输出一个数字的时候,随机Sleep了一会儿。如果是并行计算,那么输出是无序的。从上面的例子中,我们可以看出两个协程确实并行运行了。

Channel通道

Channel提供了协程之间通信方式以及运行同步机制

假设训练定点投篮和三分投篮,教练在计数。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func fixed_shooting(msg_chan chan string) {
  7. for {
  8. msg_chan <- "fixed shooting"
  9. fmt.Println("continue fixed shooting...")
  10. }
  11. }
  12. func count(msg_chan chan string) {
  13. for {
  14. msg := <-msg_chan
  15. fmt.Println(msg)
  16. time.Sleep(time.Second * )
  17. }
  18. }
  19. func main() {
  20. var c chan string
  21. c = make(chan string)
  22. go fixed_shooting(c)
  23. go count(c)
  24. var input string
  25. fmt.Scanln(&input)
  26. }

输出结果为:

我们看到在fixed_shooting函数里面我们将消息传递到channel,然后输出提示信息”continue fixed shooting…”,而在count函数里面,我们从channel里面取出消息输出,然后间隔1秒再去取消息输出。这里面我们可以考虑一下,如果我们不去从channel中取消息会出现什么情况?我们把main函数里面的go count(c)注释掉,然后再运行一下。发现程序再也不会输出消息和提示信息了。这是因为channel中根本就没有信息了,因为如果你要向channel里面写信息必须有配对的取信息的一端,否则是不会写的

我们再把三分投篮加上。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func fixed_shooting(msg_chan chan string) {
  7. for {
  8. msg_chan <- "fixed shooting"
  9. }
  10. }
  11. func three_point_shooting(msg_chan chan string) {
  12. for {
  13. msg_chan <- "three point shooting"
  14. }
  15. }
  16. func count(msg_chan chan string) {
  17. for {
  18. msg := <-msg_chan
  19. fmt.Println(msg)
  20. time.Sleep(time.Second * )
  21. }
  22. }
  23. func main() {
  24. var c chan string
  25. c = make(chan string)
  26. go fixed_shooting(c)
  27. go three_point_shooting(c)
  28. go count(c)
  29. var input string
  30. fmt.Scanln(&input)
  31. }

输出结果为:

我们看到程序交替输出定点投篮和三分投篮,这是因为写入channel的信息必须要读取出来,否则尝试再次写入就失败了。

在上面的例子中,我们发现定义一个channel信息变量的方式就是多加一个chan关键字。并且你能够向channel写入数据从channel读取数据。这里我们还可以设置channel通道的方向。

Channel通道方向*

所谓的通道方向就是。如果我们如下定义

  1. c chan<- string //那么你只能向channel写入数据

而这种定义

  1. c <-chan string //那么你只能从channel读取数据

试图向只读chan变量写入数据或者试图从只写chan变量读取数据都会导致编译错误。

如果是默认的定义方式

  1. c chan string //那么你既可以向channel写入数据也可以从channnel读取数据

多通道(Select)

如果上面的投篮训练现在有两个教练了,各自负责一个训练项目。而且还在不同的篮球场,这个时候很显然,我们一个channel就不够用了。修改一下:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func fixed_shooting(msg_chan chan string) {
  7. for {
  8. msg_chan <- "fixed shooting"
  9. time.Sleep(time.Second * )
  10. }
  11. }
  12. func three_point_shooting(msg_chan chan string) {
  13. for {
  14. msg_chan <- "three point shooting"
  15. time.Sleep(time.Second * )
  16. }
  17. }
  18. func main() {
  19. c_fixed := make(chan string)
  20. c_3_point := make(chan string)
  21. go fixed_shooting(c_fixed)
  22. go three_point_shooting(c_3_point)
  23. go func() {
  24. for {
  25. select {
  26. case msg1 := <-c_fixed:
  27. fmt.Println(msg1)
  28. case msg2 := <-c_3_point:
  29. fmt.Println(msg2)
  30. }
  31. }
  32. }()
  33. var input string
  34. fmt.Scanln(&input)
  35. }

其他的和上面的一样,唯一不同的是我们将定点投篮和三分投篮的消息写入了不同的channel,那么main函数如何知道从哪个channel读取消息呢?使用select方法,select方法依次检查每个channel是否有消息传递过来,如果有就取出来输出。如果同时有多个消息到达,那么select闭上眼睛随机选一个channel来从中读取消息,如果没有一个channel有消息到达,那么select语句就阻塞在这里一直等待。

在某些情况下,比如学生投篮中受伤了,那么就轮到医护人员上场了,教练在一般看看,如果是重伤,教练就不等了,就回去了休息了,待会儿再过来看看情况。我们可以给select加上一个case用来判断是否等待各个消息到达超时。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func fixed_shooting(msg_chan chan string) {
  7. var times =
  8. var t =
  9. for {
  10. if t <= times {
  11. msg_chan <- "fixed shooting"
  12. }
  13. t++
  14. time.Sleep(time.Second * )
  15. }
  16. }
  17. func three_point_shooting(msg_chan chan string) {
  18. var times =
  19. var t =
  20. for {
  21. if t <= times {
  22. msg_chan <- "three point shooting"
  23. }
  24. t++
  25. time.Sleep(time.Second * )
  26. }
  27. }
  28. func main() {
  29. c_fixed := make(chan string)
  30. c_3_point := make(chan string)
  31. go fixed_shooting(c_fixed)
  32. go three_point_shooting(c_3_point)
  33. go func() {
  34. for {
  35. select {
  36. case msg1 := <-c_fixed:
  37. fmt.Println(msg1)
  38. case msg2 := <-c_3_point:
  39. fmt.Println(msg2)
  40. case <-time.After(time.Second * ):
  41. fmt.Println("timeout, check again...")
  42. }
  43. }
  44. }()
  45. var input string
  46. fmt.Scanln(&input)
  47. }

在上面的例子中,我们让投篮的人在几次过后挂掉,然后教练就每次等5秒出来看看情况(累死丫的,:-P),因为我们对等待的时间不感兴趣就不用变量存储了,直接<-time.After(time.Second*5),或许你会奇怪,为什么各个channel消息都没有到达,select为什么不阻塞?就是因为这个time.After,虽然它没有显式地告诉你这是一个channel消息,但是记得么?main函数也是一个channel啊!至于time.After的功能实际上让main阻塞了5秒后返回给main的channel一个时间。所以我们在case里面把这个时间消息读出来,select就不阻塞了。

输出结果如下:

这里select还有一个default的选项,如果你指定了default选项,那么当select发现没有消息到达的时候也不会阻塞,直接开始转回去再次判断。

Channel Buffer通道缓冲区

我们定义chan变量的时候,还可以指定它的缓冲区大小。一般我们定义的channel都是同步的,也就是说接受端和发送端彼此等待对方ok才开始。但是如果你给一个channel指定了一个缓冲区,那么消息的发送和接受式异步的除非channel缓冲区已经满了

  1. c:=make(chan int, 1)

我们看个例子:

  1. package main
  2. import (
  3. "fmt"
  4. "strconv"
  5. "time"
  6. )
  7. func shooting(msg_chan chan string) {
  8. var group =
  9. for {
  10. for i := ; i <= ; i++ {
  11. msg_chan <- strconv.Itoa(group) + ":" + strconv.Itoa(i)
  12. }
  13. group++
  14. time.Sleep(time.Second * )
  15. }
  16. }
  17. func count(msg_chan chan string) {
  18. for {
  19. fmt.Println(<-msg_chan)
  20. }
  21. }
  22. func main() {
  23. var c = make(chan string, )
  24. go shooting(c)
  25. go count(c)
  26. var input string
  27. fmt.Scanln(&input)
  28. }

输出结果为:

  1. :
  2. :
  3. :
  4. :
  5. :
  6. :
  7. :
  8. :
  9. :
  10. :
  11. :
  12. :
  13. :
  14. :
  15. :
  16. :
  17. :
  18. :
  19. :
  20. :
  21. :
  22. :
  23. :
  24. :
  25. :
  26. :
  27. :
  28. :
  29. :
  30. :
  31. :
  32. :
  33. :
  34. :
  35. :
  36. :
  37. :
  38. :
  39. :
  40. :

你可以尝试运行一下,每次都是一下子输出10个数据。然后等待10秒再输出一批。

小结

并行计算这种特点最适合用来开发网站服务器,因为一般网站服务都是高并发的,逻辑十分复杂。而使用Go的这种特性恰是提供了一种极好的方法。

  1.  
  1.  
  1.  

换个语言学一下 Golang (10)——并行计算的更多相关文章

  1. 换个语言学一下 Golang (1)

    做技术的总是有些拗.这么多年一直在.net的框框里打转转.直到现在市场上.net工作越来越难找,项目越来越老才发现不做出改变不行了.就从学习Go开始吧. Go语言的特点 简洁.快速.安全 并行.有趣. ...

  2. 换个语言学一下 Golang (13)——Web表单处理

    介绍 表单是我们平常编写Web应用常用的工具,通过表单我们可以方便的让客户端和服务器进 行数据的交互.对于以前开发过Web的用户来说表单都非常熟悉.表单是一个包含表单元素的区域.表单元素是允许用户在表 ...

  3. 换个语言学一下 Golang (12)——Web基础

    一.web工作方式 我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容.在这个看似简单的用户行为背后,到底隐藏了些什么呢?对于普通的上网过程,系统其实是这样做的 ...

  4. 换个语言学一下 Golang (3)——数据类型

    在 Go 编程语言中,数据类型用于声明函数和变量. 数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存. Go 语言按类别有以下几种 ...

  5. 换个语言学一下 Golang (5)——运算符

    运算符用于在程序运行时执行数学或逻辑运算. Go 语言内置的运算符有: 算术运算符 关系运算符 逻辑运算符 位运算符 赋值运算符 其他运算符 接下来让我们来详细看看各个运算符的介绍. 算术运算符 下表 ...

  6. 换个语言学一下 Golang (11)——使用包和测试

    Go天生就是为了支持良好的项目管理体验而设计的. 包 在软件工程的实践中,我们会遇到很多功能重复的代码,比如去除字符串首尾的空格.高质量软件产品的特点就是它的部分代码是可以重用的,比如你不必每次写个函 ...

  7. 换个语言学一下 Golang (6)——数组,切片和字典

    在上面的章节里面,我们讲过Go内置的基本数据类型.现在我们来看一下Go内置的高级数据类型,数组,切片和字典. 数组(Array) 数组是一个具有相同数据类型的元素组成的固定长度的有序集合.比如下面的例 ...

  8. 换个语言学一下 Golang (6)——控制流程

    Go语言的控制结构关键字只有if..else if..else ,for 和 switch. 而且在Go中,为了避免格式化战争,对程序结构做了统一的强制的规定.看下下面的例子. 请比较一下A程序和B程 ...

  9. 换个语言学一下 Golang (4)——变量与常量

    一.变量定义 所谓的变量就是一个拥有指定名称和类型的数据存储位置. //看一个例子 package main import ( "fmt" ) func main() { var ...

随机推荐

  1. Spring Boot版本号说明

    Spring Boot的版本选择一般是这样的,如下图: 那版本号后面的英文代表什么含义呢? 具体含义,如下文所示: SNAPSHOT:快照版,表示开发版本,随时可能修改: M1(Mn):M是miles ...

  2. html-前端内容初识

    HTML解释: HTML是英文Hyper Text Mark-up Language(超文本标记语言)的缩写,他是一种制作万维网页面标准语言(标记).相当于定义统一的规则(W3C),大家都来遵守他,这 ...

  3. 什么是rpc及应用场景?

    你编过程序吧?你程序里有函数或方法间的调用吧? 比如你写了两个函数fa和fb,在fa里肯定可以调用fb,这个可以理解吧?好了,铺垫完了.接下来入正题: 别人写了个程序,程序里有个函数rf,而且这个程序 ...

  4. emacs第二天

    setq 和setq-default的区别 cursor-type是一个buffer local 变量 在每一份buffer中都有一份值 如果变量是buffer local 里面的变量 setq-de ...

  5. ESP8266 SDK开发: 测试下诱人的程序

    前言 这一节测试一下诱人的程序 实现的功能,APP通过SmartConfig给Wi-Fi模块配网并绑定设备,然后通过MQTT远程控制开发板的继电器, APP显示ESP8266采集的温湿度数据. 简而言 ...

  6. pytest--常用插件

    前戏 虽然pytest给我们提供了很多的功能,但是有些功能还是没有,而pytest的插件可以满足我们的需求,比如用例失败重跑,统计代码覆盖率等等功能. pytest-sugar pytest-suga ...

  7. 洛谷 P2580 【于是他错误的点名开始了】题解

    XS中学化学竞赛组教练是一个酷爱炉石的人. 他会一边搓炉石一边点名以至于有一天他连续点到了某个同学两次,然后正好被路过的校长发现了然后就是一顿欧拉欧拉欧拉(详情请见已结束比赛CON900). 题目背景 ...

  8. spark基础知识二

    主要围绕spark的底层核心抽象RDD进行理解.主要包括以下几个方面 RDD弹性分布式数据集的概念 RDD弹性分布式数据集的五大属性 RDD弹性分布式数据集的算子操作分类 RDD弹性分布式数据集的算子 ...

  9. 2018-2019-20175205实验四《Android程序设计》实验报告

    目录 2018-2019-20175205实验四<Android程序设计>实验报告 实验要求 教材学习 第二十五章 活动 第二十六章 UI组件 第二十七章 布局 实验步骤 任务一 任务二 ...

  10. shell(一) shell变量

    基本介绍 变量命名规范 变量名要求由字母.数字.下划线组成,尽量字母开头,有明确含义 注意:变量赋值时,等号前后不能有空格,变量名称不能和字体变量冲突 自定义变量 当前shell有效 1.定义变量 v ...