听说你的资源被盗用了
|
如我们在文章中提到的,Arachne 与其他线程管理器使用了完全不同的设计思路,因为我们的目的是充分利用 CPU 资源,所以应用程序需要对资源有着更细粒度的掌握和控制,其他的线程管理器都会尽可能地屏蔽底层的实现细节,让上层只关注内部的一些逻辑,我们从软件工程的角度也不能说谁对谁错,但是在真正追求极致的性能时,一定清楚对下一层甚至下两层的信息,在线程调度器这个场景下就是 CPU 资源的详细使用情况。 Linux 调度器中的调度类比较相似,当应用程序创建新的用户线程时,它需要指定该线程的类,核心策略会根据调度类选择 CPU 执行。默认的核心策略中包含两个线程类,分别是独占的(Exclusive)和正常的(Normal),前者会为线程保留整个 CPU 资源,而后者会在多线程之间分享资源。 资源预估 默认的核心策略中包含动态的 CPU 资源预估功能,它会使用以下的三个参数根据过去一段时间的负载和 CPU 使用情况调整当前应用程序申请的资源:
作为 Arachne 中的默认策略,它提供的一定是相对简单的、普适的模型,我们可以根据自己的需求调整策略中的三个参数,也可以实现其他的策略。
总结论文的最后在线程的创建、控制权转移等常见的调度原语方面对比了 Arachne、std::thread、Go 和 uThreads 几种具有相似功能的线程管理器,Arachne 在几个方面都有不错的表现: 为了减少程序中的缓存丢失,Arachne 的调度器不会使用准备队列,它会持续检查当前 CPU 上的所有活跃线程直到发现可以运行的线程,因为以下的两个原因,这个看起来非常简单的轮训机制实际上非常高效: 在同一时间,单个 CPU 上应该只会包含少数几个线程上下文; 当前线程一定会由其他核心唤醒,因为跨核的传输会带来缓存丢失,而在触发缓存丢失时可以并行扫描全部上下文不会带来过大的开销; 因为线程可能处于阻塞状态等待特定的执行条件满足,所以上下文中会包含 wakeupTime,即线程多久后需要被唤醒;线程可以通过 block(time) 和 signal(thread) 两个方法改变 wakeupTime 通知调度器当前线程的状态和唤醒时间。 核心策略
为了能够让应用程序对 CPU 有更细粒度的控制,Arachne 不会在运行时中指定 CPU 的使用策略,如何使用 CPU 都是由独立的策略模块确定的,Arachne 中的一些默认核心策略如果不能满足应用程序的需求,它们还可以实现一些自定义的策略满足特定的需求。 大多数的线程操作都需要在多个 CPU 之间通信,而跨核的通信会导致缓存缺失(Cahce Miss),缓存缺失大概需要 50 ~ 200 个 CPU 循环,这也是影响 Arachne 运行时性能的主要因素。为了解决缓存缺失带来的性能影响,运行时会在数据传输时并行执行其他的指令,优化 CPU 缓存以提升用户态线程的性能。线程的创建和调度是运行时的关键操作,优化这些操作的性能就可以降低延迟并提高吞吐量。 线程创建 多数的用户态线程管理器都会在同一个 CPU 上创建线程并使用工作窃取(Work-stealing)[^4]平衡多个 CPU 上的负载,但是工作窃取对于存在时间较短的线程来说是非常昂贵的,所以我们希望在线程创建时立刻触发负载均衡,将线程创建在其他的核心上;同时,为了减少程序中的缓存丢失,Arachne 将每一个线程的上下文都绑定了特定的 CPU 上,只有在仲裁者回收时才可能发生处理器迁移,大多数的线程在创建之后就不会触发迁移。当应用程序创建新的线程时:
每个 CPU 上都有对应的 maskAndCount 变量,其中存储着正在执行的线程和当前 CPU 上的线程数。为了减少线程的缓存丢失,我们使用单独的缓存块(Cache Line)存储线程所有信息,这样创建线程最少只需要触发四次缓存丢失,分别是读取maskAndCount、传输函数地址、参数和调度信息,能够最大限度的减少开销。 线程调度
传统的调度模型会在运行时中引入准备队列(Ready Queue)保存可执行的线程,例如:Linux 和 Go 语言的调度器[^5],但是如果准备队列是跨 CPU 的,那么增加或者删除任务时都需要修改多个变量以及获取共享锁,这都会触发缓存丢失进而影响性能。 (编辑:广元站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |

