【实践】Golang的goroutine和通道的8种姿势

前言

如果说php是最好的语言,那么golang就是最并发的语言。
支持golang的并发很重要的一个是goroutine的实现,那么本文将重点围绕goroutine来做一下相关的笔记,以便日后快速留恋。

10s后,以下知识点即将靠近:
1.从并发模型说起
2.goroutine的简介
3.goroutine的使用姿势
4.通道(channel)的简介
5.重要的四种通道使用
6.goroutine死锁与处理
7.select的简介
8.select的应用场景
9.select死锁

正文

1.从并发模型说起

看过很多大神简介,各种研究高并发,那么就通俗的说下并发。
并发目前来看比较主流的就三种:

1.多线程

每个线程一次处理一个请求,线程越多可并发处理的请求数就越多,但是在高并发下,多线程开销会比较大。

2.协程

无需抢占式的调度,开销小,可以有效的提高线程的并发性,从而避免了线程的缺点的部分

3.基于异步回调的IO模型

说一个熟悉的,比如nginx使用的就是epoll模型,通过事件驱动的方式与异步IO回调,使得服务器持续运转,来支撑高并发的请求

为了追求更高效和低开销的并发,golang的goroutine来了。

2.goroutine的简介

定义:在go里面,每一个并发执行的活动成为goroutine。

详解:goroutine可以认为是轻量级的线程,与创建线程相比,创建成本和开销都很小,每个goroutine的堆栈只有几kb,并且堆栈可根据程序的需要增长和缩小(线程的堆栈需指明和固定),所以go程序从语言层面支持了高并发。

程序执行的背后:当一个程序启动的时候,只有一个goroutine来调用main函数,称它为主goroutine,新的goroutine通过go语句进行创建。

3.goroutine的使用姿势

3.1单个goroutine创建

在函数或者方法前面加上关键字go,即创建一个并发运行的新goroutine。

上代码:

package main

import (
    "fmt"
    "time"
)

func HelloWorld() {
    fmt.Println("Hello world goroutine")
}

func main() {
    go HelloWorld()      // 开启一个新的并发运行
    time.Sleep(1*time.Second)
    fmt.Println("我后面才输出来")
}

以上执行后会输出:

Hello world goroutine
我后面才输出来

需要注意的是,执行速度很快,一定要加sleep,不然你一定可以看到goroutine里头的输出。

这也说明了一个关键点:当main函数返回时,所有的gourutine都是暴力终结的,然后程序退出。

3.2多个goroutine创建

package main

import (
    "fmt"
    "time"
)

func DelayPrint() {
    for i := 1; i <= 4; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Println(i)
    }
}

func HelloWorld() {
    fmt.Println("Hello world goroutine")
}

func main() {
    go DelayPrint()    // 开启第一个goroutine
    go HelloWorld()    // 开启第二个goroutine
    time.Sleep(2*time.Second)
    fmt.Println("main function")
}

函数输出:

Hello world goroutine
1
2
3
4
5
main function
1
2
3
4
5
6
7

有心的同学可能会发现,DelayPrint里头有sleep,那么会导致第二个goroutine堵塞或者等待吗?
答案是:no
疑惑:当程序执行go FUNC()的时候,只是简单的调用然后就立即返回了,并不关心函数里头发生的故事情节,所以不同的goroutine直接不影响,main会继续按顺序执行语句。

4.通道(channel)的简介

4.1简介

如果说goroutine是Go并发的执行体,那么”通道”就是他们之间的连接。
通道可以让一个goroutine发送特定的值到另外一个goroutine的通信机制。

4.2声明&传值&关闭

声明

var ch chan int      // 声明一个传递int类型的channel
ch := make(chan int) // 使用内置函数make()定义一个channel

//=========

ch <- value          // 将一个数据value写入至channel,这会导致阻塞,直到有其他goroutine从这个channel中读取数据
value := <-ch        // 从channel中读取数据,如果channel之前没有写入数据,也会导致阻塞,直到channel中被写入数据为止

//=========

close(ch)            // 关闭channel

有没注意到关键字”阻塞“?,这个其实是默认的channel的接收和发送,其实也有非阻塞的,请看下文。

5.重要的四种通道使用

1.无缓冲通道

说明:无缓冲通道上的发送操作将会被阻塞,直到另一个goroutine在对应的通道上执行接收操作,此时值才传送完成,两个goroutine都继续执行。

上代码:

package main

import (
    "fmt"
    "time"
)
var done chan bool
func HelloWorld() {
    fmt.Println("Hello world goroutine")
    time.Sleep(1*time.Second)
    done <- true
}
func main() {
    done = make(chan bool)  // 创建一个channel
    go HelloWorld()
    <-done
}

输出:

Hello world goroutine

由于main不会等goroutine执行结束才返回,前文专门加了sleep输出为了可以看到goroutine的输出内容,那么在这里由于是阻塞的,所以无需sleep。

(小尝试:可以将代码中”done <- true”和”<-done”,去掉再执行,看看会发生啥?)

2.管道

通道可以用来连接goroutine,这样一个的输出是另一个输入。这就叫做管道。

例子:

package main

import (
    "fmt"
    "time"
)
var echo chan string
var receive chan string

// 定义goroutine 1 
func Echo() {
    time.Sleep(1*time.Second)
    echo <- "咖啡色的羊驼"
}

// 定义goroutine 2
func Receive() {
    temp := <- echo // 阻塞等待echo的通道的返回
    receive <- temp
}


func main() {
    echo = make(chan string)
    receive = make(chan string)

    go Echo()
    go Receive()

    getStr := <-receive   // 接收goroutine 2的返回

    fmt.Println(getStr)
}

在这里不一定要去关闭channel,因为底层的垃圾回收机制会根据它是否可以访问来决定是否自动回收它。(这里不是根据channel是否关闭来决定的)

3.单向通道类型

当程序则够复杂的时候,为了代码可读性更高,拆分成一个一个的小函数是需要的。

此时go提供了单向通道的类型,来实现函数之间channel的传递。

上代码:

package main

import (
    "fmt"
    "time"
)

// 定义goroutine 1
func Echo(out chan<- string) {   // 定义输出通道类型
    time.Sleep(1*time.Second)
    out <- "咖啡色的羊驼"
    close(out)
}

// 定义goroutine 2
func Receive(out chan<- string, in <-chan string) { // 定义输出通道类型和输入类型
    temp := <-in // 阻塞等待echo的通道的返回
    out <- temp
    close(out)
}


func main() {
    echo := make(chan string)
    receive := make(chan string)

    go Echo(echo)
    go Receive(receive, echo)

    getStr := <-receive   // 接收goroutine 2的返回

    fmt.Println(getStr)
}

程序输出:

咖啡色的羊驼
4.缓冲管道

goroutine的通道默认是是阻塞的,那么有什么办法可以缓解阻塞?
答案是:加一个缓冲区。

对于go来说创建一个缓冲通道很简单:

ch := make(chan string, 3) // 创建了缓冲区为3的通道

//=========
len(ch)   // 长度计算
cap(ch)   // 容量计算

6.goroutine死锁与友好退出

6.1 goroutine死锁

来一个死锁现场一:

package main

func main() {
    ch := make(chan int)
    <- ch // 阻塞main goroutine, 通道被锁
}

输出:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()

死锁现场2:

package main

func main() {
    cha, chb := make(chan int), make(chan int)

    go func() {
        cha <- 1 // cha通道的数据没有被其他goroutine读取走,堵塞当前goroutine
        chb <- 0
    }()

    <- chb // chb 等待数据的写
}

为什么会有死锁的产生?

非缓冲通道上如果发生了流入无流出,或者流出无流入,就会引起死锁。
或者这么说:goroutine的非缓冲通道里头一定要一进一出,成对出现才行。
上面例子属于:一:流出无流入;二:流入无流出

当然,有一个例外:

func main() {
    ch := make(chan int)
    go func() {
       ch <- 1
    }()
}

执行以上代码将会发现,竟然没有报错。
what?
不是说好的一进一出就死锁吗?
仔细研究会发现,其实根本没等goroutine执行完,main函数自己先跑完了,所以就没有数据流入主的goroutine,就不会被阻塞和报错

6.2 goroutine的死锁处理

有两种办法可以解决:

1.把没取走的取走便是

package main

func main() {
    cha, chb := make(chan int), make(chan int)

    go func() {
        cha <- 1 // cha通道的数据没有被其他goroutine读取走,堵塞当前goroutine
        chb <- 0
    }()

    <- cha // 取走便是
    <- chb // chb 等待数据的写
}

2.创建缓冲通道

package main

func main() {
    cha, chb := make(chan int, 3), make(chan int)

    go func() {
        cha <- 1 // cha通道的数据没有被其他goroutine读取走,堵塞当前goroutine
        chb <- 0
    }()

    <- chb // chb 等待数据的写
}

这样的话,cha可以缓存一个数据,cha就不会挂起当前的goroutine了。除非再放两个进去,塞满缓冲通道就会了。

7.select的简介

定义:在golang里头select的功能与epoll(nginx)/poll/select的功能类似,都是坚挺IO操作,当IO操作发生的时候,触发相应的动作。

select有几个重要的点要强调:

1.如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行

上代码:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)

    ch<-1
    select {
    case <-ch:
        fmt.Println("咖啡色的羊驼")
    case <-ch:
        fmt.Println("黄色的羊驼")
    }
}

输出:

(随机)二者其一

1
2.case后面必须是channel操作,否则报错。

上代码:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    ch<-1
    select {
    case <-ch:
        fmt.Println("咖啡色的羊驼")
    case 2:
        fmt.Println("黄色的羊驼")
    }
}

输出报错:

2 evaluated but not used
select case must be receive, send or assign recv
3.select中的default子句总是可运行的。所以没有default的select才会阻塞等待事件

上代码:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    // ch<-1   <= 注意这里备注了。
    select {
    case <-ch:
        fmt.Println("咖啡色的羊驼")
    default:
        fmt.Println("黄色的羊驼")
    }
}

输出:

黄色的羊驼
4.没有运行的case,那么江湖阻塞事件发生报错(死锁)
package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    // ch<-1   <= 注意这里备注了。
    select {
    case <-ch:
        fmt.Println("咖啡色的羊驼")
    }
}

输出报错:

fatal error: all goroutines are asleep - deadlock!

8.select的应用场景

1.timeout 机制(超时判断)
package main

import (
    "fmt"
    "time"
)

func main() {
    timeout := make (chan bool, 1)
    go func() {
        time.Sleep(1*time.Second) // 休眠1s,如果超过1s还没I操作则认为超时,通知select已经超时啦~
        timeout <- true
    }()
    ch := make (chan int)
    select {
    case <- ch:
    case <- timeout:
        fmt.Println("超时啦!")
    }
}

以上是入门版,通常代码中是这么写的:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make (chan int)
    select {
    case <-ch:
    case <-time.After(time.Second * 1): // 利用time来实现,After代表多少时间后执行输出东西
        fmt.Println("超时啦!")
    }
}
2.判断channel是否阻塞(或者说channel是否已经满了)
package main

import (
    "fmt"
)

func main() {
    ch := make (chan int, 1)  // 注意这里给的容量是1
    ch <- 1
    select {
    case ch <- 2:
    default:
        fmt.Println("通道channel已经满啦,塞不下东西了!")
    }
}
3.退出机制
package main

import (
    "fmt"
    "time"
)

func main() {
    i := 0
    ch := make(chan string, 0)
    defer func() {
        close(ch)
    }()

    go func() {
        DONE: 
        for {
            time.Sleep(1*time.Second)
            fmt.Println(time.Now().Unix())
            i++

            select {
            case m := <-ch:
                println(m)
                break DONE // 跳出 select 和 for 循环
            default:
            }
        }
    }()

    time.Sleep(time.Second * 4)
    ch<-"stop"
}

输出:

1532390471
1532390472
1532390473
stop
1532390474

这边要强调一点:退出循环一定要用break + 具体的标记,或者goto也可以。否则其实不是真的退出。

package main

import (
    "fmt"
    "time"
)

func main() {
    i := 0
    ch := make(chan string, 0)
    defer func() {
        close(ch)
    }()

    go func() {

        for {
            time.Sleep(1*time.Second)
            fmt.Println(time.Now().Unix())
            i++

            select {
            case m := <-ch:
                println(m)
                goto DONE // 跳出 select 和 for 循环
            default:
            }
        }
        DONE:
    }()

    time.Sleep(time.Second * 4)
    ch<-"stop"
}

输出:

1532390525
1532390526
1532390527
1532390528
stop
9.select死锁

select不注意也会发生死锁,前文有提到一个,这里分几种情况,重点再次强调:

1.如果没有数据需要发送,select中又存在接收通道数据的语句,那么将发送死锁

package main
func main() {  
    ch := make(chan string)
    select {
    case <-ch:
    }
}

预防的话加default。

空select,也会引起死锁

package main

func main() {  
    select {}
}

版权声明:本文为CSDN博主「咖啡色的羊驼」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u011957758/article/details/81159481

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

推荐阅读更多精彩内容