九、Golang并发和线程模型

网友投稿 668 2022-05-30

@Author:Runsen

开始前来介绍几个概念:

进程:进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

线程:线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

并发:多线程程序在单核心的 cpu 上运行,称为并发

并行:多线程程序在多核心的 cpu 上运行,称为并行。

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,一个线程上可以跑多个协程,协程是轻量级的线程。

并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。

线程主要分为用户线程和内核线程,用户线程由各语言代码所支持,而内核线程是由操作系统内核所支持。多线程模型主要就是用户线程与内核线程的连接方式:

下面我们来探讨Go的线程模型,首先我们先来回顾下常见的三种线程模型,然后在介绍Go中独特的线程模型CSP。

文章目录

三种线程模型

Goroutine

CSP

三种线程模型

线程模型主要有三种:1、内核级别线程;2、用户级别线程;3、混合线程,分别对应的是1:1、N:1、M:N。

内核级别线程:一对一模型(1 : 1):每个用户级线程映射到一个内核级线程。优点是缓存读写快速,缺点是容易阻塞。

用户级别线程: 多对一模型(M : 1):多个用户级线程映射到一个内核级线程,线程管理在用户空间完成。

混合线程:多对多模型(M : N):内核线程和用户线程的数量比为 M : N,综合了前两种的优点

Goroutine

goroutine 是 Go 语言并行设计的核心,有人称之为 go 程。goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。在Java中,goroutine就是Thead 。

goroutine 语法格式:go 函数名( 参数列表 )。例如:go f(x, y, z),开启一个新的 goroutine:f(x, y, z)。

并发编程中,我们通常想将一个过程切分成几块,然后让每个 goroutine 各自负责一块工作,当一个程序启动时,主函数在一个单独的 goroutine 中运行,我们叫它 `main goroutine。新的 goroutine 会用 go 语句来创建。而 go 语言的并发设计,让我们很轻松就可以达成这一目的。

我们来看一个示例。

package main import ( "fmt" "time" ) func newTask() { i := 0 for { i++ fmt.Printf("new goroutine: i = %d\n", i) time.Sleep(1*time.Second) //延时1s } } func main() { //创建一个 goroutine,启动另外一个任务 go newTask() i := 0 //main goroutine 循环打印 不会暂停 for { i++ fmt.Printf("main goroutine: i = %d\n", i) time.Sleep(1 * time.Second) //延时1s } }

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

程序运行结果:

如果主 goroutine 退出后,那么其它的工作 goroutine 也会自动退出:

package main import ( "fmt" "time" ) func newTask() { i := 0 for { i++ fmt.Printf("new goroutine: i = %d\n", i) time.Sleep(1 * time.Second) //延时1s } } func main() { //创建一个 goroutine,启动另外一个任务 go newTask() fmt.Println("main goroutine exit") }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

程序运行结果如下,不会一直执行。

new goroutine: i = 1 main goroutine exit

1

2

上面就是Go实现了并发比较常见的Goroutine方法。

CSP

我们常见的多线程模型一般是通过共享内存实现的(就是Goroutine的原理),但是共享内存就会有很多问题。比如资源抢占的问题、一致性问题等等。为了解决这些问题,我们需要引入多线程锁、原子操作等等限制来保证程序执行结果的正确性。

这就引出了另外一种是Go语言特有的并发形式,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。

Go的CSP并发模型,是通过goroutine和channel来实现的。

CSP模式中,消息是通过Channel来通讯的,Channel相当于一个消息通讯的中间人,这样可以让两个通讯实体的耦合更松一些

下面Runsen先介绍下Channel,回顾一下基础知识。通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <-用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

ch <- v // 把 v 发送到通道 ch v := <-ch // 从 ch 接收数据 // 并把值赋给 v

1

2

3

声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:

ch := make(chan int)

1

channel分为两种,有缓冲channel和无缓冲channel,默认情况下,通道是不带缓冲区的。我们通过下边的代码例子来区分不同的channel种类。

package main import ( "fmt" ) func main() { pipline := make(chan string) //构造无缓冲通道 pipline <- "hello world" //发送数据 fmt.Println(<-pipline) //读数据 }

1

2

3

4

5

6

7

8

9

10

11

运行会抛出错误,如下:

fatal error: all goroutines are asleep - deadlock!

1

如果把这个例子改成有缓冲通道还会阻塞吗?我们继续看下边的例子:

package main import ( "fmt" ) func main() { pipline := make(chan string, 1 ) //构造无缓冲通道 pipline <- "hello world" //发送数据 fmt.Println(<-pipline) //读数据 } hello world

1

2

3

4

5

6

7

8

9

10

11

12

13

这里运行正常,此时就说明了缓冲和没有缓冲的区别在于在发送操作是否发生在有接受者时。那么,对于有缓冲通道会发生什么特殊情况呢?

如果这段代码,通道容量为 1,但是往通道中写入两条数据,对于一个协程来说就会造成死锁。

package main import ( "fmt" ) func main() { ch1 := make(chan string, 1) ch1 <- "hello world" ch1 <- "hello China" fmt.Println(<-ch1) } //fatal error: all goroutines are asleep - deadlock!

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

每个缓冲通道,都有容量,当通道里的数据量等于通道的容量后,此时再往通道里发送数据,就失造成阻塞,必须等到有人从通道中消费数据后,程序才会往下进行。

下面,我们看goroutine和channel实现CSP并发模型,代码来自菜鸟教程。

package main import "fmt" // 计算数字之和 func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // 把 sum 发送到通道 c } func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) //-9+4+0 go sum(s[len(s)/2:], c) // 7+2+8 x, y := <-c, <-c // 从通道 c 中接收 fmt.Println(x, y, x+y) //-5 17 12\ }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

九、Golang并发和线程模型

19

20

21

22

上面示例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和。这里的channel是没有缓冲。

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小。

package main import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // 把 sum 发送到通道 c } func main() { // 这里我们定义了一个可以存储整数类型的带缓冲通道 // 缓冲区大小为2,因为存储了一个数字,所以没有报错 s := []int{7, 2, 8, -9, 4, 0} c := make(chan int ,2) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c // 从通道 c 中接收 fmt.Println(x, y, x+y) // -5 17 12 }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

Go 任务调度

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:《数字化转型之路》 —1 新时代,数字经济与数字化转型
下一篇:Tomcat - 都说Tomcat违背了双亲委派机制,到底对不对?
相关文章