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 ...
随机推荐
- [转帖]linux中批量多行缩进与添加空格
用vim打开修改python脚本的时候,将代码整体向后移动4个空格操作如下: ESC之后,ctrl+v进入多行行首选中模式 使用上下键进行上下移动,选中多行行首 shift+i,进入插入模式 连续敲击 ...
- [转帖]rsar - Extract data from plain-text sar files
sar -A -t -f /tmp/sa11 >/tmp/sar11 https://github.com/ryran/rsar When dealing with sysstat sar da ...
- [转帖]Guanaco, Llama, Vicuña, Alpaca该怎么区别
https://zhuanlan.zhihu.com/p/106262896 在智利和秘鲁高原区经常会遇到的一种动物让人十分挠头,学术点称呼就是骆驼科其中一个族群--羊驼属和骆马属.头疼在于,分不清楚 ...
- [转帖]linux 查看CPU 内存的信息
https://bbs.huaweicloud.com/blogs/302929 [摘要] ECS信息规格:2vCPUs | 4GiB | kc1.large.2镜像:openEuler 20.0 ...
- [转帖]jvm学习三-MAT内存分析工具的使用
目录 1 模拟内存溢出程序 1.1 jvm配置 1.2 测试代码 2 MAT工具进行内存分析 2.1 大纲介绍 2.2 Histogram视图介绍 2.3 Leak Suspects视图介绍 2.4 ...
- [转帖]陈巍谈芯:NLP里比BERT更优秀的XLNet长什么样?
https://zhuanlan.zhihu.com/p/447836322 目录 收起 一.XLNet的优势 1)独得AR与AE两大绝学 2)集成了Tansformer-XL 二.XLNet的结 ...
- 简单定位占用最高CPU的java进程信息
公司里面一个应用不小心点击就会导致系统性能下降很明显. 性能组的同事定位到了, 我这里以学习的态度重现一下这个过程. 1. 问题再现 产品一个非常大数据量的帮助, 点击之后就会占用非常多的cpu 因为 ...
- 文心一言 VS 讯飞星火 VS chatgpt (186)-- 算法导论14.1 3题
三.用go语言,写出OS-SELECT的非递归版本. 文心一言: 下面是使用Go语言编写的OS-SELECT的非递归版本: func OS_SELECT(T *Node, k int) *Node { ...
- SHA加密在实际应用中的优势与局限
SHA加密算法简介 SHA(Secure Hash Algorithm)加密算法是一种单向加密算法,常用于加密数据的完整性校验和加密签名.它是由美国国家安全局(NSA)设计并广泛应用于各种安全场景.S ...
- 【小测试】rust中的无符号整数溢出
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 1.在编译阶段就可以识别出来的溢出 fn main(){ ...