编程语言应用

首页 » 常识 » 预防 » Go专栏并发编程goroutine,c
TUhjnbcbe - 2021/11/8 3:19:00

优雅的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色。

在当今这个多核时代,并发编程的意义不言而喻。使用Go开发并发程序,操作起来非常简单,语言级别提供关键字go用于启动协程,并且在同一台机器上可以启动成千上万个协程。

下面就来详细介绍。

goroutine

Go语言的并发执行体称为goroutine,使用关键词go来启动一个goroutine。

go关键词后面必须跟一个函数,可以是有名函数,也可以是无名函数,函数的返回值会被忽略。

go的执行是非阻塞的。

先来看一个例子:

packagemainimport("fmt""time")funcmain(){gospinner(*time.Millisecond)constn=45fibN:=fib(n)fmt.Printf("\rFibonacci(%d)=%d\n",n,fibN)//Fibonacci(45)=}funcspinner(delaytime.Duration){for{for_,r:=range`-\

/`{fmt.Printf("\r%c",r)time.Sleep(delay)}}}funcfib(xint)int{ifx2{returnx}returnfib(x-1)+fib(x-2)}

从执行结果来看,成功计算出了斐波那契数列的值,说明程序在spinner处并没有阻塞,而且spinner函数还一直在屏幕上打印提示字符,说明程序正在执行。

当计算完斐波那契数列的值,main函数打印结果并退出,spinner也跟着退出。

再来看一个例子,循环执行10次,打印两个数的和:

packagemainimport"fmt"funcAdd(x,yint){z:=x+yfmt.Println(z)}funcmain(){fori:=0;i10;i++{goAdd(i,i)}}

有问题了,屏幕上什么都没有,为什么呢?

这就要看Go程序的执行机制了。当一个程序启动时,只有一个goroutine来调用main函数,称为主goroutine。新的goroutine通过go关键词创建,然后并发执行。当main函数返回时,不会等待其他goroutine执行完,而是直接暴力结束所有goroutine。

那有没有办法解决呢?当然是有的,请往下看。

channel

一般写多进程程序时,都会遇到一个问题:进程间通信。常见的通信方式有信号,共享内存等。goroutine之间的通信机制是通道channel。

使用make创建通道:

ch:=make(chanint)//ch的类型是chanint

通道支持三个主要操作:send,receive和close。

ch-x//发送x=-ch//接收-ch//接收,丢弃结果close(ch)//关闭无缓冲channel

make函数接受两个参数,第二个参数是可选参数,表示通道容量。不传或者传0表示创建了一个无缓冲通道。

无缓冲通道上的发送操作将会阻塞,直到另一个goroutine在对应的通道上执行接收操作。相反,如果接收先执行,那么接收goroutine将会阻塞,直到另一个goroutine在对应通道上执行发送。

所以,无缓冲通道是一种同步通道。

下面我们使用无缓冲通道把上面例子中出现的问题解决一下。

packagemainimport"fmt"funcAdd(x,yint,chchanint){z:=x+ych-z}funcmain(){ch:=make(chanint)fori:=0;i10;i++{goAdd(i,i,ch)}fori:=0;i10;i++{fmt.Println(-ch)}}

可以正常输出结果。

主goroutine会阻塞,直到读取到通道中的值,程序继续执行,最后退出。

缓冲channel

创建一个容量是5的缓冲通道:

ch:=make(chanint,5)

缓冲通道的发送操作在通道尾部插入一个元素,接收操作从通道的头部移除一个元素。如果通道满了,发送会阻塞,直到另一个goroutine执行接收。相反,如果通道是空的,接收会阻塞,直到另一个goroutine执行发送。

有没有感觉,其实缓冲通道和队列一样,把操作都解耦了。

单向channel

类型chan-int是一个只能发送的通道,类型-chanint是一个只能接收的通道。

任何双向通道都可以用作单向通道,但反过来不行。

还有一点需要注意,close只能用在发送通道上,如果用在接收通道会报错。

看一个单向通道的例子:

packagemainimport"fmt"funccounter(outchan-int){forx:=0;x10;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)}sync

sync包提供了两种锁类型: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;i4;i++{gofunc(iint){//由于主goroutine先获取锁,程序开始5秒会阻塞在这里fmt.Printf("Locking(G%d)\n",i)mutex.Lock()fmt.Printf("locked(G%d)\n",i)time.Sleep(time.Second*2)mutex.Unlock()fmt.Printf("unlocked(G%d)\n",i)wg.Done()}(i)}//主goroutine5秒后释放锁time.Sleep(time.Second*5)fmt.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;i10;i++{gofunc(tint){//第一次运行后,写解锁。//循环到第二次时,读锁定后,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的特色,也是核心功能之一了,涉及的知识点其实是非常多的,本文也只是起到一个抛砖引玉的作用而已。

本文开始介绍了goroutine的简单用法,然后引出了通道的概念。

通道有三种:

无缓冲通道

缓冲通道

单向通道

最后介绍了Go中的锁机制,分别是sync包提供的sync.Mutex(互斥锁)和sync.RWMutex(读写锁)。

goroutine博大精深,后面的坑还是要慢慢踩的。

文章中的脑图和源码都上传到了GitHub,有需要的同学可自行下载。

1
查看完整版本: Go专栏并发编程goroutine,c