[Golang] Goroutine Concurrency多执行绪浅谈

Goroutine

Golang 中多执行绪称为" Goroutine" ,在其他程式语言中大多称为" Thread",本文提供常用的五种用法,下文提供範例并详述使用方式(channel, context, sync.WaitGroup, Select, sync.Lock)。

在Golang 中使用Goroutine 只要在 func前面加上 “ go”关键字就可以直接启动执行序。一般来说golang 执行绪会随着父亲死亡而跟着release。如下範例程式:

package mainimport (    "fmt"    "time")// 範例主要展示主执行绪执行结束后,会将子执行绪releasefunc main() {//     执行子执行序    go func(){        time.Sleep(100000000)        fmt.Println("goroutine Done!")    }()    fmt.Println("Done!")}


Goroutine 基本用法程式执行结果

以上执行的结果为"Done!",原因是在未执行完Goroutine的时候就自动的被释放掉了,导致不会印出Goroutine Done!的字样。

一般来说使用多执行绪中,最常会遇到会5个问题如下:

多执行绪相互沟通等待一执行绪结束后再接续工作多执行绪共用同一个变数不同执行绪产出影响后续逻辑兄弟执行绪间不求同生只求同死

根据上述问题,基本上都可以透过channel, context, sync.WaitGroup, Select, sync.Mutex等方式解决,下面详细解析如何解决:

1. 多执行绪相互沟通

传统作业系统学科中所学的,执行绪间的存取有两种方式:

共用透过记忆体 => 而在这边介绍的都是以记忆体的方式进行存取透过Socket的方式

Goroutine的沟通主要可以透过channel、全域变数进行操作。Channel有点类似Linux C语言中pipe的方式,主要分成分为写入端与读取端。而全域变数的方式就是单纯变数。

首先Channel的部份,宣告的方式是透过chan关键字宣告,搭配make 关键字令出空间,语法为: make(chan 型别 容量) 。如下範例程式:

package mainimport ("fmt""time")// 範例: channel控制执行绪,收集两个执行序的资料 1、2func main() {// 宣告channel make(chan 型态 <容量>)val := make(chan int)      // 执行第一个执行绪go func() {fmt.Println("intput val 1")val <- 1 //注入资料1}()        // 执行第二个执行绪go func() {fmt.Println("intput val 2")val <- 2  //注入资料2                time.Sleep(time.Millisecond * 100)}()ans := []int{}for {ans = append(ans, <-val)//取出资料 fmt.Println(ans)if len(ans) == 2 {break}}}


example 执行结果

Tips: <- chan // 代表的是从channel中取出 chan <- //代表注入资料进去channel

另一个方式就是比较传统的方式进行存取,直接使用变数进行存取,如下範例程式:

package mainimport ("fmt""time")// 範例: 共用变数func main() {val := 1// 执行第一个执行绪go func() {fmt.Println("first", val)}()// 执行第二个执行绪go func() {fmt.Println("sec ", val)}()time.Sleep(time.Millisecond * 500)}


example 2 执行结果

2. 等待一执行绪结束后再接续工作

比较熟悉Java的人可以联想到Join的概念,而在Golang中要做到等待的这件事情有两个方法,一个是sync.WaitGroup、另一个是channel。

首先Sync.WaitGroup 像是一个计数器,启动一条Goroutine 计数器 +1; 反之结束一条 -1。若计数器为複数代表Error。如下範例程式:

package mainimport ("log""sync""time")//範例: 等待一执行绪结束后再接续工作(使用WaitGroup)func main() {var wg sync.WaitGroup// 执行执行绪go func() {defer wg.Done()//defer表示最后执行,因此该行为最后执行wg.Done()将计数器-1defer log.Println("goroutine drop out")log.Println("start a go routine")time.Sleep(time.Second)//休息一秒钟}()wg.Add(1)//计数器+1time.Sleep(time.Millisecond * 30)//休息30 mslog.Println("wait a goroutine")wg.Wait()//等待计数器归0}


example 3执行结果

Channel 的作法是利用等待提取、等待可注入会lock住的特性,达到Sync.WaitGroup 的功能。如下範例程式:

package mainimport ("log""time")func main() {forever := make(chan int)//宣告一个channel//执行执行序go func() {defer log.Println("goroutine drop out")log.Println("start a go routine")time.Sleep(time.Second)//等待1秒钟forever <- 1 //注入1进入forever channel}()time.Sleep(time.Millisecond * 30)//等待30 mslog.Println("wait a goroutine")<-forever // 取出forever channel 的资料}


example 4 执行结果

3. 多执行绪共用同一个变数

在多执行绪的世界,只是读取一个共用变数是不会有问题的,但若是要进行修改可能会因为多个执行绪正在存取造成concurrent 错误。若要解决这种情况,必须在存取时先将资源lock住,就可以避免这种问题。如下範例程式:

package mainimport (        "fmt"        "sync"        "time")//範例: 多个执行序读写同一个变数func main() {        var lock sync.Mutex // 宣告Lock 用以资源佔有与解锁        var wg sync.WaitGroup // 宣告WaitGroup 用以等待执行序        val := 0        // 执行 执行绪: 将变数val+1        go func() {                defer wg.Done() //wg 计数器-1                //使用for迴圈将val+1                for i := 0; i < 10; i++ {                        lock.Lock()//佔有资源                        val++                        fmt.Printf("First gorutine val++ and val = %d\n", val)                        lock.Unlock()//释放资源                        time.Sleep(3000)                }        }()             // 执行 执行绪: 将变数val+1        go func() {                defer wg.Done()//wg 计数器-1                //使用for迴圈将val+1                for i := 0; i < 10; i++ {                        lock.Lock() //佔有资源                        val++                        fmt.Printf("Sec gorutine val++ and val = %d\n", val)                        lock.Unlock()// 释放资源                        time.Sleep(1000)                }        }()        wg.Add(2)//记数器+2        wg.Wait()//等待计数器归零}


example 5执行结果

Tips: sync.Mutex: 宣告资源锁 Lock: 在存取时需要将资源锁住 Unlock: 存取结束后需要释放出来给需要的执行序使用

4. 不同执行绪产出影响后续逻辑

执行多执行绪控制时,可能会多个执行绪产生出的结果都不一样,但每个结果都会影响下一步的动作。例如: 在做error控制时,只要某一个Goroutine 错误时,就做相对应的处置,这样的需求中,需要提不同错误不同的对应处置。此时在这种情况下,就需要select多路複用的方式解,如下範例程式:

package mainimport ("fmt""math/rand""time")//範例:不同执行绪产出影响后续逻辑,使用多路复用。func main() {firstRoutine := make(chan string) //宣告给第1个执行序的channelsecRoutine := make(chan string) //宣告给第2个执行序的channelrand.Seed(time.Now().UnixNano())go func() {r := rand.Intn(100)time.Sleep(time.Microsecond * time.Duration(r))//随机等待 0~100 msfirstRoutine <- "first goroutine"}()go func() {r := rand.Intn(100)time.Sleep(time.Microsecond * time.Duration(r))//随机等待 0~100 mssecRoutine <- "Sec goroutine"}()select {case f := <-firstRoutine: //第1个执行序先执行后所要做的动作fmt.Println(f)returncase s := <-secRoutine://第2个执行序先执行后所要做的动作fmt.Println(s)return}}


example 6执行结果

上面程式码的例子,当其中一条Goroutine先结束时,主程式就会自动结束。而Select的用法就是去听哪一个channel已经先被注入资料,而做相对应的动作,若同时则是随机採用对应的方案。

5. 兄弟执行绪间不求同生只求同死

在Goroutine主要的基本用法与应用,在上述都可以做到。在这一章节主要是介绍一些进阶用法" Context"。这种用法主要是在go 1.7之后才正式被收入官方套件中,使得更方便的控制Goroutine的生命週期。

主要提供以下几种方法:

WithCancel: 当parent呼叫cancel方法之后,所有相依的Goroutine 都会透过context接收parent要所有子执行序结束的讯息。WithDeadline: 当所设定的时间到时所有相依的Goroutine 都会透过context接收parent要所有子执行序结束的讯息。WithTimeout: 当所设定的日期到时所有相依的Goroutine 都会透过context接收parent要所有子执行序结束的讯息。WithValue: parent可透过讯息的方式与所有相依的Goroutine进行沟通。

以WithTimeout作为例子,下面例子是透过context的方式设定当超过10 ms没结束Goroutine的执行,则会发起"context deadline exceed"的错误讯息,或者成功执行就发出overslept的讯息,如下範例程式:

package mainimport (        "context"        "fmt"        "sync"        "time")//範例: 兄弟执行绪间不求同生只求同死,使用contextconst shortDuration = 1001 * time.Millisecondvar wg sync.WaitGroup //宣告计数器func aRoutine(ctx context.Context) {        defer wg.Done() //当该执行绪执行到最后计数器-1        select {        case <-time.After(1 * time.Second): // 1秒之后继续执行                fmt.Println("overslept")        case <-ctx.Done():                fmt.Println(ctx.Err()) // context deadline exceeded        }}func main() {        d := time.Now().Add(shortDuration)        ctx, cancel := context.WithDeadline(context.Background(), d)//宣告一个context.WithDeadline并注入1.001秒之类为执行完的执行绪将发产出ctx.Err        defer cancel() // 程式最后执行WithDeadline失效        go aRoutine(ctx) // 启动aRoutine执行序        wg.Add(1) // 计数器+1        wg.Wait()//等待计数器归零}


example 7 执行结果

Tips: context.Background(): 取得Context的实体 context.WithDeadline(Context实体, 时间): 使用WithDeadline并设定好时间 Cancel 则是在程式结束前需要被使用,否则会有memory leak的错误讯息

总结

在Golang多执行绪的世界中,最常用的就是共用变数、channel、 Select、sync.WaitGroup、sync.Lock等方式,比较进阶的用法是Context。Context主要就是官方提供一个interface使得大家更方便的去操作,若使用者不想使用也是可以透过channel自行实作。


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章