golang:并发编程总结
阅读原文时间:2021年11月24日阅读:1

并发编程是指在一台处理器上“同时”处理多个任务。

宏观并发:在一段时间内,有多个程序在同时运行。

微观并发:在同一时刻只能有一条指令执行,但多个程序指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个程序快速交替的执行。

并行 parallel同一时刻,多条指令在多个处理器上同时执行。

并发 concurrency:在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,通过cpu时间片轮转使多个进程快速交替的执行。

通俗来讲,并行是两组队列同时使用一个进程;并发是两个队列分别交替使用两个进程

程序,以Go语言为例,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁….)

进程,是一个抽象的概念,与操作系统原理联系紧密。以Go语言为例,将编译好的程序运行起来,在内存空间中形成一个独立的内存体,内存体有自己的独立空间,上级挂靠单位是操作系统。

进程是操作系统进行资源分配和调度的一个独立单位,一般由程序,数据集合和进程控制块三部分组成。

  • 程序:描述进程完成的功能,是控制进程执行的指令集;
  • 数据集合:程序在执行时所需要的数据和工作区;
  • 程序控制块PCB:Program Control Block,包含进程的描述信息和控制信息,是进程存在的唯一标志。

进程是活跃的程序,占用系统资源。在内存中执行。同一个程序也可以加载为不同的进程(彼此之间互不影响)

进程状态

进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。

线程的任务调度

大部分操作系统的任务调度是采用时间片轮转的抢占式调度方式。

时间片轮转是指,在一个进程中,当线程任务执行几毫秒后,由操作系统内核进行调度,通过硬件计数器终端处理器,让线程强行暂停,并将该线程的寄存器放入内存中,通过查看线程列表决定接下来执行哪一个线程,并从内存中恢复该线程的寄存器,最后恢复该线程的执行,从而去执行下一个任务。

时间片轮转中,任务执行那段时间叫做时间片,任务正在执行时的状态叫运行状态,被暂停的线程任务状态叫做就绪状态,意为等待下一个属于它的时间片的到来。

由于CPU的执行效率非常高,(i5 6600 约200亿/秒,奔腾4 约13亿/秒)CPU preformance 时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。多任务运行过程的示意图如下:

进程实现并发时会出现的问题呢

孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。

僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。

在早期操作系统当中,没有线程的概念,进程是最小分配资源与执行单位,可以看做是一个进程中只有一个线程,故进程即线程。所以线程LWP被称为::Lightweight process,轻量级的进程,是程序执行中一个单一的顺序控制流程,在Linux操作系统下,线程的本质仍是进程。

线程有独立的PCB,但没有独立的地址空间,各个线程之间共享程序的内存空间。

  • 进程:最小分配资源单位,可看成是只有一个线程的进程。
  • 线程:最小的执行单位
  • 一个进程由一个或多个线程组成
  • 进程之间相互独立,同一进程下的各个线程之间共享程序的内存空间

协程 coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。

多数语言在语法层面并不直接支持协程,而是通过库的方式支持,但用库的方式支持的功能也并不完整,比如仅仅提供协程的创建、销毁与切换等能力。如果在这样的轻量级线程中调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行轻量级线程,从而无法真正达到轻量级线程本身期望达到的目标。

协程和线程的区别

  • 占用资源:线程,初始单位为1MB,固定不可变;协程初始一般为 2KB,可随需要而增大。
  • 调度:线程,由操作系统内核完成,协程,由用户完成。
  • 性能: 线程,占用资源高,频繁创建销毁带来性能问题。占用资源小,不会带来严重的性能问题。
  • 数据: 线程,多线程需要锁机制确保数据一致性和可见性;而线程因为只有一个进程,不存在同时读/写冲突,协程中控制共享数据不用加锁,顾执行效率较线程高。

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

Go语言为并发编程而内置的上层API基于顺序通信进程模型CSP(communicating sequential processes)。这就意味着显式锁都是可以避免的,因为Go通过相对安全的通道发送和接受数据以实现同步,这大大地简化了并发程序的编写。

Go语言中的并发程序主要使用两种手段来实现。goroutine和channel。

什么是goroutine

Go语言作者Rob Pike说, “Goroutine是一个与其他goroutines并发运行在同一地址空间的Go函数或方法。一个运行的程序由一个或更多个goroutine组成。它与线程、协程、进程等不同。它是一个goroutine*。

goroutine是Go并行设计的核心。goroutine说到底其实就是协程,它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。

M 操作系统的线程抽象,一个M直接关联了一个内核线程;代表着真正执行计算的资源。

P Processor,提供相关执行环境的上下文,处理用户级代码逻辑的处理器,P的数量由用户设置的GOMAXPROCS决定,但是不论GOMAXPROCS设置为多大,P的数量最大为256。

G Goroutine,G并非执行体,每个G需要绑定到P才能被调度执行。

在操作系统每一个线程都有一个固定大小的块来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。

在Go语言中,每一个goroutine是一个独立的执行单元,goroutine的栈采取了动态扩容方式, 初始时仅为2KB,随着任务执行按需增长,最大可达1GB(64最大1G,32位最大256M)

上图,图中P正在执行的Goroutine为蓝色的,处于待执行状态的Goroutine为灰色的,灰色的Goroutine形成了一个队列runqueues。

在这里,当一个P关联多个G时,就会处理G的执行顺序,就是并发,当一个P在执行一个协程工作时,其他的会在等待,当正在执行的协程遇到阻塞情况,例如IO操作等,go的处理器就会去执行其他的协程,因为对于类似IO的操作,处理器不知道你需要多久才能执行结束,所以他不回去等你执行完。

references

go语言并发编程

进程和线程

a

groutine之间的调度