[Golang] Goroutine Concurrency多执行绪浅谈

Goroutine

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

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

package main

import (
    "fmt"
    "time"
)
// 范例主要展示主执行绪执行结束後,会将子执行绪release

func main() {
//     执行子执行序
    go func(){
        time.Sleep(100000000)
        fmt.Println("goroutine Done!")
    }()
    fmt.Println("Done!")
}


Goroutine 基本用法程序执行结果

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

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

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

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

1. 多执行绪相互沟通

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

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

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

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

package main

import (
	"fmt"
	"time"
)
// 范例: channel控制执行绪,收集两个执行序的资料 1、2
func 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 main

import (
	"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 main

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


example 3执行结果

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

package main

import (
	"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 ms
	log.Println("wait a goroutine")
	<-forever // 取出forever channel 的资料
}


example 4 执行结果

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

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


package main

import (
        "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 main

import (
	"fmt"
	"math/rand"
	"time"
)
//范例:不同执行绪产出影响後续逻辑,使用多路复用。
func main() {
	firstRoutine := make(chan string) //宣告给第1个执行序的channel
	secRoutine := make(chan string) //宣告给第2个执行序的channel
	rand.Seed(time.Now().UnixNano())

	go func() {
		r := rand.Intn(100)
		time.Sleep(time.Microsecond * time.Duration(r))//随机等待 0~100 ms
		firstRoutine <- "first goroutine"
	}()
	go func() {
		r := rand.Intn(100)
		time.Sleep(time.Microsecond * time.Duration(r))//随机等待 0~100 ms
		secRoutine <- "Sec goroutine"
	}()
	select {
	case f := <-firstRoutine: //第1个执行序先执行後所要做的动作
		fmt.Println(f)
		return
	case s := <-secRoutine://第2个执行序先执行後所要做的动作
		fmt.Println(s)
		return
	}
}


example 6执行结果

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

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

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

主要提供以下几种方法:

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

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

package main

import (
        "context"
        "fmt"
        "sync"
        "time"
)

//范例: 兄弟执行绪间不求同生只求同死,使用context

const shortDuration = 1001 * time.Millisecond

var 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自行实作。


<<:  资讯安全战略(information security strategy)

>>:  {CMoney战斗营} 的第十一周 #两天转换程序语言

[Day 08] Sass - Nesting

Nest CSS with Sass 在Sass中,可以将CSS一层一层的包起来,不但简单直觉能直接...

day9 Kotlin coroutine 的黑魔法 suspend

suspend他并不能切换线程,切换线程的是内部自带的suspend函数,ex. withConte...

[DAY28]番外篇-使用fetch发送请求

大家好,距离完赛越来越近了,过完最後一天的双十连假,心情也开始忧郁了QQ,还好本系列复杂的文章差不多...

Day07 iOS15介面的小差别

Hello 大家, 上班了, 厌世的上班日... 继昨天文末说的, iOS的介面不同了, 我稍微的逛...

[NestJS 带你飞!] DAY17 - Injection Scopes

Nest 在大多数情况下是采用 单例模式 (Singleton pattern) 来维护各个实例,也...