【并发编程重要概念记录】

一、协程

为什么需要协程?
当需要多项任务同时执行的时候,就会采用多线程并发执行,但是如果有海量线程并行,即线程数量非常多的时候,就会产生问题。举一个例子:

假设最开始我们只有可怜的10个用户,收到10条付款消息之后,我们开启启动10个线程去查询数据库,由于用户量很少,结果马上就返回了。第2天用户增加到了100人,你选择增加100个线程去查询数据库,等到第三天,你们加大了优惠力度,这时候有1000人同时在线付款,你按照之前的方法,继续采用1000个线程去查询数据库,并且隐隐觉察到有什么不对。

几天之后,见势头大好,运营部门开始不停的补贴消费券,展开了史无前例的大促销,你们的用户开始爆炸增长,这时候有10000人同时在线付款,你打算启动10000个线程来处理任务。等等,问题来了,因为每个线程至少会占用4M的内存空间,10000个线程会消耗39G的内存,而服务器的内存配置只有区区8G,这时候你有2种选择,一是选择增加服务器,二是选择提高代码效率。那么是否有方法能够提高效率呢?

操作系统在线程等待IO的时候,会阻塞当前线程,这样在线程数量非常庞大的时候,就会有问题。一是系统线程会占用非常多的内存空间,二是过多的线程切换会占用大量的系统时间。

协程刚好可以解决上述2个问题。协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。

注意事项
假设协程运行在线程之上,并且协程调用了一个阻塞IO操作,这时候会发生什么?实际上操作系统并不知道协程的存在,它只知道线程,因此在协程调用阻塞IO操作的时候,操作系统会让线程进入阻塞状态,当前的协程和其它绑定在该线程之上的协程都会陷入阻塞而得不到调度,这往往是不能接受的。
因此在协程中不能调用导致线程阻塞的操作。也就是说,协程只有和异步IO结合起来,才能发挥最大的威力。

那么如何处理在协程中调用阻塞IO的操作呢?一般有2种处理方式:

1.在调用阻塞IO操作的时候,重新启动一个线程去执行这个操作,等执行完成后,协程再去读取结果。这其实和多线程没有太大区别。
2.对系统的IO进行封装,改成异步调用的方式,这需要大量的工作,最好寄希望于编程语言原生支持。

附:Java不支持协程,这种老古董语言发明的时候当时还没有协程这个概念,协程是近年才逐渐兴起并应用,如Golang中的goroutine。

二、上下文切换

上下文是由程序运行所需的状态组成,包括程序的代码和数据,堆栈,寄存器,程序计数器等。

上文(过去):已执行过的进程指令和数据在相关寄存器与堆栈中的内容;

正文(现在):正在执行的指令和数据在寄存器与堆栈中的内容;

下文(未来):待执行的指令和数据在寄存器与堆栈中的内容;

一个进程(Process)可以包含多个线程(Thread),每一个线程都有自己的上下文和唯一的线程id,叫做tid;切换各个进程/线程,这个过程就叫做上下文切换(context with),上下文切换有开销,但是由于一个进程内的线程们共享一个虚拟地址空间,所以线程的上下文切换开销比进程要小。

linux中进程的上下文开销有下面几项:

  • 地址空间的转换,如切换页表全局目录,刷新TLB
  • 硬件上下文切换,如切换内核栈,载入寄存器数据
  • 对于同一个进程内的线程来说,上下文切换,跟进程类似,相对开销略低一些。

另外上下文的切换有可能会导致L1、L2、L3缓存中的数据会失效,新的进程会穿透到内存,破坏了数据的局部性,这也是会有开销的。

三、用户态和内核态

简单来说,用户态和内核态就是权限不同;
底层硬件系统中有个叫CPU指令集的东西,可以直接实现软件智慧硬件执行的媒介,具体来说每一条汇编语句都对应了一条 C P U 指令,而非常非常多的 C P U 指令 在一起,可以组成一个、甚至多个集合,指令的集合叫 C P U 指令集。
而把如此重要的东西随随便便交给用户明显不是个理智的行为,所以就有了用户态和内核态,也就是操作CPU指令集的权限区分。

以Intel为例,Intel把CPU指令集操作权限由高到低设置为四级:

  • ring 0
  • ring 1
  • ring 2
  • ring 3

其中 ring 0 权限最高,可以使用所有 C P U 指令集,ring 3 权限最低,仅能使用常规 C P U 指令集,不能使用操作硬件资源的 C P U 指令集,比如 I O 读写、网卡访问、申请内存都不行,Linux系统仅采用ring 0 和 ring 3 这2个权限。

高情商
ring 0被叫做内核态,完全在操作系统内核中运行
ring 3被叫做用户态,在应用程序中运行
低情商
执行内核空间的代码,具有ring 0保护级别,有对硬件的所有操作权限,可以执行所有C P U 指令集,访问任意地址的内存,在内核模式下的任何异常都是灾难性的,将会导致整台机器停机
在用户模式下,具有ring 3保护级别,代码没有对硬件的直接控制权限,也不能直接访问地址的内存,程序是通过调用系统接口(System Call APIs)来达到访问硬件和内存,在这种保护模式下,即时程序发生崩溃也是可以恢复的,在电脑上大部分程序都是在,用户模式下运行的.

用户态与内核态

通关了C P U 指令集权限,现在再说用户态与内核态就十分简单了,用户态与内核态的概念就是C P U 指令集权限的区别,进程中要读写 I O,必然会用到 ring 0 级别的 C P U 指令集,而此时 C P U 的指令集操作权限只有 ring 3,为了可以操作ring 0 级别的 C P U 指令集, C P U 切换指令集操作权限级别为 ring 0,C P U再执行相应的ring 0 级别的 C P U 指令集(内核代码),执行的内核代码会使用当前进程的内核栈。

PS:每个进程都有两个栈,分别是用户栈与内核栈,对应用户态与内核态的使用

用户态与内核态的切换

相信大家都听过这样的话「用户态和内核态切换的开销大」,但是它的开销大在那里呢?简单点来说有下面几点

  • 保留用户态现场(上下文、寄存器、用户栈等)
  • 复制用户态参数,用户栈切到内核栈,进入内核态
  • 额外的检查(因为内核代码对用户不信任)
  • 执行内核态代码
  • 复制内核态代码执行结果,回到用户态
  • 恢复用户态现场(上下文、寄存器、用户栈等)
  • 实际上操作系统会比上述的更复杂,这里只是个大概,我们可以发现一次切换经历了「用户态 -> 内核态 -> 用户态」。

用户态要主动切换到内核态,那必须要有入口才行,实际上内核态是提供了统一的入口。

最后来说说,什么情况会导致用户态到内核态切换

  • 系统调用:用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用,系统调用的机制核心使用了操作系统为用户特别开放的一个中断来实现,如Linux 的 int 80h 中断,也可以称为软中断
  • 异常:当 C P U 在执行用户态的进程时,发生了一些没有预知的异常,这时当前运行进程会切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常
  • 中断:当 C P U 在执行用户态的进程时,外围设备完成用户请求的操作后,会向 C P U 发出相应的中断信号,这时 C P U 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。

参考文章链接:
什么是协程?
什么是进程和线程的上下文切换
用户态和内核态

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值