进程、线程、协程与Go中的协程

  • 进程(process)

    进程是指计算机中已执行的程序。程序本身只是指令、数据及其组织形式的描述,相当于一个名词,进程才是程序的真正执行实例。可以简单理解为进程就是运行中的程序

    进程是操作系统进行资源分配的最小单位。不同进程之间的资源是相互独立和隔离的,同一个进程内的资源是共享的,比如共享内存,所以多个线程在使用同一个内存的时候需要使用加锁等方法避免造成预料外的结果。

  • 线程(thread)

    线程是指将进程划分两个或多个线程,由单处理器(单线程)或者多处理器(多线程)或多核处理系统并发执行。一个进程下可包含一个或者多个线程。线程可以理解为子进程。进程有独立的内存单元,进程下的所有线程共享这些内存。

    线程是CPU调度的最小单位。CPU能调度的最细粒度就是线程了,更细的协程是由用户程序实现的。

  • 协程(coroutine)

    协程是计算机程序的一类组件,推广了协作式多任务的子例程,允许执行被挂起与恢复。协程是一种用户态轻量级线程,协程的调度完全由用户控制。一个线程下可以包含一个或多个协程

  • Go中的协程(goroutine)

    goroutine是由Go运行时管理的轻量级线程 ,与coroutine只能运行在一个线程上不同,goroutine可以运行在一个或多个线程上

从进程、线程、到协程,"粒度"越细,上下文之间切换的开销就越小。

除了进程、线程、协程、goroutine之外,还需要了解一下以下几个概念:

  1. 用户态和内核态

    • 用户态(User Mode),运行用户程序。

    • 内核态(Kernel Mode),运行操作系统程序,操作硬件。

​ 在内核态下代码可以执行任何CPU指令以及引用任何内存地址,用户程序不能直接进入内核态,需要通过系统调用。

  1. 并行和并发

    • 并行指的是多个事情在同一个时间点上同时发生了。

    • 并发指的是多个事情,在同一时间段内同时发生了。

  2. 同步和异步

    • 同步指的是程序执行的顺序是单一的,只能一步一步地顺序执行。

    • 异步就是指程序执行的顺序是不确定的,不需要等待某个步骤执行完才能执行下一步。

    在之前写的《IO多路复用》中,也提到过同步和异步的概念,那篇文章中的同步和异步指的是访问数据的机制,同步一般指主动请求并等待I/O操作完成的方式。异步指主动请求数据后便可以继续处理其他任务,随后等待I/O操作完毕的通知。在这篇文章中,同步和异步的意义有所不同,相对广义一些。

  3. 阻塞和非阻塞

    • 阻塞指的是进程调用接口后如果接口没有准备好数据,那么这个进程会被挂起什么也不能做,直到有数据返回时唤醒。

    • 非阻塞就是进程调用接口后如果接口没有准备好数据,进程也能处理后续的操作,但是需要不断地去轮询检查数据是否已经处理完成。

这些概念听起来真的很容易让人迷糊,比如异步,如果把异步理解为不必等待某个步骤执行完成才能执行下一步,那么coroutine也可以认为是实现异步并发的,但是coroutine只能在一个线程上执行,所以它的执行顺序一定是单一的,它也可以认为是同步的,为了避免混淆,本文着重"不必等待某个步骤执行完才能执行下一步"这一点,认为coroutine是异步的。

下面用一些例子让这些概念便于理解,为了简单清晰一些后面直接用process、thread、coroutine、和goroutine来表示进程、线程、协程和Go中的协程。注意文中的图片只是画的大致示意图,关于CPU详细的调度机制,我目前也不清楚,这些示意图的主要目的是演示进程、线程、协程和Go中的协程的并发或并行。

1核CPU多进程

以1核CPU、2processes为例。

在一个1核CPU的电脑上,运行着程序A和程序B,将这两个运行中的程序分别称为process A和process B。操作系统为process A和process B分配了资源,比如内存、CPU时间等。时间片指的是操作系统分配给进程的微观上的CPU时间,在一个1核CPU上,一段微小时间运行process A,一段微小的时间运行process B,看起来就像同时运行着程序A和程序B一样。

图里面不同颜色的时间片分别表示分配给process A和process B的CPU时间。

process A和process B 是并发的,因为它们在一个时间段内同时发生了,但不是并行的,因为某一个时间点一定只能执行一个process。

这里简单说一下I/O操作,I/O操作也就是输入/输出操作一般分为内存I/O,磁盘I/O和网络I/O,如下图所示,从CPU到远程存储,能存储的数据量是越来越大,但是数据读写的速度是越来越慢的,所以在CPU中处理任务时,一定不会阻塞着等I/O操作完成,否则CPU的速度就会降低到I/O的速度。

2核CPU多进程

以2核CPU、2processes为例。

在一个2核CPU的电脑上,运行着两个进程process A和process B。

图中不同颜色的时间片分别表示分配给process A和process B的CPU时间。

可以看到在同一个时间点,process A 和 process B 可以同时发生,所以process A 和 process B 是能并行的。

多线程

以2核CPU、2processes、5threads为例。

一般我们所说的thread都是用户态线程,是语言实现的用来并发处理一些问题的线程,这些线程和操作系统的内核线程(例如Linux的内核调度实体(Kernal Scheduling Entry, KSE))不同,thread不能直接操作内核相关的资源,必须通过KSE来操作。

有以下三种thread模型:

KSE : thread 为1 :1 的是 内核级线程模型,因为一个KSE对应一个thread,所以多线程的部分直接使用内核自己的多线程就行。但是threads之间切换时,也需要内核态的KSE的切换,开销较大。

KSE : thread 为 1 : M 的是用户级线程模型,多线程包括线程之间的切换等需要由用户态的程序自己实现,由于都在用户态中执行,并且用的是同一个进程的共享内存,切换开销较小。但是不能有效地利用多核的CPU,因为threads在一个KSE中,也就是只能使用CPU的其中1个核心。

KSE : thread 为N : M 的是两级线程模型,这样能有效地利用多核CPU,但是调度器的实现较大。

Go语言的调度模型使用的就是一种特殊的两级线程模型(GPM调度模型)。

为了简化场景,假设用的是1:1的内核级线程模型,看一下2核CPU下、2processes、5threads的运行情况:

在这个关于一个线程能否运行在多个CPU核心的问答中,有回答到"操作系统可以自由地在不同的CPU上提供一个线程的时间片,它可以在不同的时间片上轮换运行所有的CPU。但是操作系统不能在多个CPU上同时运行一个线程"。这句话的意思是,同一个线程是能运行在多个CPU核心上的,这完全取决于操作系统怎么做,下图这种同一个线程运行在多个CPU核心的情况是可能出现的:

但是在不同CPU核心上不可能同时运行同一个线程,下图这种情况不可能出现:

简单看一下线程间的切换,假设运行过程是这样的:

thread A2 执行的过程中遇到了I/O操作,就中断然后切换到thread A1运行,当I/O操作执行完毕并且thread A1运行完毕或者中断之后,就再继续运行thread A2。

threads之间的切换和processes之间的切换类似,但是切换的开销要小一些,比如因为threads共享了processes内存,所以不需要像processes切换那样进行内存的切换。

多协程

以2核CPU、2processes、5threads、6coroutines为例。

coroutine只能包含在一个线程中。因为只能在同一个线程中运行,所以这些coroutine的执行顺序一定是确定的。

把图中圈出的部分单拎出来:

以下面这段Python代码为例:

go 复制代码
def genNum(max):
  n = 0
  while n < max:
    yield n
    n += 1

def genStr(max):
  s = "a"
  while s < max:
    yield s
    s = chr(ord(s) + 1)


n = genNum(3)
s = genStr("e")

print(n.__next__())
print("其他1")
print("其他2")
print(s.__next__())
print(n.__next__())
print("其他3")
print(s.__next__())
print(s.__next__())

运行结果:

shell 复制代码
0
其他1
其他2
a
1
其他3
b
c

一个coroutine打印数字,一个coroutine打印字符,在两个coroutine之间,可以执行其他代码。

多goroutine

以2核CPU、2processes、5threads、200goroutines为例。

Go使用的是一种特殊的两级线程模型,GPM调度模型:

  • G,Goroutine,应用层开启的任务。
  • P,Processer,逻辑处理器,关联G和M。
  • M,Machine,Go语言运行时开启的线程,与内核线程一一对应。

如图所示,M和内核线程是一对一的关系,M和P是多对多的关系,每个P都有自己的goroutine队列,但是G和P之间的关系不是固定的,如果某个P的队列中的goroutine已经全部执行完了,它会从全局的goroutines队列中取goroutine执行,如果全局队列中的goroutine运行完了,它会从其他P那拿goroutine来运行。goroutines之间的切换不会走内核态,但是goroutine对应的P的M是可能变化的,所以goroutine和内核线程之间是多对多的关系

由于创建goroutine消耗的内存很小,并且切换时的消耗也小,所以可以创建成千上万的goroutine(当然资源是有限的,因此可以创建的goroutine的数量一定也是有限的)。

2核CPU、2processes、5threads、200goroutines示例如下:

线程下的coroutine只能存在同一个线程中运行,不可能一个coroutine在线程1,另一个coroutine在线程2。

但是goroutine不一定全都在一个线程中,可能一个goroutine在线程1,另一个goroutine在线程2,假如线程1和线程2是运行在不同的CPU Core的,那么两个goroutine是有可能并行的。

同一个goroutine也可能一会运行在这个线程,一会运行在另一个线程上,例如,正在运行的gorutine(称之为G1)去执行系统调用了,这期间P闲置了,于是调度系统把这个P绑定到了另一个M上,等系统调用返回的时候,发现P已经被另一个M占用了,于是M再去申请另一个P,但是没有申请到P,于是把G1放到了全局队列中,G1再次执行时,不一定是在原来的那个M上执行了,可能在另一个M上执行。

更多关于GPM调度模型的内容,可以查看《Go中的GPM调度模型》。 关于goroutine的使用,可以查看《Go中的goroutine和channel》。

参考地址&书籍

  1. 维基百科中的名词解释:进程线程协程
  2. 协程:www.liaoxuefeng.com/wiki/101695...
  3. 进程、线程、协程:blog.csdn.net/WuDan_1112/...
  4. 《深入Go语言------原理、关键技术与实战》by 历冰、朱荣鑫、黄迪璇
相关推荐
Estar.Lee3 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
2401_857610035 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
凌冰_5 小时前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis
码农飞飞5 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货5 小时前
Rust 的简介
开发语言·后端·rust
monkey_meng6 小时前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
Estar.Lee6 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
新知图书7 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放7 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang7 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net