* 本文从两点进行分析:
* 线程调度
* 内存占用
* 文章很多内容都是仅凭个人认知推测, 并未进行实时考证, 不过个人感觉应该有参考价值(不然也不会写了)
## 线程调度
### `linux`进程调度的时机
* 进程不再处于`TASK_RUNNING`状态
* 如: 由于等待IO而阻塞(`TASK_UNINTERRUPTIBLE`),或者因等待资源和特定事件而休眠(`TASK_INTERRUPTIBLE`),又或者被debug/trace设置为`TASK_STOPPED`/`TASK_TRACED`状态。
* 周期性的时钟中断处理函数中, 判断进程被抢占(preemption)(取决于`preemption`算法)
```c
xxx_irq_xxx(){
...{
twd_handler(){
...{
update_process_times(){
scheduler_tick(){
...{
update_curr() //更新时间片计数
//检查是否被抢占(preemption)(取决于`preemption`算法)
//如:进程时间片达到最大值,有优先级更高的进程
//判断是否需要重新调度,需要则设置TIF_NEED_RESCHED标志
check_preempt_tick()
}
}
}
}
}
}
ret_to_user_from_irq (){
slow_work_pending (){
if TIF_NEED_RESCHED {
do_work_pending(){
schedule() //进程调度
}
}
}
}
}
```
* 新进程创建的时候, 判断进程被抢占(preemption)(取决于`preemption`算法)
* ......
### `linux`的线程
* 其实`linux`最初是不支持线程的, 好像`unix`标准中就没有线程, 线程是在`Posix`标准规定的(貌似,记不太得), `linux`中的线程其实就是进程(轻量级进程), 线程的概念最初来源于`windows`, 这里不作深究。
### 为什么多线程阻塞`io`服务器模型会频繁触发进程调度
* 每次`io`操作, 进程变为睡眠状态, 触发进程调度。
* 周期性的时钟中断处理函数中, 会判断进程是否被抢占(preemption), 这取决于`preemption`算法(个人推测 4 核 40个进程 肯定 比 4 核 16个进程触发的次数多)。
* 虽然使用了线程池, 但是还是免不了创建新线程, 可能会触发进程调度。
* 另外需要注意的一点是, 多线程阻塞`io`服务器模型不仅会频繁触发进程调度, 而且每次调度的进程数量(虽然有一部分进程处于`TASK_UNINTERRUPTIBLE`,`TASK_INTERRUPTIBLE`状态不需要被调度)要比事件驱动服务器模型的进程数量要多很多, 那么调度的时间是否也会变长, 当然这取决于进程调度算法,这里不作深究。
## 内存占用
* 一个线程至少要占用固定大小的内存作为函数栈, 还有一些内核结构体, 相对来说事件驱动模型更节省内存占用
## `Golang`- 用户态线程(`goroutine`)调度
* 问题: `goroutine`是如何进行调度的呢?
* 首先`linux`进程调度执行时机:
* 从系统调用(`syscall`)返回用户态时
* 从中断返回用户态时
* 显然`golang`程序不可能基于时钟中断, 系统调用, 那么go语言在什么时候进行进程调度呢?
* 个人认为, go语言的所有`io`操作在底层实现都采用多路**`IO`多路转接**,而并非 **阻塞`IO`**(因为这会触发系统进程调度), 当`goroutine`进行`io`操作, `channel`读写时(可能还有其他操作)时, 触发`goroutine`调度。
* 所以`goroutinue`没有强制的调度方式, 此时拿 多路IO复用事件驱动的服务器模型 做一下对比, 神似, 事件分发 换成 用户态线程调度, `goroutinue`没有强制的调度方式, 在一些文章中也有体现:
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。
* 个人结论:
* go语言的服务器模型是基于多路`io`复用以及用户态线程(`goroutine`)调度的高并发服务器模型。
* 用户态线程调度比系统进程调度好在哪里呢, 我觉得是非强制的调度方式, 操作系统时钟中断的抢占调度算法是为了尽量公平的分配`cpu`资源, 而非强制的调度方式, 不在乎公平, 更多在乎性能, 使`cpu`占用更多的跑在功能代码上, 提高并发。
* 我认为, 这种用户态的线程调度在性能上还是比不过事件驱动, 是两种服务器模型的折中, 提高了性能,降低了开发难度, 非常复杂的设计, 不愧为 为编写高性能高并发服务器设计的语言。
## 内容参考
* [Linux进程调度浅析](https://blog.csdn.net/zhoutaopower/article/details/86290196)
* https://blog.csdn.net/zhoutaopower/article/details/86290196
* [Go语言基础之并发](https://www.liwenzhou.com/posts/Go/14_concurrence/)
* https://www.liwenzhou.com/posts/Go/14_concurrence
有疑问加站长微信联系(非本文作者))