前言

在golang中,只需要在函数调用前加上关键字go即可创建一个并发任务单元,而这个新建的任务会被放入队列中,等待调度器安排。相比系统的MB级别线程栈,goroutine的自定义栈只有2KB,这使得我们能够轻易创建上万个并发任务,如此对性能提升不少。但随之而来的有以下几个问题:

本文记录了笔者就以上几个问题进行探究的过程,文中给出了大部分问题的解决方案,同时也抛出了未解决的问题,期待与各位交流:p

准备

开始之前先定义一个常量const N=100以及一个HeavyWork函数,假定该函数具有极其冗长、复杂度高、难以解耦的特性

func HeavyWork(id int) {
rand.Seed(int64(id))
interval := time.Duration(rand.Intn(3)+1) * time.Second
time.Sleep(interval)
fmt.Printf("HeavyWork %-3d cost %v\n", id, interval)
}

以上定义的内容将在之后的代码中直接使用以缩减篇幅,大部分完整代码可在 Github: explore-goroutine 中找到

如何等待所有goroutine的退出

"Do not communicate by sharing memory; instead, share memory by communicating"——GO的一大设计哲学《Share Memory By Communicating》

翻译成中文就是,用通信来共享内存数据,而不要通过共享内存数据来进行通信。

Go中的goroutines和channel提供了一种优雅而独特的结构化并发软件的方法,我们可以利用通道(channel)的特性,来实现当前等待goroutine的操作。但是channel并不是当前这个场景的最佳方案,用它来实现的方式是稍显笨拙的,需要知道确定个数的goroutine,同时稍不注意就极易产生死锁,代码如下:

// "talk is cheap, show me the code."
func main() {
waitChan := make(chan int, 1)
for i := 0; i < N; i++ {
go func(n int) {
HeavyWork(n)
waitChan <- 1
}(i)
}
cnt := 0
for range waitChan {
cnt++
if cnt == N {
break
}
}
close(waitChan)
fmt.Println("finished")
}

上述代码使用了一个缓存大小为1的通道(channel),创建N个goroutine用于运行HeavyWork,每个任务完成后向waitChan写入一个数据,在收到N个完成信号后退出。

但事实上比较优雅的方式是使用go标准库sync,其中提供了专门的解决方案sync.WaitGroup用于等待一个goroutines集合的结束

// "talk is cheap, show me the code."
func main() {
wg := sync.WaitGroup{}
for i := 0; i < N; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
HeavyWork(n)
}(i)
}
wg.Wait()
fmt.Println("finished")
}

关于sync.WaitGroup的具体使用请参照官方文档 [GoDoc] sync.WaitGroup ,这里不再赘述

如何限制goroutine的创建数量(信号量实现)

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。

其中V操作会增加信号量的数值即释放资源,而P操作会减少它即占用资源

那么非常容易想到的就是利用channel(通道)缓存有限的特性,它允许我们可以自实现一个简单的数量控制,就如同使用信号量一般,在这基础再加上前面提到的sync.WaitGroup,我们可以打出一套组合拳,提供可阻塞的信号量PV操作,能够实现固定创建goroutine数量并且支持等待当前goroutine的退出。结构体定义如下:

type Semaphore struct {
Threads chan int
Wg sync.WaitGroup
}

而P操作只需在channel中加入一个元素同时调用WaitGroup.Add即可,这一操作完成对资源的申请

func (sem *Semaphore) P() {
sem.Threads <- 1
sem.Wg.Add(1)
}

相反则是V操作,进行资源的释放

func (sem *Semaphore) V() {
sem.Wg.Done()
<-sem.Threads
}

Wait则阻塞等待直到当前所有资源都归还,直接调用WaitGroup的方法即可

func (sem *Semaphore) Wait() {
sem.Wg.Wait()
}

完整代码可以在 Github: semaphore 中查看

利用上面的信号量就可以做到,在一个时刻的goroutines数量不会超过信号量值的大小,而某个goroutine退出后将返还占用的信号量,而正在等待的goroutine就可以立即申请,下图形象地展现了运行时的状态

怎么让goroutine主动退出

对于goroutine的主动退出,比较友好的做法就是循环监听一个channel,通过类似信号的方式来告知goroutine的”该退出了“,然后goroutine自己主动退出,这种做法在网上十分常见,也是Golang官方推荐的做法,思想也很简单。

func main() {
ok, quit := make(chan int, 1), make(chan int, 1)
go func() {
i := 0
for {
select {
case <-quit:
ok <- 1
return
default:
HeavyWork(i)
i++
}
}
}()
time.Sleep(5 * time.Second)
quit <- 1
<-ok
}

运行结果如下图

探索——如何从外部杀死goroutine

上面讲了一些关于goroutines和channel的简单使用,接下来终于写到本文的重点了。笔者并没有解决如何从外部杀死一个goroutine,但记录了尝试“杀死”中的可行或不可行方法,希望对各位有所帮助。

因为近期在开发中遇到这样一个问题,当一个函数是极其冗长、复杂度高、难以解耦的顺序结构代码时(例如某个极其复杂无循环结构的加密算法),而且由于数据量巨大,需要反复调用该函数,由于每运行一次,程序都会消耗大量的时间、空间,那么当一个任务已经被用户抛弃时,如何才能抛弃仍在做着无用功的goroutine?

为了达到“杀死goroutine”的目的,笔者做了很多尝试,如

  • select结构(条件实现)
  • panic退出机制(失败)
  • 获取pid杀死(失败)
  • ptrace单步调试(失败)
  • ...(失败)

利用select语句实现

关于“如何杀死goroutine”,网上有一部分答案就是利用select实现的,但是这种方式实现的代码并不适用于服务类的程序,但是对于一般非服务类程序的确能够实现杀死goroutine的效果,代码如下:

func main() {
wrapper := func() chan int {
c := make(chan int)
go func() {
HeavyWork(0)
c <- 1
}()
return c
}
select {
case <-wrapper():
case <-time.After(1 * time.Second):
fmt.Println("time limit exceed")
}
// time.Sleep(3 * time.Second)
}



但是一旦主函数没有立即退出,而是作为某种服务而继续运行时,这里删除了main函数的最后一行注释time.Sleep(3 * time.Second),延迟三秒后退出。可以看见尽管已经超时并输出"time limit exceed"之后,HeavyWork在main函数没退出前依旧在运行。效果如下

所以使用select-timeout的方式比较适合实时退出类型的程序,能够实现一定程度上的并发控制,

小结

就目前而言,还没有完美的方案来解决控制goroutine的问题,事实上Go似乎并不允许和推荐人们直接控制goroutine,所以暂时还无法做到从外部直接控制goroutine的生命周期,所以比较推荐的做法还是只能通过goroutine主动退出的方法,循环监听channel,在发出退出信号后最多只消耗一轮资源后就退出,但这就要求该代码具有循环结构否则就很难使用。有更好解决方案的朋友,请务必告诉我!

转载请注明出处:http://www.cnblogs.com/tr3e/p/7995689.html

Golang 探索对Goroutine的控制方法的更多相关文章

  1. Golang之chan/goroutine(转)

    原文地址:http://tchen.me/posts/2014-01-27-golang-chatroom.html?utm_source=tuicool&utm_medium=referra ...

  2. golang并发编程goroutine+channel(一)

    go语言的设计初衷除了在不影响程序性能的情况下减少复杂度,另一个目的是在当今互联网大量运算下,如何让程序的并发性能和代码可读性达到极致.go语言的并发关键词 "go" go dos ...

  3. Golang 入门 : 等待 goroutine 完成任务

    Goroutine 是 Golang 中非常有用的功能,但是在使用中我们经常碰到下面的场景:如果希望等待当前的 goroutine 执行完成,然后再接着往下执行,该怎么办?本文尝试介绍这类问题的解决方 ...

  4. golang中的goroutine

    1. 概念 go中可以并发执行的活动单元称为goroutine当一个go程序启动时,一个执行main function的goroutine会被创建,称为main goroutinego func() ...

  5. golang学习笔记 --- goroutine

    package main import ( "fmt" "io" "io/ioutil" "net/http" &quo ...

  6. Golang教程:goroutine信道

    在上一篇教程中,我们讨论了如何使用协程实现并发.在这篇教程中,我们将讨论信道以及如何使用信道实现协程间通信. 什么是信道 信道(Channel)可以被认为是协程之间通信的管道.与水流从管道的一端流向另 ...

  7. Golang教程:goroutine协程

    在上一篇中,我们讨论了并发,以及并发和并行的区别.在这篇教程中我们将讨论在Go中如何通过Go协程实现并发. 什么是协程 Go协程(Goroutine)是与其他函数或方法同时运行的函数或方法.可以认为G ...

  8. Golang并发编程——goroutine、channel、sync

    并发与并行 并发和并行是有区别的,并发不等于并行. 并发 两个或多个事件在同一时间不同时间间隔发生.对应在Go中,就是指多个 goroutine 在单个CPU上的交替运行. 并行 两个或者多个事件在同 ...

  9. [golang学习] goroutine调度

    这两天有些闲功夫, 学习下golang, 确实非常简洁. 不过有些缺憾. 在我的测试中. golang的调度(goroutine)似乎不是非常好. func say(k int) { fmt.Prin ...

随机推荐

  1. express紧急回顾随笔

    四行代码搭建服务器 var express = require('express'); var app = express(); //设定静态路径 所有请求优先在此路径查找 //不要把服务器配置JS文 ...

  2. where id in用 order by field 保持排序

    转载自http://blog.linuxphp.org/archives/1588/ 先看下mysql的默认排序 select id from article where id in(63261,63 ...

  3. python生成式

    本篇将介绍Python的列表生成式,更多内容请参考:Python列表生成式 列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式. 举个 ...

  4. Leetcode题解(25)

    77. Combinations 题目 分析:求给定数字n,k的组合数,方法是采用深度搜索算法,代码如下(copy网上代码) class Solution { public: void dfs77(v ...

  5. Turn the corner

    Problem Description Mr. West bought a new car! So he is travelling around the city. One day he comes ...

  6. 哈尔滨理工大学第六届程序设计团队 I-Team

    /* 以前做过一个插队的题,这个类似从后往前操作 */ #include <iostream> #include <stdio.h> #include <algorith ...

  7. SharePoint Server 2013 安装篇 - 如何解决无法找到 .net 4.5 的问题

    SharePoint Server 2013 在安装前,是不能安装 VS 等会自动安装 .net 4.5.x 以上版本的 .net Framework 的软件的.因为安装了 .net Framewor ...

  8. idea tomee required to support ear ejb deployment问题

    先删掉原来的uname,然后添加项目即可

  9. Winform C# 简单实现子窗口显示进度条

    主窗口代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data ...

  10. 《天书夜读:从汇编语言到windows内核编程》一 汇编指令与C语言

    1. Debug模式下,VC++6.0下断点运行,按CTRL+F11可查看汇编代码:另外可以用cl /c /FAs YourCppFile.cpp命令行在同目录生成YourCppFile.asm汇编文 ...