Go并发控制简明教程-WaitGroup和Context简明教程

控制并发的两种方式

  • 使用WaitGroup
  • 使用Context

WaitGroup简单例子

使用WaitGroup可以把一个作业分包,使用多个协程完成,节省作业处理时间。

func main(){
    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        time.Sleep(2 * time.Second)
        fmt.Println("job 1 done.")
        wg.Done()
    }()

    go func() {
        time.Sleep(1 * time.Second)
        fmt.Println("job 2 done.")
        wg.Done()
    }()

    fmt.Println("Wait for job finish")
    wg.Wait()
}

声明一个WaitGroup,大小设置为2,表示有需要等待两个子协程完成。创建两个子协程,分别以睡眠代替作业负载,子协程结束前调用wg.Done()表示此任务已经完成。主协程在wg.Wait()时阻塞,等待子协程结束,countDown数为0时就继续执行。countDown数如何减少呢?通过wg.Done()完成的。

以下是输出:

~ » go run main.go
Wait for job finish
job 2 done.
job 1 done.

但是,我们会发现,创建一个子协程后,主协程无法控制子协程,只能等待,不能因为时间过长而发送信号通知子协程停止执行。

Channel + Selete控制协程

为了解决上面提到的问题,可以简单使用Channel + Selete来实现子协程执行时间过长后,主协程通知子协程结束返回的功能。

func main(){
    stop := make(chan bool)

    go func() {
        for {
            select {
            case <- stop:
                fmt.Println("job timeout return")
                return
            default:
                fmt.Println("job still working")
                time.Sleep(1 * time.Second)
            }
        }
    }()

    time.Sleep(5 * time.Second)
    fmt.Println("Timeout stop the job")
    stop <- true
    time.Sleep(5 * time.Second)
    fmt.Println("Main goroutine finished!!")
}

创建一个布尔类型的Channel用于通知子协程是否停止。启动一个子协程模拟搞IO作业,里面有一个select,用于等待主协程的stop信号,如果没有此信号就执行default下面的语句。主协程5s后表示子协程执行超时,那么发送stop信号,子协程接收到信号后返回。主协程正常结束。

下面是执行日志:

~ » go run main.go
job still working
job still working
job still working
job still working
job still working
Timeout stop the job
job timeout return
Main goroutine finished!!

Context简单例子

上面的Channel + Selete简单例子只能解决一层主子协程的控制,如果子协程里也有子协程,那么此方法就无法奏效了。

不如下图,Worker2启动Job03协程工作,而Job03也要启动Job04协程配合工作。此时一旦Job04超时未返回,需要把此信号传递给Worker02,让它通知Job03和Job04停止工作,这种情况就很难解决了。

层级子协程

以此,Go引入了Context处理这种情况,Context是一种很好的设计模式。

下面使用Context改造Channel + Selete的例子:

func main(){
    ctx, cancel := context.WithCancel(context.Background())

    go func() {
        for {
            select {
            case <- ctx.Done():
                fmt.Println("job timeout return")
                return
            default:
                fmt.Println("job still working")
                time.Sleep(1 * time.Second)
            }
        }
    }()

    time.Sleep(5 * time.Second)
    fmt.Println("Timeout stop the job")
    cancel()
    time.Sleep(5 * time.Second)
    fmt.Println("Main goroutine finished!!")
}

改造点如下:

  • 把创建一个stop的Channel换成创建一个Context。
  • 把子协程里监听stop的Channel的信号换成ctx.Done()
  • 把在主协程给Channel写入true信号换成调用创建Context时返回的cancel方法。

context.WithCancel方法用于创建一个可以发送取消信号的Context,它的入参需要一个父Context,此处使用了context.Background(),它正是根Context。调用cancel方法,会通过ctx.Done()通知子协程,然后使用select处理此信号。

Context控制多个子协程
func main(){
    ctx, cancel := context.WithCancel(context.Background())

    go Work(ctx, "node1")
    go Work(ctx, "node2")
    go Work(ctx, "node3")

    time.Sleep(5 * time.Second)
    fmt.Println("Timeout stop the job")
    cancel()
    time.Sleep(5 * time.Second)
    fmt.Println("Main goroutine finished!!")
}

func Work(ctx context.Context, name string){
    for {
        select {
        case <- ctx.Done():
            fmt.Println(name, "job timeout return")
            return
        default:
            fmt.Println(name, "job still working")
            time.Sleep(1 * time.Second)
        }
    }
}

把子协程的方法抽取出来,我们尝试启动3个子协程,调用cancel()方法发送取消信号,所有子协程都停止了。

如下面打印日志所示:

~ » go run main.go
node2 job still working
node3 job still working
node1 job still working
node1 job still working
node3 job still working
node2 job still working
node3 job still working
node2 job still working
node1 job still working
node1 job still working
node2 job still working
node3 job still working
node3 job still working
node1 job still working
node2 job still working
Timeout stop the job
node2 job timeout return
node1 job timeout return
node3 job timeout return
Main goroutine finished!!

第二个例子,符合上面图的情况,主协程启动node1子协程,node1子协程启动node2子协程,那么只需要把Context传递下去,所有的子协程就能接受到主协程的取消信息,然后马上返回。

func main(){
    ctx, cancel := context.WithCancel(context.Background())

    go Work(ctx, "node1")

    time.Sleep(5 * time.Second)
    fmt.Println("Timeout stop the job")
    cancel()
    time.Sleep(5 * time.Second)
    fmt.Println("Main goroutine finished!!")
}

func Work(ctx context.Context, name string){
    go Work2(ctx, "node2")
    for {
        select {
        case <- ctx.Done():
            fmt.Println(name, "job timeout return")
            return
        default:
            fmt.Println(name, "job still working")
            time.Sleep(1 * time.Second)
        }
    }
}

func Work2(ctx context.Context, name string){
    for {
        select {
        case <- ctx.Done():
            fmt.Println(name, "job timeout return")
            return
        default:
            fmt.Println(name, "job still working")
            time.Sleep(1 * time.Second)
        }
    }
}

执行日志如下:

~ » go run main.go
node2 job still working
node1 job still working
node1 job still working
node2 job still working
node1 job still working
node2 job still working
node2 job still working
node1 job still working
node1 job still working
node2 job still working
Timeout stop the job
node2 job timeout return
node1 job timeout return
Main goroutine finished!!

晚安~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 开发go程序的时候,时常需要使用goroutine并发处理任务,有时候这些goroutine是相互独立的,而有的时...
    驻马听雪阅读 2,385评论 0 21
  • go并发编程入门到放弃 并发和并行 并发:一个处理器同时处理多个任务。 并行:多个处理器或者是多核的处理器同时处理...
    yangyunfeng阅读 507评论 0 2
  • GoLang并发控制(上) 在go程序中,最被人所熟知的便是并发特性,一方面有goroutine这类二级线程,对这...
    不喜欢夜雨天阅读 3,555评论 0 8
  • 本文从上下文Context、同步原语与锁、Channel、调度器四个方面介绍Go语言是如何实现并发的。本文绝大部分...
    彦帧阅读 1,500评论 1 3
  • 前言:本专题用于记录自己(647)在Go语言方向的学习和积累。系列内容比较偏基础,推荐给想要入门Go语言开发者们阅...
    齐舞647阅读 816评论 0 3