你不好奇 CPU 是如何执行任务的?( 三 )


你不好奇 CPU 是如何执行任务的?

文章插图
这样 a 和 b 变量就不会在同一个 Cache Line 中了 , 如下图:
你不好奇 CPU 是如何执行任务的?

文章插图
所以 , 避免 Cache 伪共享实际上是用空间换时间的思想 , 浪费一部分 Cache 空间 , 从而换来性能的提升 。
我们再来看一个应用层面的规避方案 , 有一个 JAVA 并发框架 Disruptor 使用「字节填充 + 继承」的方式 , 来避免伪共享的问题 。
Disruptor 中有一个 RingBuffer 类会经常被多个线程使用 , 代码如下:
你不好奇 CPU 是如何执行任务的?

文章插图
你可能会觉得 RingBufferPad 类里 7 个 long 类型的名字很奇怪 , 但事实上 , 它们虽然看起来毫无作用 , 但却对性能的提升起到了至关重要的作用 。
我们都知道 , CPU Cache 从内存读取数据的单位是 CPU Line , 一般 64 位 CPU 的 CPU Line 的大小是 64 个字节 , 一个 long 类型的数据是 8 个字节 , 所以 CPU 一下会加载 8 个 long 类型的数据 。
根据 JVM 对象继承关系中父类成员和子类成员 , 内存地址是连续排列布局的 , 因此 RingBufferPad 中的 7 个 long 类型数据作为 Cache Line 前置填充 , 而 RingBuffer 中的 7 个 long 类型数据则作为 Cache Line 后置填充 , 这 14 个 long 变量没有任何实际用途 , 更不会对它们进行读写操作 。
你不好奇 CPU 是如何执行任务的?

文章插图
另外 , RingBufferFelds 里面定义的这些变量都是 final 修饰的 , 意味着第一次加载之后不会再修改 ,  又由于「前后」各填充了 7 个不会被读写的 long 类型变量 , 所以无论怎么加载 Cache Line , 这整个 Cache Line 里都没有会发生更新操作的数据 , 于是只要数据被频繁地读取访问 , 就自然没有数据被换出 Cache 的可能 , 也因此不会产生伪共享的问题 。
你不好奇 CPU 是如何执行任务的?

文章插图
 
CPU 如何选择线程的?了解完 CPU 读取数据的过程后 , 我们再来看看 CPU 是根据什么来选择当前要执行的线程 。
在 Linux 内核中 , 进程和线程都是用 tark_struct 结构体表示的 , 区别在于线程的 tark_struct 结构体里部分资源是共享了进程已创建的资源 , 比如内存地址空间、代码段、文件描述符等 , 所以 Linux 中的线程也被称为轻量级进程 , 因为线程的 tark_struct 相比进程的 tark_struct 承载的 资源比较少 , 因此以「轻」得名 。
一般来说 , 没有创建线程的进程 , 是只有单个执行流 , 它被称为是主线程 。如果想让进程处理更多的事情 , 可以创建多个线程分别去处理 , 但不管怎么样 , 它们对应到内核里都是 tark_struct 。
你不好奇 CPU 是如何执行任务的?

文章插图
所以 , Linux 内核里的调度器 , 调度的对象就是 tark_struct , 接下来我们就把这个数据结构统称为任务 。
在 Linux 系统中 , 根据任务的优先级以及响应要求 , 主要分为两种 , 其中优先级的数值越小 , 优先级越高:
  • 实时任务 , 对系统的响应时间要求很高 , 也就是要尽可能快的执行实时任务 , 优先级在 0~99 范围内的就算实时任务;
  • 普通任务 , 响应时间没有很高的要求 , 优先级在 100~139 范围内都是普通任务级别;
 
调度类由于任务有优先级之分 , Linux 系统为了保障高优先级的任务能够尽可能早的被执行 , 于是分为了这几种调度类 , 如下图:
你不好奇 CPU 是如何执行任务的?

文章插图
Deadline 和 Realtime 这两个调度类 , 都是应用于实时任务的 , 这两个调度类的调度策略合起来共有这三种 , 它们的作用如下: