一看就懂系列之Golang的goroutine和通道

https://blog.csdn.net/u011957758/article/details/81159481

前言

如果说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('我后面才输出来') }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

以上执行后会输出:

Hello world goroutine
我后面才输出来1212

需要注意的是,执行速度很快,一定要加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') }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

函数输出:

Hello world goroutine
1
2
3
4
5
main function12345671234567

有心的同学可能会发现,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

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

有没注意到关键字'阻塞'?,这个其实是默认的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
}12345678910111213141516171234567891011121314151617

输出:

Hello world goroutine

  • 1

  • 1

由于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)
}123456789101112131415161718192021222324252627282930313233123456789101112131415161718192021222324252627282930313233

在这里不一定要去关闭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) }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

程序输出:

咖啡色的羊驼11

4.缓冲管道

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

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

ch := make(chan string, 3) // 创建了缓冲区为3的通道 //========= len(ch) // 长度计算 cap(ch) // 容量计算

  • 1

  • 2

  • 3

  • 4

  • 5

  • 1

  • 2

  • 3

  • 4

  • 5

6.goroutine死锁与友好退出

6.1goroutine死锁

来一个死锁现场一:

package main

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

输出:

fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main()

  • 1

  • 2

  • 3

  • 4

  • 1

  • 2

  • 3

  • 4

死锁现场2:

package main

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

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

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

为什么会有死锁的产生?

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

当然,有一个例外:

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

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

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

6.2goroutine的死锁处理

有两种办法可以解决:

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 等待数据的写
}1234567891011121312345678910111213

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 等待数据的写 }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

这样的话,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('黄色的羊驼')
}
}123456789101112131415123456789101112131415

输出:

(随机)二者其一

  • 1

  • 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('黄色的羊驼')
}
}12345678910111213141234567891011121314

输出报错:

2 evaluated but not used select case must be receive, send or assign recv

  • 1

  • 2

  • 1

  • 2

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('黄色的羊驼')
}
}12345678910111213141234567891011121314

输出:

黄色的羊驼

  • 1

  • 1

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

package main

import 'fmt'

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

输出报错:

fatal error: all goroutines are asleep - deadlock!

  • 1

  • 1

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('超时啦!')
}
}12345678910111213141516171819201234567891011121314151617181920

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

package main import ( 'fmt' 'time' ) func main() { ch := make (chan int) select { case <-ch: case <-time.After(time.Second * 1): // 利用time来实现,After代表多少时间后执行输出东西 fmt.Println('超时啦!') } }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

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已经满啦,塞不下东西了!')
}
}123456789101112131415123456789101112131415

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' }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

输出:

1532390471
1532390472
1532390473
stop
15323904741234512345

这边要强调一点:退出循环一定要用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' }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

  • 34

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

  • 34

输出:

1532390525
1532390526
1532390527
1532390528
stop1234512345

9.select死锁

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

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

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

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

预防的话加default。

空select,也会引起死锁

package main

func main() {
    select {}
}1234512345
(0)

相关推荐

  • 回答我,停止 Goroutine 有几种方法?

    大家好,我是煎鱼. 协程(goroutine)作为 Go 语言的扛把子,经常在各种 Go 工程项目中频繁露面,甚至有人会为了用 goroutine 而强行用他. 在 Go 工程师的面试中,也绕不开他, ...

  • 学习channel设计:从入门到放弃

    前言 哈喽,大家好,我是asong.终于回归了,停更了两周了,这两周一直在搞留言号的事,经过漫长的等待,终于搞定了.兄弟们,以后就可以在留言区尽情开喷了,只要你敢喷,我就敢精选

  • 信道:如何通过信道完成Go程(goroutine)同步?

    中文译为信道,英文是Channel,发音为[ˈtʃænl]),在Go语言中简写为chan.chan是Go语言的基本数据类型之一,也是Go语言中为难不多三个使用make关键字进行初始化的三个类型(信道. ...

  • 手把手教姐姐写消息队列

    前言 这周姐姐入职了新公司,老板想探探他的底,看了一眼他的简历,呦呵,精通kafka,这小姑娘有两下子,既然这样,那你写一个消息队列吧.因为要用go语言写,这可给姐姐愁坏了.赶紧来求助我,我这么坚贞不 ...

  • Go并发处理

    写了一个web接口,想高并发的请求这个接口,进行压力测试,所以服务端就实现了一个线程池. 代码从网上理解了之后写的.代码实例 简单的介绍: 首先实现一个Job接口,只要有方法实现了Do方法即可 定义个 ...

  • 22 Go常见的并发模式和并发模型

    一 Go并发模型 传统的编程语言C++ Java Python等,他们的并发逻辑多事基于操作系统的线程.并发执行单元(线程)之间的通信利用的就是操作系统提供的线程或进程间通信的原语.如:共享内存.信号 ...

  • 让手机秒变显微镜、望远镜!人人都能看得懂系列

    原来手机 既能变成显微镜 还能成为智能望远镜? 你信吗? 小科本来是不信的华中科技大学 "原创光学成像"项目团队 正是借此斩获 "互联网+"大赛 湖北省赛高教主 ...

  • 一看就懂的手诊(手诊真东西系列)

    手一伸出来,整个人体结构,五脏六腑都按比例缩放投影到手上,本着上应上,下应下,里应里,外应外的原则,那么今天明月就用最简单的语言,用最清晰的图片,让你一次记住,一次学会!让手诊变的如此简单,人人可学! ...

  • 16G101系列钢筋平法图集,内含全彩高清三维详图,一看就懂

    施工图比较抽象难懂,内含的许多设计规范专业程度较高,对初学者.刚入行的建筑从业人士来说,确实存在一定的难度. 这份2021最新版钢筋平法三维速查图集,主要从柱.墙.梁.板四个方面进行详细的解析.采用三 ...

  • 看完就懂系列之正则表达式(值得收藏)

    正则表达式是很多程序员,甚至是一些有了多年经验的开发者薄弱的一项技能.大家都很多时候都会觉得正则表达式难记.难学.难用,但不可否认的是正则表达式是一项很重要的技能,所有我将学习和使用正则表达式时的关键 ...

  • 《液压机构》日本经典技能系列丛书,图文并茂一看就懂

    液压和电一样都是看不见的, 所以很难理解,和电线 一样 也用管路连接.通过本书可以学习由液压驱动的机械原理.管路的作用.常用的液压回路等液压知识,了解现实中应用的液压装置. 预览 目录: 液压的原理 ...

  • 一看就懂的8个Excel表格操作小技巧,远离加班

    [温馨提示]亲爱的朋友,阅读之前请您点击[关注],您的支持将是我最大的动力! 掌握一些Excel操作,对于职场中的小伙伴,可以把工作效率再提高一丢丢. 01.把所有列缩放到一页宽 表格制作好了,打印预 ...

  • 初学站桩十七个标准,一看就懂

    很多人都想学站桩,但苦于不知道如何调整站桩的姿势.接下来说说具体的姿势要求: 一.头颈:头颈要做到"头顶虚悬".就是说头部百会穴好像有根绳子把头部轻轻吊起悬在空中一样,颈部是虚空的 ...

  • 聪明人看得懂 精明人看得准 高明人看得远

    分类: 人生箴言 拥有远见比拥有资产重要,拥有能力比拥有知识重要,拥有人才比拥有机器重要,拥有健康比拥有金钱重要! 思路决定出路,观念决定方向,性格决定命运,生活方式决定健康! 表面上缺的是金钱,本质 ...