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个问题如下:
根据上述问题,基本上都可以透过channel, context, sync.WaitGroup, Select, sync.Mutex等方式解决,下面详细解析如何解决:
传统作业系统学科中所学的,执行绪间的存取有两种方式:
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 执行结果
比较熟悉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 执行结果
在多执行绪的世界,只是读取一个共用变数是不会有问题的,但若是要进行修改可能会因为多个执行绪正在存取造成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: 存取结束後需要释放出来给需要的执行序使用
执行多执行绪控制时,可能会多个执行绪产生出的结果都不一样,但每个结果都会影响下一步的动作。例如: 在做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已经先被注入资料,而做相对应的动作,若同时则是随机采用对应的方案。
在Goroutine主要的基本用法与应用,在上述都可以做到。在这一章节主要是介绍一些进阶用法" Context"。这种用法主要是在go 1.7之後才正式被收入官方套件中,使得更方便的控制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战斗营} 的第十一周 #两天转换程序语言
Nest CSS with Sass 在Sass中,可以将CSS一层一层的包起来,不但简单直觉能直接...
suspend他并不能切换线程,切换线程的是内部自带的suspend函数,ex. withConte...
大家好,距离完赛越来越近了,过完最後一天的双十连假,心情也开始忧郁了QQ,还好本系列复杂的文章差不多...
Hello 大家, 上班了, 厌世的上班日... 继昨天文末说的, iOS的介面不同了, 我稍微的逛...
Nest 在大多数情况下是采用 单例模式 (Singleton pattern) 来维护各个实例,也...