上一篇文章我总结了进程的调度,现在操作系统为了支持多任务处理引入分时调度,达到在有限的CPU资源下能够运行更多程序;以允许在计算机是运行任意数量的程序,通过抢占式调度器实现分时技术来回换切换程序并且运行,换句话说,它通过在正在运行的进程之间共享CPU时间、快速从一个进程切换到下一个进程来制造多个进程同时运行的假象,这就是基本任务调度处理流程。
但是这种模式缺点是:进程本身是串行执行的,并且每次时间片用完之后会把当前进程状态保存到PCB中,在把下一个进程PCB加载到寄存器然后继续运行下一个进程,这样做是很消耗时间的,切换内存的会独立分配相关的数据重新分配;为了解决这个问题,后面的操作系统引入了多线程的概念,也就是把进程当做一个容器,而进程内部又引入了新的调度资源概念线程,线程成了CPU成了最小的基本的执行任务命令的单元。
多线程调度
每个进程都有一个线程或多个线程来执行调度任务,在进程内存为线程提供不同的栈,供进程内部的线程来临时存放数据;在内核中,每个线程也有对的内核栈,当线程切换到内核执行时就会使用到内核中栈;
传统多任务只是每个不同的程序进程互相间的并发执行,当线程支持了那么进程内就可以支持多任务并发执行了。例如打开一个浏览器,浏览器打开直接就会在操作系统上跑起来一个进程,而如果在浏览器里面打开单独的页面,这就是线程间的并发操作了,页面窗口A可以播放视频,页面窗口B可以浏览网页,这就是多线程的应用。
因为内核线程和用户线程是分开的,如果用户线程要去操作一些系统调用那么就要内核线程负责完成这个工作;但是好处就是用户态的线程调度不属于操作系统调度器管理,上图中的用户线程有可以自己实现线程调度器,来控制多个用户线程协调工作。
例如Rust语言中的第三方软件库Tokio,Tokio是Rust编程语言的软件库。它提供了运行时和启用了异步I/O的功能,可以使用它来编写多线程版本的异步运行时,可以运行使用async/await
编写的代码,Tokio其实就是标准的Async Rust用户态异步编程功能实现。
多线程模型
在早期的操作系统想要让进程支持多线程并发执行的话,可以使用第三方软件库来完成,可以在程序中编写一个简单的循环来执行不同代码块逻辑实现并发执行,这种模式的用户态线程多个需要调用内核线程的时候就会发生竞争等待情况,因为每次内核线程只能为一个用户态线程服务。
身经百战的开发者一定遇到过几次这样的情况:某个循环无法在开头和结尾判断是否继续进行循环,必须在循环体中间某处控制循环的进行,如果遇到这种情况,我们经常会在一个while (true)
循环体里实现中途退出循环的操作,但是在Rust中有一个loop
循环可以将整个进程进入真正的死循环状态,并且独占CPU资源;可以通过下面的代码模拟一个代码并行执行逻辑快的操作,如下代码:
// Rust 语言有原生的无限循环结构 —— loop:
fn main() {
let mut n:i64 = 0;
loop {
// action 要重复执行的代码
if n == 0 {
println!("播放音乐");
}
if n == 1 {
println!("播放视频");
}
if n == 2 {
println!("下载文件");
}
n = (n + 1) % 3;
}
}
这种方式的缺点很明显:当一部分代码块阻塞主了,其他逻辑代码块也不能执行了,并且阻塞也是整个进程,进程被阻塞就相当于整个程序停止运行;为了解决这个问题操作系统内核也引入了线程概念;
现在大多数操作系统都支持了内核级别的线程,内核级的线程是由操作系统管理的,如果用户态调用系统调用那么就需要操作系统内核辅助完成这个,如上图就是线程1:1模型,每个人用户线程调用系统调用不需要等待其他线程占用的情况。每个线程都有自己的对应的内核线程来完成工作,但是缺点很明显用户线程增加,那么内核线程也会增加。往往操作系统会限制用户态线程的数量,如果用户态数量没有限制内核线程也过多,导致整个操作系统性能下降,例如下面代码的线程调度顺序取决于操作系统的调度方式:
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..5 {
println!("number is {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("number is {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
最后一种线程模型就是N:M,即用户态线程数量要大于内核态的线程数量的关系模型;在多核处理器机器上可以把物理内核线程数设置为M,而用户态用户自己实现的线程数据设置N=M+1,也可以不做限制,这种设计缓解多对一模型中的内核线程过少导致线程阻塞的问题,也减少一对一模型中的内核线程数量随着用户态线程增多导致的数量过多问题,下图就为多对多模型关系图:
但是这种多对多模型的,内核线程调度和用户态线程任务调度管理算法实现起来很复杂,但是如果实现很好线程工作效率也会有很大提升。
加入用户态线程之后那么也有有线程自己状态控制模块即TCB,我之前的文章写过进程的PCB模块,当发送上下文切换的时候保存和还原PCB到程序寄存器里面工作开销很大;线程的TCB只是用户态的状态保存实现,但是如果实现起来很复杂,因为实现到多核的CPU缓存一致性问题,用户态线程的TLS存储数据会在不同的线程里使用的该变量都是副本,访问都是副本这个对应要怎么管理和实际用户态调度来说是个很大考验。
小结
想玩好多线程并发很困难,如果真的对多线程模型这么做研究的话,这方面需要深挖很多知识,为了解决这些问题,很多其它语言如Java
采用特殊的运行时(runtime)软件来协调资源,例如最近Java19要在今年9月要发布的新版本中加入协程的预览版本JEP425虚拟线程功能,如果这个功能添加那么写Java可以使用自己实现多线程模型也可以使用官方提供的协程功能。
另外一种就是Go语言,Go语言在用户态实现协程有自己的GMP调度器模型,但是他的GMP实现的协程Go的runtime屏蔽用户自己对真实的线程状态控制权,有利有弊吧;
Rust在这方面做了一些新的尝试,安全高效的处理并发是 Rust 诞生的目的之一,但 Rust 在语言本身就设计了包括所有权机制在内的手段来尽可能地把最常见的错误消灭在编译阶段,但是想玩好Rust本身也需要投入一点时间,本文就写到这着探讨一下多线程并发相关的问题,有兴趣的找找多线程并发或者并行模型相关的论文看看。
– END –
原文始发于微信公众号(TPaper):多线程模型的一些探讨
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/23584.html