Yayako's Blog

「Shooting for the stars when I couldn't make a killing.」

仅作java2go的简单记录,有错误烦请提出

合集整理:Java转Go的学习-合集

demo已上传git:https://gitee.com/yayako/go-learning.git

go中的并发

goroutine

协程

用户级线程,对内核透明,系统并不知道有协程的存在,完全由自己的程序进行调度。

在一个golang程序的主线程上可以起多个协程,golang中多协程可以实现并发或者并行。

当主线程执行完毕后即使协程没有执行完毕,程序也会退出。

goroutine

golang的一大特色:从语言层面原生支持协程,goroutine就是其表现形式。

goroutine默认占用内存远比线程少,os线程一般都有固定的栈内存(2mb左右),而一个goroutine占用内存非常小(2kb左右)。另一方面,多协程goroutine切换调度的开销远比线程调度要小。

基本使用

使用goroutine的方式很简单,直接在需要另起协程执行的方法前加上 go 关键字即可,以及,为了防止在协程因主线程停止而被动停止,需要使用协程计数器来保证“通讯”。相关操作实现都在sync包中。

一个简单的使用goroutine的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 协程计数主要由sync中的WaitGroup结构体实现
var wg sync.WaitGroup

func main() {
wg.Add(1) // 协程计数器 +1
go goroutineTest() // 开启一个协程
wg.Wait() // 等待协程执行完毕
fmt.Println("main 退出")
}

func goroutineTest() {
defer wg.Done()
// 方法体
}

其中 wg.Done() 源码如下,实际上就是计数器-1。

1
2
3
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
goroutine的异常处理

假设在协程执行的方法中出现了异常,则需要使用 defer+recover来捕获处理

但必须要注意执行 wg.Done() ,和Java的finally一定要释放锁是同一个道理。

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
var wg sync.WaitGroup

func main() {
wg.Add(2)
go errFunc()
go healthyFunc()
wg.Wait()
}

func errFunc() {
defer func() {
if err := recover(); err != nil {
fmt.Println("【errFunc】执行失败,异常已捕获")
}
wg.Done()
}()
// 一段有问题的代码
var myMap map[int]int
myMap[0] = 0
}

func healthyFunc() {
defer wg.Done()
time.Sleep(time.Millisecond * 50)
fmt.Println("【healthyFunc】执行成功")
}

控制台输出结果

1
2
【errFunc】执行失败,异常已捕获
【healthyFunc】执行成功

channel

channel(管道)是语言级别上提供的goroutine间的通讯方式,可以在多个goroutine之间传递消息,是一种通信机制

go语言的并发模型是CSP(Conmunicating Sequential Process),提倡通过通信共享内存而不是通过共享内存实现通信。

channel在go中是一种特殊的类型,且是引用类型,遵循fifo的规则,保证收发数据的顺序,每一个管道都是一个具体类型的导管,声明时需要为其指定元素类型。

基本使用

通过 chan 关键字声明,通过 make 函数初始化

1
make(chan 元素类型, 容量)

通过 <- 读取/写入数据

1
2
3
4
5
6
7
8
9
10
channel := make(chan int, 2)
channel <- 1
channel <- 2
// channel <- 3 // 超出容量 -> 管道阻塞 all goroutines are asleep - deadlock!
// 值:0xc00010a000,类型:chan int,容量:3,长度:2
fmt.Printf("值:%v,类型:%T,容量:%v,长度:%v\n", channel, channel, cap(channel), len(channel))
a := <-channel
fmt.Println(a) // 1
fmt.Println(<-channel) // 2
// fmt.Println(<- channel) // 超取 -> 管道阻塞 all goroutines are asleep - deadlock!

通过 for 遍历读取

1
2
3
for i := 0; i < 2; i++ {
fmt.Println(<-channel)
}

但需要注意的是,如果通过 for range 遍历读取,则在遍历之前必须关闭通道,否则会出现死锁问题

1
2
3
4
close(channel) // 关闭管道
for v := range channel {
fmt.Println(v)
}
单向管道

channel默认情况下是双向管道,可读可写,只写只读的管道称为单向管道。

单向管道只需要在初始化时声明类型即可

1
2
3
4
// 只写管道 —— chan<-
wch := make(chan<- int, 2)
// 只读管道 —— <-chan
rch := make(<-chan int, 2)

下面结合goroutine和channel实现并行读写

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
var wg sync.WaitGroup

func main() {
ch := make(chan int, 10)
wg.Add(2)
go write(ch)
go read(ch)
wg.Wait()
}

func write(ch chan int /* 可使用单向管道(只写):ch chan<- int */) {
for i := 0; i < 10; i++ {
ch <- i
fmt.Printf("【写入】%v\n", i)
time.Sleep(time.Millisecond * 50)
}
// 写完记得关闭
close(ch)
wg.Done()
}

func read(ch chan int /* 可使用单向管道(只读):ch <-chan int */) {
for v := range ch {
fmt.Printf("【读取】%v\n", v)
time.Sleep(time.Millisecond * 50)
}
wg.Done()
}

控制台输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
【写入】0
【读取】0
【写入】1
【读取】1
【写入】2
【读取】2
【写入】3
【读取】3
【写入】4
【读取】4
【写入】5
【读取】5
【写入】6
【读取】6
【写入】7
【读取】7
【写入】8
【读取】8
【写入】9
【读取】9
select多路复用

当我们需要同时从多个channel接收数据时,可以选择开启多个协程来完成,但这种方式的运行性能会差很多,可以进一步使用 select-case 来实现多路复用。

每个case对应一个channel的通信过程,select会一直等待,直到某个case的通信操作完成时,就会执行case分支内的语句。

假设有两个channel,分别写入了int和float64,现在要通过select实现同时从两个channel中读取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
// 写入数据
intChan := make(chan int, 5)
floatChan := make(chan float64, 5)
for i := 0; i < 5; i++ {
intChan <- i
floatChan <- float64(i) + 0.1
}

// 使用select来获取channel中的数据时无需关闭channel
for {
select {
case v := <-intChan:
fmt.Printf("【intChan】%v\n", v)
time.Sleep(time.Millisecond * 50)
case v := <-floatChan:
fmt.Printf("【floatChan】%v\n", v)
time.Sleep(time.Millisecond * 50)
default:
fmt.Println("【over】")
break
}
}
}

控制台输出结果

1
2
3
4
5
6
7
8
9
10
11
【floatChan】0.1
【floatChan】1.1
【intChan】0
【intChan】1
【floatChan】2.1
【intChan】2
【intChan】3
【floatChan】3.1
【floatChan】4.1
【intChan】4
【over】

lock

同样的,sync包中还提供了多种锁组件,如互斥锁、读写锁等。

以下是一个简单的互斥锁的实践,通过互斥锁保证共享资源 count 在并发情况下的的递增。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var wg sync.WaitGroup
var mutex sync.Mutex

var count int

func main() {
for i := 0; i < 20; i++ {
wg.Add(1)
go mutexTest()
}
go wg.Wait()
}

func mutexTest() {
mutex.Lock() // 上锁
count++
fmt.Println("count =", count)
wg.Done()
mutex.Unlock() // 解锁
}

获取cpu数量

go的runtime包中提供了多种对环境变量的操作,例如获取cpu核数

1
2
cpuNum := runtime.NumCPU()   // 相当于java中的 Runtime.getRuntime().availableProcessors()
fmt.Println("cpu数量", cpuNum) // 12

评论