写点什么

java 高级用法之: 绑定 CPU 的线程 Thread-Affinity

作者:程序那些事
  • 2022 年 4 月 20 日
  • 本文字数:4031 字

    阅读完需:约 13 分钟

java高级用法之:绑定CPU的线程Thread-Affinity

简介

在现代计算机系统中,可以有多个 CPU,每个 CPU 又可以有多核。为了充分利用现代 CPU 的功能,JAVA 中引入了多线程,不同的线程可以同时在不同 CPU 或者不同 CPU 核中运行。但是对于 JAVA 程序猿来说创建多少线程是可以自己控制的,但是线程到底运行在哪个 CPU 上,则是一个黑盒子,一般来说很难得知。


但是如果是不同 CPU 核对同一线程进行调度,则可能会出现 CPU 切换造成的性能损失。一般情况下这种损失是比较小的,但是如果你的程序特别在意这种 CPU 切换带来的损耗,那么可以试试今天要讲的 Java Thread Affinity.

Java Thread Affinity 简介

java thread Affinity 是用来将 JAVA 代码中的线程绑定到 CPU 特定的核上,用来提升程序运行的性能。


很显然,要想和底层的 CPU 进行交互,java thread Affinity 一定会用到 JAVA 和 native 方法进行交互的方法,JNI 虽然是 JAVA 官方的 JAVA 和 native 方法进行交互的方法,但是 JNI 在使用起来比较繁琐。所以 java thread Affinity 实际使用的是 JNA,JNA 是在 JNI 的基础上进行改良的一种和 native 方法进行交互的库。


先来介绍 CPU 中几个概念,分别是 CPU,CPU socket 和 CPU core。


首先是 CPU,CPU 的全称就是 central processing unit,又叫做中央处理器,就是用来进行任务处理的关键核心。


那么什么是 CPU socket 呢?所谓 socket 就是插 CPU 的插槽,如果组装过台式机的同学应该都知道,CPU 就是安装在 Socket 上的。


CPU Core 指的是 CPU 中的核数,在很久之前 CPU 都是单核的,但是随着多核技术的发展,一个 CPU 中可以包含多个核,而 CPU 中的核就是真正的进行业务处理的单元。


如果你是在 linux 机子上,那么可以通过使用 lscpu 命令来查看系统的 CPU 情况,如下所示:


Architecture:          x86_64CPU op-mode(s):        32-bit, 64-bitByte Order:            Little EndianCPU(s):                1On-line CPU(s) list:   0Thread(s) per core:    1Core(s) per socket:    1Socket(s):             1NUMA node(s):          1Vendor ID:             GenuineIntelCPU family:            6Model:                 94Model name:            Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHzStepping:              3CPU MHz:               2400.000BogoMIPS:              4800.00Hypervisor vendor:     KVMVirtualization type:   fullL1d cache:             32KL1i cache:             32KL2 cache:              4096KL3 cache:              28160KNUMA node0 CPU(s):     0Flags:                 fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl eagerfpu pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single fsgsbase bmi1 hle avx2 smep bmi2 erms invpcid rtm mpx avx512f avx512dq rdseed adx smap avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 arat
复制代码


从上面的输出我们可以看到,这个服务器有一个 socket,每个 socket 有一个 core,每个 core 可以同时处理 1 个线程。


这些 CPU 的信息可以称为 CPU layout。在 linux 中 CPU 的 layout 信息是存放在/proc/cpuinfo 中的。


在 Java Thread Affinity 中有一个 CpuLayout 接口用来和这些信息进行对应:


public interface CpuLayout {        int cpus();
int sockets();
int coresPerSocket();
int threadsPerCore();
int socketId(int cpuId);
int coreId(int cpuId);
int threadId(int cpuId);}
复制代码


根据 CPU layout 的信息, AffinityStrategies 提供了一些基本的 Affinity 策略,用来安排不同的 thread 之间的分布关系,主要有下面几种:


    SAME_CORE - 运行在同一个core中。    SAME_SOCKET - 运行在同一个socket中,但是不在同一个core上。    DIFFERENT_SOCKET - 运行在不同的socket中    DIFFERENT_CORE - 运行在不同的core上    ANY - 任何情况都可以
复制代码


这些策略也都是根据 CpuLayout 的 socketId 和 coreId 来进行区分的,我们以 SAME_CORE 为例,按下它的具体实现:


SAME_CORE {        @Override        public boolean matches(int cpuId, int cpuId2) {            CpuLayout cpuLayout = AffinityLock.cpuLayout();            return cpuLayout.socketId(cpuId) == cpuLayout.socketId(cpuId2) &&                    cpuLayout.coreId(cpuId) == cpuLayout.coreId(cpuId2);        }    }
复制代码


Affinity 策略可以有顺序,在前面的策略会首先匹配,如果匹配不上则会选择第二策略,依此类推。

AffinityLock 的使用

接下来我们看下 Affinity 的具体使用,首先是获得一个 CPU 的 lock,在 JAVA7 之前,我们可以这样写:


AffinityLock al = AffinityLock.acquireLock();try {     // do some work locked to a CPU.} finally {     al.release();}
复制代码


在 JAVA7 之后,可以这样写:


try (AffinityLock al = AffinityLock.acquireLock()) {    // do some work while locked to a CPU.}
复制代码


acquireLock 方法可以为线程获得任何可用的 cpu。这个是一个粗粒度的 lock。如果想要获得细粒度的 core,可以用 acquireCore:


try (AffinityLock al = AffinityLock.acquireCore()) {    // do some work while locked to a CPU.}
复制代码


acquireLock 还有一个 bind 参数,表示是否将当前的线程绑定到获得的 cpu lock 上,如果 bind 参数=true,那么当前的 thread 会在 acquireLock 中获得的 CPU 上运行。如果 bind 参数=false,表示 acquireLock 会在未来的某个时候进行 bind。


上面我们提到了 AffinityStrategy,这个 AffinityStrategy 可以作为 acquireLock 的参数使用:


    public AffinityLock acquireLock(AffinityStrategy... strategies) {        return acquireLock(false, cpuId, strategies);    }
复制代码


通过调用当前 AffinityLock 的 acquireLock 方法,可以为当前的线程分配和之前的 lock 策略相关的 AffinityLock。


AffinityLock 还提供了一个 dumpLocks 方法,用来查看当前 CPU 和 thread 的绑定状态。我们举个例子:


private static final ExecutorService ES = Executors.newFixedThreadPool(4,           new AffinityThreadFactory("bg", SAME_CORE, DIFFERENT_SOCKET, ANY));
for (int i = 0; i < 12; i++) ES.submit(new Callable<Void>() { @Override public Void call() throws InterruptedException { Thread.sleep(100); return null; } }); Thread.sleep(200); System.out.println("\nThe assignment of CPUs is\n" + AffinityLock.dumpLocks()); ES.shutdown(); ES.awaitTermination(1, TimeUnit.SECONDS);
复制代码


上面的代码中,我们创建了一个 4 个线程的线程池,对应的 ThreadFactory 是 AffinityThreadFactory,给线程池起名 bg,并且分配了 3 个 AffinityStrategy。 意思是首先分配到同一个 core 上,然后到不同的 socket 上,最后是任何可用的 CPU。


然后具体执行的过程中,我们提交了 12 个线程,但是我们的 Thread pool 最多只有 4 个线程,可以预见, AffinityLock.dumpLocks 方法返回的结果中只有 4 个线程会绑定 CPU,一起来看看:


The assignment of CPUs is0: CPU not available1: Reserved for this application2: Reserved for this application3: Reserved for this application4: Thread[bg-4,5,main] alive=true5: Thread[bg-3,5,main] alive=true6: Thread[bg-2,5,main] alive=true7: Thread[bg,5,main] alive=true
复制代码


从输出结果可以看到,CPU0 是不可用的。其他 7 个 CPU 是可用的,但是只绑定了 4 个线程,这和我们之前的分析是匹配的。


接下来,我们把 AffinityThreadFactory 的 AffinityStrategy 修改一下,如下所示:


new AffinityThreadFactory("bg", SAME_CORE)
复制代码


表示线程只会绑定到同一个 core 中,因为在当前的硬件中,一个 core 同时只能支持一个线程的绑定,所以可以预见最后的结果只会绑定一个线程,运行结果如下:


The assignment of CPUs is0: CPU not available1: Reserved for this application2: Reserved for this application3: Reserved for this application4: Reserved for this application5: Reserved for this application6: Reserved for this application7: Thread[bg,5,main] alive=true
复制代码


可以看到只有第一个线程绑定了 CPU,和之前的分析相匹配。

使用 API 直接分配 CPU

上面我们提到的 AffinityLock 的 acquireLock 方法其实还可以接受一个 CPU id 参数,直接用来获得传入 CPU id 的 lock。这样后续线程就可以在指定的 CPU 上运行。


    public static AffinityLock acquireLock(int cpuId) {        return acquireLock(true, cpuId, AffinityStrategies.ANY);    }
复制代码


实时上这种 Affinity 是存放在 BitSet 中的,BitSet 的 index 就是 cpu 的 id,对应的 value 就是是否获得锁。


先看下 setAffinity 方法的定义:


    public static void setAffinity(int cpu) {        BitSet affinity = new BitSet(Runtime.getRuntime().availableProcessors());        affinity.set(cpu);        setAffinity(affinity);    }
复制代码


再看下 setAffinity 的使用:


long currentAffinity = AffinitySupport.getAffinity();Affinity.setAffinity(1L << 5); // lock to CPU 5.
复制代码


注意,因为 BitSet 底层是用 Long 来进行数据存储的,所以这里的 index 是 bit index,所以我们需要对十进制的 CPU index 进行转换。

总结

Java Thread Affinity 可以从 JAVA 代码中对程序中 Thread 使用的 CPU 进行控制,非常强大,大家可以运用起来。


本文已收录于 http://www.flydean.com/01-java-thread-affinity/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

发布于: 2022 年 04 月 20 日阅读数: 25
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
java高级用法之:绑定CPU的线程Thread-Affinity_Java_程序那些事_InfoQ写作社区