我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Golang 协程 / 线程 / 进程 区别详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Golang 协程 / 线程 / 进程 区别详解

概念

进程 每个进程都有自己的独立内存空间,拥有自己独立的地址空间、独立的堆和栈,既不共享堆,亦不共享栈。一个程序至少有一个进程,一个进程至少有一个线程。进程切换只发生在内核态。

线程 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,是由操作系统调度,是操作系统调度(CPU调度)执行的最小单位。对于进程和线程,都是有内核进行调度,有 CPU 时间片的概念, 进行抢占式调度。内核由系统内核进行调度, 系统为了实现并发,会不断地切换线程执行, 由此会带来线程的上下文切换。

协程 协程线程一样共享堆,不共享栈,协程是由程序员在协程的代码中显示调度。协程(用户态线程)是对内核透明的, 也就是系统完全不知道有协程的存在, 完全由用户自己的程序进行调度。在栈大小分配方便,且每个协程占用的默认占用内存很小,只有 2kb ,而线程需要 8mb,相较于线程,因为协程是对内核透明的,所以栈空间大小可以按需增大减小。

并发 多线程程序在单核上运行
并行 多线程程序在多核上运行

协程与线程主要区别是它将不再被内核调度,而是交给了程序自己而线程是将自己交给内核调度,所以golang中就会有调度器的存在。

详解

进程

在计算机中,单个 CPU 架构下,每个 CPU 同时只能运行一个任务,也就是同时只能执行一个计算。如果一个进程跑着,就把唯一一个 CPU 给完全占住,显然是不合理的。而且很大概率上,CPU 被阻塞了,不是因为计算量大,而是因为网络阻塞。如果此时 CPU 一直被阻塞着,其他进程无法使用,那么计算机资源就是浪费了。
这就出现了多进程调用了。多进程就是指计算机系统可以同时执行多个进程,从一个进程到另外一个进程的转换是由操作系统内核管理的,一般是同时运行多个软件。

线程

有了多进程,为什么还要线程?原因如下:

  • 进程间的信息难以共享数据,父子进程并未共享内存,需要通过进程间通信(IPC),在进程间进行信息交换,性能开销较大。

  • 创建进程(一般是调用 fork 方法)的性能开销较大。

在一个进程内,可以设置多个执行单元,这个执行单元都运行在进程的上下文中,共享着同样的代码和全局数据,由于是在全局共享的,就不存在像进程间信息交换的性能损耗,所以性能和效率就更高了。这个运行在进程中的执行单元就是线程。

协程

官方的解释:链接:goroutines说明

Goroutines 是使并发易于使用的一部分。 这个想法已经存在了一段时间,就是将独立执行的函数(协程)多路复用到一组线程上。 当协程阻塞时,例如通过调用阻塞系统调用,运行时会自动将同一操作系统线程上的其他协程移动到不同的可运行线程,这样它们就不会被阻塞。 程序员看不到这些,这就是重点。 我们称之为 goroutines 的结果可能非常便宜:除了堆栈的内存之外,它们的开销很小,只有几千字节。

为了使堆栈变小,Go 的运行时使用可调整大小的有界堆栈。 一个新创建的 goroutine 被赋予几千字节,这几乎总是足够的。 如果不是,运行时会自动增加(和缩小)用于存储堆栈的内存,从而允许许多 goroutine 存在于适度的内存中。 每个函数调用的 CPU 开销平均约为三个廉价指令。 在同一个地址空间中创建数十万个 goroutine 是很实际的。 如果 goroutines 只是线程,系统资源会以更少的数量耗尽。

从官方的解释中可以看到,协程是通过多路复用到一组线程上,所以本质上,协程就是轻量级的线程。但是必须要区分的一点是,协程是用用户态的,进程跟线程都是内核态,这点非常重要,这也是协程为什么高效的原因。

协程的优势如下:

  • 节省 CPU:避免系统内核级的线程频繁切换,造成的 CPU 资源浪费。协程是用户态的线程,用户可以自行控制协程的创建于销毁,极大程度避免了系统级线程上下文切换造成的资源浪费。

  • 节约内存:在 64 位的 Linux 中,一个线程需要分配 8MB 栈内存和 64MB 堆内存,系统内存的制约导致我们无法开启更多线程实现高并发。而在协程编程模式下,只需要几千字节(执行Go协程只需要极少的栈内存,大概4~5KB,默认情况下,线程栈的大小为1MB)可以轻松有十几万协程,这是线程无法比拟的。

  • 开发效率:使用协程在开发程序之中,可以很方便的将一些耗时的 IO 操作异步化,例如写文件、耗时 IO 请求等。并且它们并不是被操作系统所调度执行,而是程序员手动可以进行调度的。

  • 高效率:协程之间的切换发生在用户态,在用户态没有时钟中断,系统调用等机制,因此效率高。

Golang GMP 调度器

注: 以下相关知识摘自刘丹冰(AceLd)的博文:[Golang三关-典藏版] Golang 调度器 GMP 原理与调度全分析

简介

  • G 表示:goroutine,即 Go 协程,每个 go 关键字都会创建一个协程。

  • M 表示:thread 内核级线程,所有的 G 都要放在 M 上才能运行。

  • P 表示:processor 处理器,调度 GM 上,其维护了一个队列,存储了所有需要它来调度的G

Goroutine 调度器 POS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行,

线程和协程的映射关系

在上面的 Golang 官方关于协程的解释中提到:

将独立执行的函数(协程)多路复用到一组线程上。 当协程阻塞时,例如通过调用阻塞系统调用,运行时会自动将同一操作系统线程上的其他协程移动到不同的可运行线程,这样它们就不会被阻塞。

也就是说,协程的执行是需要通过线程来先实现的。下图表示的映射关系:

在协程和线程的映射关系中,有以下三种:

  • N:1 关系

  • 1:1 关系

  • M:N 关系

N:1 关系

N 个协程绑定 1 个线程,优点就是协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速。但也有很大的缺点,1 个进程的所有协程都绑定在 1 个线程上。

缺点:

  • 某个程序用不了硬件的多核加速能力

  • 一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了。

1:1 关系

1 个协程绑定 1 个线程,这种最容易实现。协程的调度都由 CPU 完成了,不存在 N:1 缺点。

缺点:

  • 协程的创建、删除和切换的代价都由 CPU 完成,有点略显昂贵了。

M:N 关系

M 个协程绑定 1 个线程,是 N:11:1 类型的结合,克服了以上 2 种模型的缺点,但实现起来最为复杂。
协程跟线程是有区别的,线程由 CPU 调度是抢占式的,协程由用户态调度是协作式的,一个协程让出 CPU 后,才执行下一个协程。

调度器实现原理

注:Go 目前使用的调度器是 2012 年重新设计的。

2012 之前的调度原理,如下图所示:

M 想要执行、放回 G 都必须访问全局 G 队列,并且 M 有多个,即多线程访问同一资源需要加锁进行保证互斥 / 同步,所以全局 G 队列是有互斥锁进行保护的。

缺点:

  • 创建、销毁、调度 G 都需要每个 M 获取锁,这就形成了激烈的锁竞争。

  • M 转移 G 会造成延迟和额外的系统负载。比如当 G 中包含创建新协程的时候,M 创建了 G,为了继续执行 G,需要把 G 交给 M 执行,也造成了很差的局部性,因为 GG 是相关的,最好放在 M 上执行,而不是其他 M

  • 系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销。

2012 年之后的调度器实现原理,如下图所示:

在新调度器中,除了 M (thread)G (goroutine),又引进了 P (Processor)Processor,它包含了运行 goroutine 的资源,如果线程想运行 goroutine,必须先获取 PP 中还包含了可运行的 G 队列。

Go 中,线程是运行 goroutine 的实体,调度器的功能是把可运行的 goroutine 分配到工作线程上。调度过程如下:

  1. 全局队列(Global Queue):存放等待运行的 G

  2. P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G 时,G 优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。

  3. P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。

  4. M:线程想运行任务就得获取 P,从 P 的本地队列获取 GP 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 GG 执行之后,M 会从 P 获取下一个 G,不断重复下去。

Goroutine 调度器和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行。

调度器设计策略

复用线程: 避免频繁的创建、销毁线程,而是对线程的复用。

  1. work stealing 机制

当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。

  1. hand off 机制

当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。

  • 利用并行:GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。

  • 抢占:在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程,在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这就是 goroutine 不同于 coroutine 的一个地方。

  • 全局 G 队列:在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G

go func () 调度流程

流程如下:

  1. 通过 go func () 来创建一个 goroutine

  2. 有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中;

  3. G 只能运行在 M 中,一个 M 必须持有一个 PMP1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会向其他的 MP 组合偷取一个可执行的 G 来执行;

  4. 一个 M 调度 G 执行的过程是一个循环机制;

  5. M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞,如果当前有一些 G 在执行,runtime 会把这个线程 MP 中摘除 (detach),然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P

  6. M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。

调度器的生命周期

特殊的 M0G0

M0

M0 是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在 heap 上分配,M0 负责执行初始化操作和启动第一个 G, 在之后 M0 就和其他的 M 一样了。

G0

G0 是每次启动一个 M 都会第一个创建的 goroutineG0 仅用于负责调度的 GG0 不指向任何可执行的函数,每个 M 都会有一个自己的 G0。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0M0G0

我们来跟踪一段代码:

package main

import "fmt"

func main() {
    fmt.Println("Hello world")
}

接下来我们来针对上面的代码对调度器里面的结构做一个分析,也会经历如上图所示的过程:

  1. runtime 创建最初的线程 m0goroutine g0,并把 2 者关联。

  2. 调度器初始化:初始化 m0、栈、垃圾回收,以及创建和初始化由 GOMAXPROCSP 构成的 P 列表。

  3. 示例代码中的 main 函数是 main.mainruntime 中也有 1main 函数 ——runtime.main,代码经过编译后,runtime.main 会调用 main.main,程序启动时会为 runtime.main 创建 goroutine,称它为 main goroutine 吧,然后把 main goroutine 加入到 P 的本地队列。

  4. 启动 m0m0 已经绑定了 P,会从 P 的本地队列获取 G,获取到 main goroutine

  5. G 拥有栈,M 根据 G 中的栈信息和调度信息设置运行环境。

  6. M 运行 G

  7. G 退出,再次回到 M 获取可运行的 G,这样重复下去,直到 main.main 退出,runtime.main 执行 DeferPanic 处理,或调用 runtime.exit 退出程序。

调度器的生命周期几乎占满了一个 Go 程序的一生,runtime.maingoroutine 执行之前都是为调度器做准备工作,runtime.maingoroutine 运行,才是调度器的真正开始,直到 runtime.main 结束而结束。


免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Golang 协程 / 线程 / 进程 区别详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Golang 协程 / 线程 / 进程 区别详解

概念进程 每个进程都有自己的独立内存空间,拥有自己独立的地址空间、独立的堆和栈,既不共享堆,亦不共享栈。一个程序至少有一
2023-06-08

golang中线程和协程有哪些区别

这篇文章主要介绍golang中线程和协程有哪些区别,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!区别:线程中数据存储在内核态的内存空间;而协程中数据存储在线程提供的用户态内存空间。线程的任务调度由内核实现,抢占方式,
2023-06-14

python线程、进程和协程详解

引言解释器环境:python3.5.1我们都知道python网络编程的两大必学模块socket和socketserver,其中的socketserver是一个支持IO多路复用和多线程、多进程的模块。一般我们在socketserver服务端代
2022-06-04

golang中协程与线程的区别是什么

golang中协程与线程的区别有”调度器“、”内存和性能“、”锁和同步“和”异常处理“四点:1、协程则是由 Go 语言运行时调度的,而线程是由操作系统内核调度的;2、协程在相同的堆栈空间内运行,而线程都需要独立的堆栈空间和上下文切换的开销;
golang中协程与线程的区别是什么
2023-12-12

Golang中线程与协程的区别及应用

Golang中线程与协程的区别及应用Golang是一种开发效率高、并发性能强大的编程语言,其中线程(goroutine)和协程(thread)是其并发编程的关键概念。在Golang中,goroutine是一种轻量级线程,由Go语言运行时管
Golang中线程与协程的区别及应用
2024-02-29

Python中进程和线程的区别详解

Num01?>线程 线程是操作系统中能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一个线程指的是进程中一个单一顺序的控制流。 一个进程中可以并发多条线程,每条线程并行执行不同的任务。 Num02?>进程 进程就是
2022-06-04

golang线程池和协程池有什么区别

Golang中没有线程池的概念,而是通过协程(goroutine)来实现并发。协程是一种轻量级的线程,由Go语言的运行时环境(runtime)进行调度。在Golang中,通过关键字go来启动一个协程,可以同时执行多个协程,实现并发执行。协
2023-10-26

Python中多线程、多进程、协程的区别是什么

今天就跟大家聊聊有关Python中多线程、多进程、协程的区别是什么,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。首先我们写一个简化的爬虫,对各个功能细分,有意识进行函数式编程。下面代
2023-06-16

协程和线程的区别和联系

本篇内容介绍了“协程和线程的区别和联系”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!进程进程是什么进程是操作系统对一个正在运行的程序的一种抽
2023-06-15

go协程和线程有什么区别

Go协程和线程是两种并发执行的机制,它们有以下几个主要区别:1. 调度器:Go协程由Go语言的运行时调度器(Goroutine Scheduler)调度,而线程由操作系统的调度器(Thread Scheduler)调度。Go调度器使用了类似
2023-10-20

Python进程/线程/协程

第1章 操作系统历史1.1为什么要有操作系统?程序员无法把所有的硬件操作细节全部了解到,管理这些硬件并且加以优化使用时非常繁琐的工作,这个繁琐的工作就是由操作系统来干的,有了它,程序员就从这些繁琐的工作中解脱了出来,只需要考虑自己的应用软件
2023-01-31

python协程与golang协程的区

进程、线程和协程进程的定义:进程,是计算机中已运行程序的实体。程序本身只是指令、数据及其组织形式的描述,进程才是程序的真正运行实例。线程的定义:操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。进程和线程的关系
2023-01-31

Linux进程与线程的区别

这篇文章主要介绍“Linux进程与线程的区别”,在日常操作中,相信很多人在Linux进程与线程的区别问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Linux进程与线程的区别”的疑惑有所帮助!接下来,请跟着小编
2023-06-06

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录