Go语言并发编程(1):对多进程、多线程、协程和并发、并行的理解
一、进程和线程
对操作系统进程和线程以及协程的了解,可以看看我前面的文章:
对进程、线程和协程的理解以及它们的区别:https://www.cnblogs.com/jiujuan/p/16193142.html。
这篇文章我用了多张图片,尽可能清楚表达对它们的理解和区别。
还有一篇 Linux 进程,源码分析,Linux进程: task_struct结构体成员:https://www.cnblogs.com/jiujuan/p/11715853.html
1.1 多进程和多线程
如果是单个进程,单个线程,那么就不涉及到并发。
并发都是涉及到多个进程、多个线程。
为了加快任务的处理,我们可以把一个任务分解成多个小任务进行处理。这就是多任务。
在计算机中,我们可以用多进程或多线程来处理多个任务,这样完成任务的效率就会提高,因为 CPU 运算速度非常快。
比如有一个任务,切菜炒菜后洗衣服:
把这个任务分为 3 个任务,1、切菜 2、炒菜 3、洗衣服,一个进程完成这个任务就按照 1,2,3 这样的次序完成。如下图:

如果是多进程,比如有 2 个进程,那么可以把上面的一个大任务分为 2 个小任务,第一个小任务 1 和 2,第二个小任务 3,由 2 个进程完成,如下图:

这就是并发执行。
为什么需要并发执行,要压榨 CPU 让 CPU 发挥最大运算效率,执行更多任务。
在计算机硬件组成中,CPU 的运算速度远远大于其它硬件设备的速度。

1.2 关系协调(进程间通信)
如果只有一个进程,就不需要沟通协调。
如果是多个进程,彼此之间有联系,那么就涉及到进程之间的沟通和协调。涉及沟通,就需要进程间进行"通话"了,这叫进程间通信。
进程间通信一般有 3 方面内容:
- 一个进程如何向另外一个进程传递信息。
- 多个进程操作共享数据时相互不会产生影响。
- 存在依赖关系时确定适当的顺序。
来自:《操作系统设计与实现》
不只多进程,多线程,还有多协程都会有上面 3 方面的内容。
二、协程
在计算机系统中,有一个层次关系,硬件位于最下面,操作系统控制硬件,应用程序运行在操作系统之上,更进一步理解,应用程序是运行在操作系统的用户空间中。
简图如下:

用户创建的多线程/多进程都是运行在用户空间,但调度它们运行是操作系统。
协程也是运行在用户空间,但调度协程运行的是各种语言自己的 runtime,比如 Go 语言的 goroutine 运行在 Go runtime 中。
也就是说在操作系统和调度协程运行之间增加了一个“中间层” - 语言自己的调度系统,比如 Go 语言的 GMP 调度模型,Go runtime 与操作系统线程挂钩进行调度处理,而不是协程直接被操作系统调度处理。
(在计算机中,没有什么是增加一层解决不了的)
Go 中 GMP 和操作系统线程的关系,可以看我这篇文章:https://www.cnblogs.com/jiujuan/p/16193142.html#2888921075 ,里面有一张图画出了它们之间的关系。

三、并发和并行
并发 concurrency 和并行 parallelism。
很多人容易把这 2 者搞混了,认为它们没有区别,其实是有区别。从 CPU 核心个数来理解,就比较容易懂了。
并发
如果 CPU 只有一个核心,那么它就只能并发执行任务,同一个时间只能执行一个任务。如下图:

并行
如果 CPU 有多个核心,那么它就可以并行的执行多个任务,可以在同一时间执行多个任务。
比如 CPU 有 2 个核心,它可以在同一时间同时执行 2 个任务:

而现代计算机往往有多个 CPU 核心数,Go 语言可以轻松利用多个核心执行程序。而多数编程语言需要写线程同步代码利用多个核,这样容易导致错误。Go 具体是用什么来执行?就是 goroutine。
四、进程/线程/协程间通信相关概念
通信的问题
多个进程线程需要协调,彼此之间就需要通信。那进程间通信一般会有什么问题?在上面第 1.2 小节已经有讲。
第一个问题
一个进程把信息传递给另外一个进程
第二个问题
两个或多个进程同时操作某一数据时,怎么保证多个进程间彼此不影响。
比如在 12306 购票,你和其他用户抢最后一张票,系统用两个进程执行抢票操作,怎么保证 12306 只卖出最后一张票而不是两张票?
第三个问题
与顺序相关。最简单的例子就是打印机,A 进程生产数据,B 进程打印数据,B 打印前必须等待 A 产生一些数据。
竞争条件和临界区
竞争条件 - 两个或多个进程共享读写数据,而最后的结果取决于进程运行的精确时序,称为竞争条件(race condition)。
怎么避免竞争条件?
凡涉及到共享内存、共享文件及共享任何资源都可能引起这种错误,要避免这种错误,关键是要找出某种途径来阻止多个进程同时读写共享的数据。换言之,我们需要互斥,即用某种手段确保当一个进程使用一个共享变量或文件时,其他进程不能做同样的操作。
-- 《现代操作系统》。
我们把共享内存进行访问的程序片段称为临界区或临界区域。如果我们能使两个进程不可能同时处于临界区中,就能避免竞争条件。
忙等待互斥
书中提到了忙等待互斥的几个方法:Peterson 解法和 TSL 和 XCHG,这些解法本质上:当一个进程进入临界区,先检查是否允许进入,若不允许,则该进程将原地等待,直到允许位置。
缺点:浪费 CPU,还可能引起预想不到的结果。如果是等待时间非常短,则可以用。
睡眠与唤醒
进程无法进入临界区时将阻塞,而不是忙等待。
最简单的就是 sleep 和 wakeup。 sleep 是一个将引起系统进程阻塞的调用,即被挂起,直到另外一个进程将其唤醒。
典型应用就是生产者-消费者。
信号量
信号量(semaphore)是 E.W.Dijkstra 在 1965 年提出的一种方法,它使用一个整型变量来累计唤醒次数,供以后使用。
用信号量也可以解决生产者-消费者问题。
互斥量
互斥量是信号量最简化的一个版本,不需要计数。互斥量是一个处于两种状态之一的变量:加锁和解锁。
互斥量使用两个过程:
当一个线程需要访问临界区,它调用 mutex_lock。如果互斥量当前是解锁的(即临界区可用),此调用成功,调用线程可自由进入临界区。
另一方面,如果该互斥量已经加锁,调用线程阻塞,直到在临界区的线程完成并调用 mutex_unlock。
如果多个线程阻塞在互斥量上,将随机选择一个线程并允许它获得锁。
管程和条件变量
Brinch Hansen(1973)和Hoare(1974)提出了一种高级同步原语,称为管程(monitor)。
一个管程是有一个过程、变量及数据结构等组成的一个集合,他们组成一个特殊的包或者软件包。
管程是一种语言的概念。管程有一个很重要的特性,即任一时刻管程中只能有一个活跃进程,这一特性使管程能有效地完成互
斥。管程是编程语言的组成部分,编译器知道它们的特殊性。
进入管程时的互斥由编译器负责,但通常的做法是用一个互斥量或二元信号量。因为是由编译器而非程
序员来安排互斥,所以出错的可能性要小得多。在任一时刻,写管程的人无须关心编译器是如何实现互斥
的。他只需知道将所有的临界区转换成管程过程即可,决不会有两个进程同时执行临界区中的代码
当然解决生成者-消费者,缓存区满的问题,引入了条件变量(condition variables)以及相关2个操作:wait 和 signal。
在管程运行过程中发现生成者满时,它可以在条件变量上执行 wait 操作。该操作会导致调用进程自身阻塞,并且还将另一个以前等在管程之外的进程调入管程。
另外一个进程,比如消费者,还可以唤醒正在睡眠的伙伴进程。这可以通过对其伙伴正在等待的一个条件变量执行signal完成。
消息传递
进程间通信的方法使用两条原语 send 和 receive 。
Go 语言中通过 channel 在 goroutine 之间安全的传递消息。
五、操作系统进程间和Go语言通信方式有哪些
操作系统进程间通信方式
1、管道
2、消息队列
3、共享内存
4、信号量
5、信号
6、socket
1、管道
在 linux 系统的命令行下,运用 shell 命令来操作时,最常用的就是这种通信方式。
ps aux | grep nginx
管道 就是将前一个命令的输出内容作为下一个命令的输入内容,它的功能是一种单向命令传输。
管道又分为:1、 匿名管道 2、命名管道
匿名管道:上面的 shell 例子就是一种匿名管道,它没有名字。
命名管道:与上面相对应的就是有名字的管道,叫命名管道,它是 FIFO 结构,先进先出。
mkfifo pipename # pipename 就是管道的名字,mkfifo 是命令
2、消息队列
linux 内核也实现了消息队列,为进程间消息通信方式之一。
内核消息队列数据结构是一种链表,它里面的每一个数据都是一个单独的数据,叫消息体。
它与我们平时用的 kafka 消息队列作用差不多,是不是?kafka也是作为不同软件间消息通信的中间件,只不过 kafka 消息队列扩展出了很多功能,功能更多更强大。
3、共享内存
共享内存就是 2 个进程操作同一块内存。不需要拷贝来拷贝去的。
4、信号量
信号量其实是一种计数器,主要实现进程间的互斥和同步。
它并不缓存进程间的通信数据。
信号量是对资源计数,它有 2 个操作:
- P 操作:减 1 操作。相减后信号量 < 0,资源被占用尽,进程需要阻塞等待;相减后信号量 >= 0,还有可用资源,进程可进入正常执行。
- V 操作:加 1 操作。相加后信号量 <= 0,当前有阻塞中的进程,唤醒该进程运行;相加后信号量 > 0,当前没有阻塞的进程。
比如,多个进程操作共享内存时,在同一时刻不能有 2 个进程同时写数据到共享内存里,这样操作话,可能数据会发生错误,不是期望的数据。
5、信号
在 linux 中,为了响应各种事件操作,定义了很多信号,不同的信号代表不同的含义。
比如我们最常用的 SIGHUP 就是挂起。
当然还有很多信号,可以通过 kill -l 命令查询所有的信号。
信号与信号量虽然只差了一个字,但是两者用途完全不一样,千万别搞混淆了。
6、Socket
socket 用于网络间的通信,不同计算机间之间进程间的通信。
最常用的就是在 TCP/IP 网络编程中。
而上面的 5 种通信方式都是在同一台计算机的进程间进行通信。
Go语言通信方式
Go 语言中采用 CSP 模型来进行通信。
CSP - Communicating Sequential Process,通信顺序进程。
这是一种用于描述两个独立的并发实体通过共享的通讯 Channel(管道)进行通信的并发模型。
在 Go 语言中,关于通信方式有一句非常有名的话:
不要通过共享内存来通信,而应该通过通信来共享内存
六、并发编程常见问题
数据竞争
当两个或更多操作必须以正确的顺序执行时,就会出现竞争状态。
死锁
所有并发进程都在彼此等待的状态,都不能运行。在这种情况下,如果没有外部干预,程序永远不会恢复。
Go 运行时会检测一些死锁。
活锁
活锁就是并发的程序都可以运行,但好像彼此都在等待彼此,又都没运行。
饥饿
与死锁和活锁相似,它是指并发程序无法获得执行工作所需的资源。
七、Go协程:Goroutine
goroutine 是 Go 语言中进行并发编程一个很重要的概念。
goroutine 可以与其它 goroutine 并发(不一定是并行)执行函数,同时也会与主程序(main)并行执行,这个就是我们常说的主 goroutine。
goroutine 的一些优点:
- 轻量级,比线程使用内存更少
- 自主调度,效率高。Go 自己的 runtime 在用户空间调度 goroutine,不需要在内核空间和用户空间之间切换
- 利用多核,提高程序执行速度
- 避免阻塞,Go runtime 会监控协程所在的线程是否发生阻塞,如果阻塞,Go runtime 的调度器会把阻塞在线程上的协程调度到没有阻塞的线程上,继续运行。
怎么使用 goroutine?
Go 语言中运行并行编程关键字:go
func main() {
go sayHello()
go func() {
fmt.Println("hello 2")
}()
time.Sleep(3 * time.Second)
}
func sayHello() {
fmt.Println("hello")
}
文章如果有不足或错误之处,欢迎大家批评指出,也欢迎大家评论
八、参考
- 《操作系统设计与实现》作者: (美)ANDREW S.TANENBAUM / ALBERT S.WOODHULL
- 《现代操作系统》 作者:[荷] Andrew S. Tanenbaum / [荷] Herbert Bos
Go语言并发编程(1):对多进程、多线程、协程和并发、并行的理解的更多相关文章
- python 之 并发编程(线程Event、协程)
9.14 线程Event connect线程执行到event.wait()时开始等待,直到check线程执行event.set()后立即继续线程connect from threading impor ...
- Python 多进程 多线程 协程 I/O多路复用
引言 在学习Python多进程.多线程之前,先脑补一下如下场景: 说有这么一道题:小红烧水需要10分钟,拖地需要5分钟,洗菜需要5分钟,如果一样一样去干,就是简单的加法,全部做完,需要20分钟:但是, ...
- python 多进程/多线程/协程 同步异步
这篇主要是对概念的理解: 1.异步和多线程区别:二者不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段.异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事 ...
- python采用 多进程/多线程/协程 写爬虫以及性能对比,牛逼的分分钟就将一个网站爬下来!
首先我们来了解下python中的进程,线程以及协程! 从计算机硬件角度: 计算机的核心是CPU,承担了所有的计算任务.一个CPU,在一个时间切片里只能运行一个程序. 从操作系统的角度: 进程和线程,都 ...
- python并发编程-进程池线程池-协程-I/O模型-04
目录 进程池线程池的使用***** 进程池/线程池的创建和提交回调 验证复用池子里的线程或进程 异步回调机制 通过闭包给回调函数添加额外参数(扩展) 协程*** 概念回顾(协程这里再理一下) 如何实现 ...
- web服务-2、四种方法实现并发服务器-多线程,多进程,协程,(单进程-单线程-非堵塞)
知识点:1.使用多线程,多进程,协程完成web并发服务器 2.单进程-单线程-非堵塞也可以实现并发服务器 1.多进程和协程的代码在下面注释掉的部分,我把三种写在一起了 import socket im ...
- python教程:使用 async 和 await 协程进行并发编程
python 一直在进行并发编程的优化, 比较熟知的是使用 thread 模块多线程和 multiprocessing 多进程,后来慢慢引入基于 yield 关键字的协程. 而近几个版本,python ...
- Python并发编程系列之多进程(multiprocessing)
1 引言 本篇博文主要对Python中并发编程中的多进程相关内容展开详细介绍,Python进程主要在multiprocessing模块中,本博文以multiprocessing种Process类为中心 ...
- Python多线程、多进程和协程的实例讲解
线程.进程和协程是什么 线程.进程和协程的详细概念解释和原理剖析不是本文的重点,本文重点讲述在Python中怎样实际使用这三种东西 参考: 进程.线程.协程之概念理解 进程(Process)是计算机中 ...
- 多线程、多进程、协程、IO多路复用请求百度
最近学习了多线程.多进程.协程以及IO多路复用,那么对于爬取数据来说,这几个方式哪个最快呢,今天就来稍微测试一下 普通方式请求百度5次 import socket import time import ...
随机推荐
- [转帖]A Quick Look at the Huawei HiSilicon Kunpeng 920 Arm Server CPU
https://www.servethehome.com/a-quick-look-huawei-hisilicon-kunpeng-920-arm-server-cpu/ Huawei Hi ...
- [转帖]021系统状态检测命令sosreport
https://www.cnblogs.com/anyoneofus/p/16467677.html sosreport命令用于收集系统配置及架构信息并输出诊断文档.
- [转帖]Linux-文本处理三剑客grep详解
https://developer.aliyun.com/article/885611?spm=a2c6h.24874632.expert-profile.311.7c46cfe9h5DxWK 简介: ...
- Fabric配置块结构解析
本文是区块链浏览器系列的第二篇. 上一篇介绍了交易块中的数据结构,这一篇介绍区块链网络中的配置块数据结构. 这两种区块中数据结构内容的区别主要Payload结构体中的Data域中的内容,接下来将以类图 ...
- TienChin 渠道管理-渠道类型
在上一篇文章当中,表里面有一个渠道类型,我们这节主要是将这个渠道类型创建好,首先我们来看看字典表. sys_dict_type 表: 字段名 数据类型 注释 dict_id bigint 字典主键 d ...
- Java中YYYY-MM-dd在跨年时出现的bug
先看一张图: Bug的产生原因: 日期格式化时候,把 yyyy-MM-dd 写成了 YYYY-MM-dd Bug分析: 当时间是2019-08-31时, public class DateTest { ...
- @RequestBody中使用@DateTimeFormat报错:JSON parse error: Expected array or string.; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException
原因分析 根据异常提示:不匹配输入异常,指输入的参数错误,说是只支持String类型和Array数组类型的. @PostMapping("/test") public Dto ge ...
- 8.1 C++ 标准输入输出流
C/C++语言是一种通用的编程语言,具有高效.灵活和可移植等特点.C语言主要用于系统编程,如操作系统.编译器.数据库等:C语言是C语言的扩展,增加了面向对象编程的特性,适用于大型软件系统.图形用户界面 ...
- System V|共享内存基本通信框架搭建|【超详细的代码解释和注释】
前言 那么这里博主先安利一下一些干货满满的专栏啦! 手撕数据结构https://blog.csdn.net/yu_cblog/category_11490888.html?spm=1001.2014. ...
- java线程池实现多任务并发执行
Java线程池实现多任务并发执行 1️⃣ 创建一些任务来落地多任务并发执行 每一个数组里面的数据可以看成任务,或者是需要并发的业务接口, 数组与数组之间,可以看作为他们之间有血缘关系,简单来说就是: ...