概述
最近在做的一个项目,需要对大量数据进行一些基本的统计和处理,整个程序的思路很简单,但处理起来却很慢,特别是有二重循环的地方,龟速前进,眼看着16核32线程 的服务器只有一个线程被利用,束手无策。之前一直听说Go是一门对并发编程有很好的支持的语言,七牛的许牛大力推崇Go语言,于是就开始了对Go并行编程的探索之旅。
一些准备工作
在正式开始Go并行编程之前,首先我们需要准备Go的编程开发环境:Go编译器、Go编辑器。
在golang的官网上有go的下载链接:
http://code.google.com/p/go/downloads/list
我选择的是go1.2.1.windows-amd64.msi安装包,下载完后直接点击该安装包,按照默认选项安装即可。安装完后,可以在命令行中查看go版本以检查是否安装成功:
1
2
C:\Users\Bill>go version
go version go1.2.1 windows/amd64
关于Go的一些特性的介绍这里也不讲了,有兴趣的可以移步酷壳的这两篇文章Go 语言简介(上)— 语法 和 Go 语言简介(下)— 特性。下面我们直接进入Go并发编程。
Go并发编程
#0x01.goroutine
优雅的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色。在Go中,通过一种叫做goroutine的go协程这种轻量级线程来支持
并发编程范式。协程是比进程和线程更轻量级的线程,go语言标准库提供的所有系统调用操作都会出让CPU给其他goroutine,协程的切换管理不依赖于系统的线程和
进程,也不依赖于CPU的核心数量。下面我们来看一个简单的goroutine的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
arr := [10]int{}
for i := 0; i < 10; i++ {
fmt.Print("Result of ", i, ":")
go func() {
arr[i] = i + i*i
fmt.Println(arr[i])
}()
}
fmt.Println("Done")
}
1
2
3
4
5
6
7
8
9
10
11
Result of 0:0
Result of 1:2
Result of 2:6
Result of 3:12
Result of 4:20
Result of 5:30
Result of 6:42
Result of 7:56
Result of 8:72
Result of 9:90
Done
1
Result of 0:Result of 1:Result of 2:Result of 3:Result of 4:Result of 5:Result of 6:Result of 7:Result of 8:Result of 9:Done
其实,这与golang的程序执行顺序有关。go程序从初始化main package并执行main()函数开始,当main()函数返回时,程序退出,且程序并不等待其他goroutine (非主goroutine)结束。于是上面的程序中,主函数虽然启动了10gegoroutine,但都没来得及执行,程序就已经退出了。那么怎么解决这个问题捏?很显然,我们 在退出程序之前,需要判断这些创建的goroutine执行完了没。我们可以用一个全局变量来计数执行了的协程数,如果计数变量小于10,我们就等待或sleep。
#0x02.并发通讯
等一等,多个协程读写同一个变量,我们是不是需要对这个变量枷锁呀?答案是肯定的,我们可以采用类似与C/C++的线程通讯、数据共享的思路来实现,如下:
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
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
var cnt int = 0 // 全局计数器
mylock := &sync.Mutex{} // 互斥锁
arr := [10]int{}
for i := 0; i < 10; i++ {
fmt.Print("Result of ", i, ":")
go func() {
arr[i] = i + i*i
fmt.Println(arr[i])
mylock.Lock() // 写之前枷锁
cnt++
mylock.Unlock() // 写之后释放锁
}()
}
for {
mylock.Lock() // 读之前枷锁
temp := cnt
mylock.Unlock() // 读之后释放锁
runtime.Gosched() // 协程切换
if temp >= 10 {
break
}
}
fmt.Println("Done")
}
1
2
Result of 0:Result of 1:Result of 2:Result of 3:Result of 4:Result of 5:Result of 6:Result of 7:Result of 8:Result of 9:panic: runtime error: index out of range
...
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
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
var cnt int = 0 // 全局计数器
mylock := &sync.Mutex{} // 互斥锁
arr := [11]int{}
for i := 0; i < 10; i++ {
go func() {
arr[i] = i + i*i
mylock.Lock() // 写之前枷锁
cnt++
mylock.Unlock() // 写之后释放锁
}()
}
for {
mylock.Lock() // 读之前枷锁
temp := cnt
mylock.Unlock() // 读之后释放锁
runtime.Gosched() // 协程切换
if temp >= 10 {
break
}
}
for i := 0; i < 11; i++ {
fmt.Println("Result of ", i, ":", arr[i])
}
fmt.Println("Done")
}
1
2
3
4
5
6
7
8
9
10
11
12
Result of 0 : 0
Result of 1 : 0
Result of 2 : 0
Result of 3 : 0
Result of 4 : 0
Result of 5 : 0
Result of 6 : 0
Result of 7 : 0
Result of 8 : 0
Result of 9 : 0
Result of 10 : 110
Done
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
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
var cnt int = 0 // 全局计数器
mylock := &sync.Mutex{} // 互斥锁
arr := [11]int{}
for i := 0; i < 10; i++ {
go func(i int) { // 这里的i是形参
arr[i] = i + i*i
mylock.Lock() // 写之前枷锁
cnt++
mylock.Unlock() // 写之后释放锁
}(i) // 这里的i是实参
}
for {
mylock.Lock() // 读之前枷锁
temp := cnt
mylock.Unlock() // 读之后释放锁
runtime.Gosched() // 协程切换
if temp >= 10 {
break
}
}
for i := 0; i < 11; i++ {
fmt.Println("Result of ", i, ":", arr[i])
}
fmt.Println("Done")
}
想象一下,在一个大的系统中具有无数的锁、无数的共享变量、无数的业务逻辑与错误处理分支,那将是一场噩梦。这噩梦就是众多C/C++开发者正在经历的,其实Java和C#开发 者也好不到哪里去。Go语言既然以并发编程作为语言的最核心优势,当然不至于将这样的问题用这么无奈的方式来解决。Go语言提供的是另一种通信模型,即以消息机制而非共享 内存作为通信方式。
#0x03.channel
Go语言提供的消息通信机制被称为channel,接下来我们将详细介绍channel。现在,让我们用Go语言社区的那句著名的口号来开始这一小节:
channel是Go语言在语言级别提供的goroutine间的通信方式,可以使用channel在两个或多个goroutine之间传递消息。channel是进程内的通信方式,因此通过channel传递 对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。如果需要跨进程通信,建议用分布式系统的方法来解决,比如使用Socket或者HTTP等通信协议。 channel是类型相关的。也就是说,一个channel只能传递一种类型的值,这个类型需要在声明channel时指定。我们先看下用channel的方式重写上面的例子是什么样子的不要通过共享内存来通信,而应该通过通信来共享内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
1 2 3 4 |
|
到这里,go的并行编程基本已经入门了,关于channel的更多详细的用法可以参见参考资料。
参考资料
[1] golang 官方主页:http://golang.org/ [2] go语言编程,许世伟