写在前面
其实不论是哪一种编程语言,在当下这种大数据的业务压力下,多线程都是重中之重。本文的一些关于go语言的多线程实践都是基于MIT6.824课程总结而来的。
多线程通信
使用channel通道
关于利用channel实现多线程通信的方法,在另外一篇博客中已经提及了,需要注意的主要是其可能造成的死锁问题,本文主要介绍下面这种多线程通信的方式。
使用全局变量+锁
其实这种方法与channel最大的不同就是,前者是go语言所独有的特性,因为其天然的可以集成多线程并发。这种方法在很多编程语言中都有,譬如Java中的使用volatile这种所有线程均可见的全局变量,再配合lock锁实现多线程协同。
在GO语言中,并未有专用的用于线程协同的全局变量,所有全局变量均可以担任这项职责。下面给出一个简单的实践场景需求。
实践场景
6.824课程中的lab2需要实现Raft共识算法。Raft公式算法有两个非常重要的过程:leader election以及log replicaed。在leader election流程中,一个节点可以有三个状态:follower、candidate、以及leader。当一个节点获得大多数投票之后,他需要告诉所有其他candidate节点我已当选,你们需要改变为follower节点。这就存在了一个多线程的实践场景:两个线程正常运行,在其中一个线程满足某种条件后,影响另外一个线程的退出或者执行状态。具体代码见下。
实践代码
package main
import (
"sync"
"time"
"fmt"
)
var done bool
var mu sync.Mutex
func main() {
time.Sleep(1 * time.Second)
go dosome()
time.Sleep(10 * time.Second)
mu.Lock()
done = true
mu.Unlock()
fmt.Println("cancelled")
time.Sleep(2 * time.Second)
}
func dosome() {
for {
if done {
return
}
fmt.Println("tick")
time.Sleep(1 * time.Second)
}
}
可以看到,在main这个主线程中新开一个线程调用了dosome方法,dosome类似于心跳机制,每隔1s输出一个“tick”字符,前提是done变量为flase。而主线程在等待10s之后,将done变量设置为true,注意这里需要用lock锁住,保证线程安全(其实不需要,但是在更复杂的多线程环境中这是必不可少的)。当运行dosome方法的线程看到done变量为true之后,退出线程。主线程打印cancelled之后,等待2s,判断tick是否真的结束,然后退出。
具体运行结果见下:
tenris-MBP:src Sirius$ go run demo.go
tick
tick
tick
tick
tick
tick
tick
tick
tick
tick
cancelled
可以看到正常输出10个tick以及一个cancelled。当然这是很简单的实践方式,后序会面临更加复杂的多线程并发场景。