优雅的并发编程范式、完善的并发支持、优秀的并发性能是Go语言区别于其他语言的一大特点。在如今的多核时代,并发编程的意义不言而喻。使用Go开发并发程序非常容易操作。语言层面提供了关键字go来启动协程,同一台机器上可以启动上千个协程。让我们详细介绍一下。goroutineGo语言的并发执行体称为goroutine,关键字go用于启动一个goroutine。go关键字后面必须跟一个函数,可以是命名函数也可以是未命名函数,函数的返回值将被忽略。go的执行是非阻塞的。我们先来看一个例子:packagemainimport("fmt""time")funcmain(){gospinner(100*time.Millisecond)constn=45fibN:=fib(n)fmt.Printf("\rFibonacci(%d)=%d\n",n,fibN)//Fibonacci(45)=1134903170}funcspinner(delaytime.Duration){for{for_,r:=range`-\|/`{fmt.Printf("\r%c",r)time.Sleep(delay)}}}funcfib(xint)int{ifx<2{returnx}returnfib(x-1)+fib(x-2)}从执行结果来看,斐波那契计算成功了deedsequence的值表示程序没有阻塞在spinner处,spinner函数一直在屏幕上打印提示字符,表示程序正在执行中。当计算出斐波那契数列的值时,主函数打印结果并退出,微调器也退出。让我们看另一个例子,执行循环10次,并打印两个数字的和:0;i<10;i++{goAdd(i,i)}}有问题,屏幕上什么都没有,为什么?这取决于Go程序的执行机制。一个程序启动时,只有一个goroutine调用main函数,称为maingoroutine。新的goroutines使用go关键字创建并并发执行。当main函数返回时,并没有等待其他goroutine执行完毕,而是暴力结束所有goroutine。有办法解决吗?当然有,请继续阅读。Channel在编写多进程程序时一般会遇到一个问题:进程间通信。常见的通信方式有信号、共享内存等,goroutine之间的通信机制是channel通道。使用make创建通道:ch:=make(chanint)//ch的类型为chanint通道支持三种主要操作:发送、接收和关闭。ch<-x//发送x=<-ch//接收<-ch//接收,丢弃结果close(ch)//关闭无缓冲通道make函数接受两个参数,第二个参数为可选参数,表示信道容量。传递none或0以创建无缓冲通道。无缓冲通道上的发送操作将阻塞,直到另一个goroutine在相应的通道上执行接收操作。相反,如果先执行接收,则接收goroutine将阻塞,直到另一个goroutine在相应的通道上执行发送。因此,无缓冲通道是同步通道。让我们使用无缓冲通道来解决上面示例中的问题。packagemainimport"fmt"funcAdd(x,yint,chchanint){z:=x+ych<-z}funcmain(){ch:=make(chanint)fori:=0;i<10;i++{goAdd(i,i,ch)}fori:=0;i<10;i++{fmt.Println(<-ch)}}可以正常输出结果。主goroutine阻塞,直到它从通道读取值,程序继续执行,最后退出。缓冲通道创建一个容量为5的缓冲通道:ch:=make(chanint,5)缓冲通道的发送操作在通道的末尾插入一个元素,接收操作从通道的头部移除一个元素这个频道。如果通道已满,发送将阻塞,直到另一个goroutine执行接收。相反,如果通道为空,则接收阻塞,直到另一个goroutine执行发送。有没有感觉,其实bufferchannels和队列一样,解耦操作。单向通道类型chan<-int是一个只能发送的通道,类型<-chanint是一个只能接收的通道。任何双向通道都可以用作单向通道,但反之则不行。另外需要注意的是close只能用在发送通道上,用在接收通道上会报错。看一个单向通道的例子:packagemainimport"fmt"funccounter(outchan<-int){forx:=0;x<10;x++{out<-x}close(out)}funcsquarer(outchan<-int,in<-chanint){forv:=rangein{out<-v*v}close(out)}funcprinter(in<-chanint){forv:=rangein{fmt.Println(v)}}funcmain(){n:=make(chanint)s:=make(chanint)gocounter(n)gosquarer(s,n)printer(s)}syncsync包提供了两种锁类型:sync.Mutex和sync.RWMutex,前者是互斥锁,后者后者是读写锁。当一个goroutine获取到Mutex后,其他goroutine不管读还是写,都只能等到锁被释放。packagemainimport("fmt""sync""time")funcmain(){varmutexsync.Mutexwg:=sync.WaitGroup{}//主goroutine先获取锁fmt.Println("Locking(G0)")mutex.Lock()fmt.Println("locked(G0)")wg.Add(3)fori:=1;i<4;i++{gofunc(iint){//由于主goroutine先获取到锁,所以程序会阻塞在这里fmt5秒.Printf("Locking(G%d)\n",i)mutex.Lock()fmt.Printf("locked(G%d)\n",i)time.Sleep(time.Second*2)互斥锁。Unlock()fmt.Printf("unlocked(G%d)\n",i)wg.Done()}(i)}//主协程5秒后释放锁time.Sleep(time.Second*5)英尺。Println("readyunlock(G0)")mutex.Unlock()fmt.Println("unlocked(G0)")wg.Wait()}RWMutex属于经典的单写多读模型,当读锁被占用时,它将阻止写入,但不会阻止读取。写锁会阻止写入和读取。packagemainimport("fmt""sync""time")funcmain(){varrwMutexsync.RWMutexwg:=sync.WaitGroup{}Data:=0wg.Add(20)fori:=0;i<10;i++{gofunc(tint){//第一次运行后,写入unlock。//循环到第二次时,读锁后,goroutine没有阻塞,读成功。fmt.Println("Locking")rwMutex.RLock()deferrwMutex.RUnlock()fmt.Printf("Readdata:%v\n",Data)wg.Done()time.Sleep(2*time.Second)}(i)gofunc(tint){//写入锁需要在写入前解锁rwMutex.Lock()deferrwMutex.Unlock()Data+=tfmt.Printf("WriteData:%v%d\n",Data,t)wg.Done()time.Sleep(2*time.Second)}(i)}wg.Wait()}总结并发编程是Go的一个特性,也是它的核心功能之一。其实涉及的知识点很多,本文只是为了起到一个引玉的作用。这篇文章从介绍goroutines的简单使用开始,然后介绍了channels的概念。通道分为三种:UnbufferedchannelBufferedchannel单向通道最后介绍一下Go中的锁机制,分别是sync包提供的sync.Mutex(互斥锁)和sync.RWMutex(读写锁).Goroutine博大精深,后面的坑还是要慢慢踩的。
