Go 语言入门很简单:Go 并发概念

220 阅读9分钟

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战

前置知识

有人把Go语言比作 21 世纪的C语言,第一是因为Go语言设计简单,第二则是因为 21 世纪最重要的就是并发程序设计,而 Go 从语言层面就支持并发。同时实现了自动垃圾回收机制。 Go语言的并发机制运用起来非常简便,在启动并发的方式上直接添加了语言级的关键字就可以实现,和其他编程语言相比更加轻量。 下面来介绍几个概念:

进程/线程

进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。 线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。

并发/并行

在操作系统中,进程和线程的执行都具有并发性。

并发是指向一段时间内,多个任务可以共享系统资源,同时执行。

并行是指从某个时刻开始,多个任务同时执行。

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

协程/线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

线程:一个线程上可以跑多个协程,协程是轻量级的线程。

程序的顺序执行

如果在程序中,语句一条语句一条语句顺序排列的,如果系统中只有一个程序,那么程序执行时也是按照程序语句排列先后次序,一条一条地执行下去。这种方式就像工厂生产流水线加工方式那样,这种程序设计方式就叫做顺序程序设计。

在单任务、单处理机系统环境中,内存中只有一道程序作业在执行,一个程序完成后,下一个程序作业才能进入内存继续执行。

这种顺序执行的程序有三个特点:

  • 严格顺序执行。每条程序语句的执行都以前一条语句的结束为前提条件
  • 一个程序在计算机中运行时独占全部系统资源。只有程序本身的动作才能改变程序的运行环境。
  • 程序的执行结果与程序的运行速度无关。即处理机在执行程序任何两条语句之间的停顿,对程序的运算结果不发生影响。

这三个特点概括起来就是程序的封闭性可再现性

封闭性:程序一旦运行起来,其计算结果仅仅取决于程序本身,即运行结果唯一。

可再现性:指同一程序可反复执行,且每次执行结果相同。

程序的并发执行

程序的顺序执行限制系统内存中只有一道程序作业,这显然限制了系统性能的发挥,且资源利用率不高。

现代操作系统都支持程序的并发执行。

所谓并发执行是指在同一时间间隔内,多个程序可以“同时”执行。

在单处理机系统中,进程(或线程)通过时间片或者让出控制权来实现任务切换,以达到“同时”运行多个程序的目的。

这种方式叫做程序并发执行,但实际上任何时刻都只有一个任务被执行,其他任务则通过某个算法来排队准备执行。

即宏观上多个程序任务是“同时”执行,但微观上各任务还是一个一个地顺序执行。

程序的并发执行可以使得多个程序可以共享系统资源,提高系统资源利用率,还可以增加系统吞吐量。

同时,系统的并发执行和资源共享也使得系统环境变得非常复杂,不像顺序执行那么简单。

程序的并发执行基本是由操作系统提供的,Go 在语言层面就支持并发特性。

程序的并行执行

和并发执行不同,程序的并行执行是指同一时刻,多个程序可以同时执行。在多处理机系统中,可以让多个进程,或同一进程内的多个线程做到真正意义上的同时执行,它们之间不需要排队(这是在理想情况下,系统中进程(线程)的数量可能超过处理机的数量,这是依然需要排队)。在这种情况下,多个程序才能达到真正意义上的“同时”执行,即并行执行。

进程的概念

进程是在并发环境下,程序的一次动态执行过程。它由进程控制块(PCB)、程序和数据三部分组成,进程在它的生命周期内可能处于执行、就绪、阻塞三种基本状态。

在多任务操作系统中,多个进程可以并发执行,而且进程是系统资源分配的基本单位。系统中每个进程都有自己的内存映像区,且互不影响,所以管理简单,但缺点是系统开销大。所以,系统能同时创建的进程数量是有限的,不能太多。

线程的概念

由于进程的系统开销大,操作系统的设计者们又提出来更小的独立运行的单位——线程。

通过线程来提高系统内程序并发执行的程度,从而进一步提高系统的吞吐量。

在操作系统中,线程是由进程创建的,所以它继承了进程的部分资源,且具有进程的一些基本特征。所以多个线程之间也可以并发执行,且比进程的系统开销小。

但是,和进程一样,线程依然是由系统内核管理的,所以在高并发情况下,系统能创建的线程数量依然有限,效率也不高。

协程的概念

协程本质上是一种用户态线程,不需要操作系统进行抢占式调度,而且在真正的实现中寄存于线程中。因此,协程系统开销极小,可以有效提高线程任务的并发性,避免高并发模式下线程的缺点。

协程最大优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源衰竭,而系统最多能创建的进程、线程的数量却少得多。

使用协程的有点是编程简单,结果清晰。但缺点是需要语言的支持,如果语言不支持,则需要用户在程序中自行实现调度。

目前,原生支持协程的语言还很少。

goroutine

Go 语言在语言级别支持轻量级线程,叫做 goroutine,Go 语言标准库提供的所有系统调用操作(包括同步I/O操作),都会让出处理机给其他 Goroutine。这使得轻量级线程的切换管理不依赖于系统的进程和线程,也不依赖于 CPU 的核心数量。

并发指在同一时间内可以执行多个任务。并发编程含义比较广泛,包含多线程编程、多进程编程及分布式程序等。本章讲解的并发含义属于多线程编程。

img

Go 语言通过编译器运行时(runtime),从语言上支持了并发的特性。Go 语言的并发通过 goroutine 特性完成。goroutine 类似于线程,但是可以根据需要创建多个 goroutine 并发工作。goroutine 是由 Go 语言的运行时调度完成,而线程是由操作系统调度完成。 Go 语言还提供 channel 在多个 goroutine 间进行通信。goroutine 和 channel 是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。本章中,将详细为大家讲解 goroutine 和 channel 及相关特性。

goroutine 介绍


goroutine 是一种非常轻量级的实现,可在单个进程里执行成千上万的并发任务,它是Go语言并发设计的核心。 说到底 goroutine 其实就是线程,但是它比线程更小,十几个 goroutine 可能体现在底层就是五六个线程,而且Go语言内部也实现了 goroutine 之间的内存共享。 使用 go 关键字就可以创建 goroutine,将 go 声明放到一个需调用的函数之前,在相同地址空间调用运行这个函数,这样该函数执行时便会作为一个独立的并发线程,这种线程在Go语言中则被称为 goroutine。 goroutine 的用法如下:

 //go 关键字放在方法调用前新建一个 goroutine 并执行方法体
 go GetThingDone(param1, param2);
 ​
 ​
 //新建一个匿名方法并执行
 go func(param1, param2) {
 }(val1, val2)
 ​
 ​
 //直接新建一个 goroutine 并在 goroutine 中执行代码块
 go {
     //do someting...
 }

channel 介绍

channel 是Go语言在语言级别提供的 goroutine 间的通信方式。我们可以使用 channel 在两个或多个 goroutine 之间传递消息。

channel 是进程内的通信方式,因此通过 channel 传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。如果需要跨进程通信,我们建议用分布式系统的方法来解决,比如使用 Socket 或者 HTTP 等通信协议。

Go语言对于网络方面也有非常完善的支持。 channel 是类型相关的,也就是说,一个 channel 只能传递一种类型的值,这个类型需要在声明 channel 时指定。如果对 Unix 管道有所了解的话,就不难理解 channel,可以将其认为是一种类型安全的管道。

定义一个 channel 时,也需要定义发送到 channel 的值的类型,注意,必须使用 make 创建 channel,代码如下所示:

 ci := make(chan int)
 cs := make(chan string)
 cf := make(chan interface{})