求助>使用 ThreadPoolExecutor 的时候看不到 CPU 绑定任务的上下文切换开销>
1回复

使用 ThreadPoolExecutor 的时候看不到 CPU 绑定任务的上下文切换开销



我在做一个简单的实验,我想找出当有一堆CPU密集型任务时合适的线程池大小。

我知道这个大小应该等于机器上的内核数,但我想验证这一点。 这是我的代码:

public class Main {

    public static void main(String[] args) throws ExecutionException {
        List<Future> futures = new ArrayList<>();
        ExecutorService threadPool = Executors.newFixedThreadPool(4);

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 100; i++) {
            futures.add(threadPool.submit(new CpuBoundTask()));
        }

        for (int i = 0; i < futures.size(); i++) {
            futures.get(i).get();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("Time = " + (endTime - startTime));
        threadPool.shutdown();
    }

    static class CpuBoundTask implements Runnable {
        @Override
        public void run() {
            int a = 0;
            for (int i = 0; i < 90000000; i++) {
                a = (int) (a + Math.tan(a));
            }
        }
    }
}

每个任务在大约 700 毫秒内执行(我觉得这足以被 ThreadScheduler 抢占至少一次了)。

我在 MacbookPro 2017、3.1 GHz Intel Core i5、2 个已激活超线程的物理内核上运行它,所以有 4 个逻辑 CPU。

我调整了线程池的大小,并多次运行这个程序(平均时间)。 结果如下:

1 thread = 57 seconds
2 threads = 29 seconds
4 threads = 18 seconds
8 threads = 18.1 seconds
16 threads = 18.2 seconds
32 threads = 17.8 seconds
64 threads = 18.2 seconds

因为上下文切换开销,所以一旦我添加了这么多线程(超过 CPU 内核的数量),我预计执行时间会增加比较多,但好像并没有发生。

我用 VisualVM 来监视程序,看起来所有线程都已经创建,并且跟预期一样处于运行状态。 还有CPU占用率也不是很高(接近 95%)。

请问大佬我漏掉什么要点了吗

586 阅读
请先登录,再评论

这种情况,你应该用 System.nanoTime() 而不是 System.currentTimeMillis()。

你的算法在 4 个线程的时候停止扩展,简单起见,我们假设所有线程执行相同数量的任务,因此每个线程 25 个。 每个线程大约需要 18 秒来计算 25 次迭代。

当你使用 64 个线程运行时,每个内核就有 8 个线程,并且在前 4 次迭代中,有 4 个线程并行运行(每个内核 1 个),其他 60 个线程处于空闲模式等待 CPU 资源来计算它们的迭代:

Iteration 0 : Thread 1 (running)
Iteration 1 : Thread 2 (running)
Iteration 2 : Thread 3 (running)
Iteration 3 : Thread 4 (running)
Iteration 4 : Thread 5 (waiting)
Iteration 5 : Thread 6 (waiting)
Iteration 6 : Thread 7 (waiting)
Iteration 7 : Thread 8 (waiting)
...
Iteration 63 : Thread 64 (waiting)

当这 4 个线程完成它们的迭代时,它们将分别获得另一个迭代。 与此同时,线程 5 到 8 开始在接下来的四次迭代中工作(同样是 4 个线程并行执行工作),而其他线程被阻塞等待 CPU,依此类推。 所以你总是有 4 个线程并行运行,这就是为什么:

8 threads = 18.1 seconds
16 threads = 18.2 seconds
32 threads = 17.8 seconds
64 threads = 18.2 seconds

你的执行时间大致相同,与 4 个线程并行完成 25 次迭代所需的执行时间大致相同。

因为这是一个不受 CPU 限制的算法,没有以下问题:
同步;加载不平衡(即,每个循环迭代花费大约相同的执行时间);内存带宽饱和;缓存失效;伪共享。
当你增加每个内核的线程数时,它不会在整体执行时间上反映出那么多。

11月前