GO学习笔记(三)进程到线程再到协程
写在开头
前文程序到进程,我们介绍了一个文本程序是如何加上运行状态变成一个进程的(进程=程序+运行状态),分析ELF文件格式我们知道每个进程都有自己独立的一块内存空间来存放资源,但是对于任何一个进程,不论是否主动创建线程,它都会有一个默认的主线程,那它是干嘛的?它负责执行二进制指令(进程主要负责内存、文件系统的管理和调度)
1.线程
(来自阮一峰老师博客的比喻)假定将我们电脑的CPU抽象成一座工厂,它时刻在运行执行计算任务,但是工厂的电力有限,一次只能给一个车间提供电力,那么一个车间开的时候其他车间必须休息,即单个CPU一次只能运行一个任务,进程就是工厂的车间,任何时刻,工厂只能运行一个车间,CPU总是运行一个进程,其他进程是非运行状态,车间里面有许多人,他们一起完成车间的生产任务,车间的工人就好比线程,车间的空间是工人共享的,工人也有每个自己的房间,即进程的内存空间,每个线程都可以使用,他们也有自己的私有资源,房间大小不同,有的房间只能容纳一个人,即一个线程使用某些共享内存时,其他线程必须等待,怎么让其他人等待?最简单的办法就是加锁,
总结时间:
1.一个CPU任何时间只能执行一个进程
2.一个进程可以有多个线程
3.他们能共享一些资源,也能独占一些资源
4.进程是拥有和分配资源的基本单位,线程是进程的一部分,描述指令流执行的状态,是进程指令执行流的最小单元(二进制指令一行一行执行下来),即CPU调度的基本单位,进程负责给线程分配资源
为什么完成一个任务用多个线程,而不是多个进程?
1.进程的创建麻烦,开销大(申请资源,内存独占);2,进程间的通信不方便
但是多个线程共享进程的堆和方法区资源,每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在生产一个线程或来回切换线程的负担要比进程小,这也被称为轻量级进程(进程切换时需要转换内存地址空间,将不用的代码切入到外存)
为什么内存地址空间转换这么慢?Linux 实现中,每个进程的地址空间都是虚拟的,虚拟地址空间转换到物理地址空间需要查页表,这个查询是很慢的过程,因此会用一种叫做 TLB 的 cache 来加速,当进程切换后,TLB 也随之失效了,所以会变慢。
1.1线程的数据
我们把线程的数据分为三类 线程数据1.共享内存中的全局变量。
虽然在不同进程间是隔离的,但是整个进程是共享的,如果两个线程同时修改需要建立保护机制
2.线程本地数据。
如函数执行过程中的局部变量,每个线程都有自己的栈空间,函数的调用会使用栈,局部变量存放在里面,线程之间还会有小块区域来保护隔离自己的栈空间,一旦另一个线程踏入隔离区就会引发段错误
3.线程私有数据。
在线程内部,私有数据可以被各个函数访问,但对其他线程是屏蔽的,即全局变量私有化(同名不同值或一键多值)。
1.2操作系统调度线程
线程具有就绪、阻塞、运行三种基本状态,可以并发执行,在多CPU环境下,各个线程也可以分派到不同的CPU上并行执行。
CPU一般会有多个核心(core)每个核心都调度一个线程(多核让电脑更快的原理),操作系统(OS)要在合适的时候分配CPU核心来调度合适的线程,如何调度:
- 根据线程优先级分配每次调度最多的执行时间片,时间一到重新调度
- 等待某些特定的条件(如IO,网卡发送数据,休眠等)导致线程被挂起(挂起的意思就是当前代码被放入外存,保存上下文执行环境,没执行,唤起就是从外存交换进内存执行),OS会重新找一个新线程执行,直到挂起的条件满足再放回调度队列里等待调度
综上,我们了解到了线程切换是操纵系统调度器(os scheduler)让CPU的一个核(core)从执行一个线程(thread)变成执行另一个线程,虽然开销不像进程一样大,但是每次切换耗时1000-1500纳秒(这些时间可以执行12000-18000条CPU指令!),并且因为分配时间片,线程在一个core上执行一段时间后是会被强制重新调度的(好处是写程序的人不需要考虑切换,,不让自己的程序配合OS Scheduler来做切换)
2.协程
当程序的IO密集型时(如Web服务器、网管),为了提高吞吐量,有两种思路:
1.为每个请求开一个线程,为降低线程创建开销,使用线程池,但是线程池越大CPU花在切换上的开销越大(上文说到切换耗时)
线程创建和销毁都需要调用系统调用,每次请求都创建高并发下开销很大(调用系统调用涉及到用户态和内核态的转换)
2.使用异步非阻塞,用一个进程或线程接受请求,然后通过IO多路复用让进程或线程不阻塞
简单来说就是一排水龙头都开着,一个进程等待哪个水龙头有水就去接
方案1实现简单,但是性能不高(线程切换);方案2性能好,但是实现复杂。协程(coroutine)就是介于这两种方案之间出现的(GO适合IO密集型的原因)
操作系统没有创建协程的系统调用,即没有协程这个概念,他是基于用户的视角,主要是在用户态实现调度算法,协程的调度完全由用户控制,他这玩意是我们自己创建的(协程实际上是一个特殊的数据结构)
2.1协程调度
协程的调度方式是non-preemptive,即一个gorutine干了一段时间干累了,主动告诉GO Scheduler切换为另一个gorutine(主动放弃执行机会,协作完成任务),其中这个告诉操作被隐藏在GO标准函数库函数,以及一些语法操作如 go 和 channel io里 进程,线程和协程这里我们简单介绍到这里,讲到runtime时再具体研究
参考
1.一文读懂什么是进程、线程、协程
2.理解协程、线程的区别
3.有线程了,为啥还要协程
4.趣谈linux操作系统
5.CPU、Processor、Core的区别
6.进程和线程区别
7.一文彻底弄懂进程和线程调度
8.进程和线程基础必知