ForkJoinPool的工作窃取机制无法正常工作

时间:2018-04-22 18:41:31

标签: java multithreading threadpool forkjoinpool

我有以下测试代码

public static void main(String[] args){
    ForkJoinPool pool = new ForkJoinPool(2);

    ForkJoinTask task3 = ForkJoinTask.adapt(() -> {
        System.out.println("task 3 executing");
        for(int i = 0; i < 10; ++i){
            System.out.println("task 3 doing work " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    ForkJoinTask task2 = ForkJoinTask.adapt(() -> {
        try {
            System.out.println("task 2 executing");
            Thread.sleep(5000);
            System.out.println("task 2 finishing");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    });

    pool.submit(task2);

    ForkJoinTask task1 = pool.submit(() -> {
        System.out.println("task 1 executing");
        pool.submit(task3); // EDIT: Original code was task3.fork();
        System.out.println("task 1 joining task 2");
        task2.join();
        System.out.println("task 1 finished");
    });

    task1.join();
}

它基本上向ForkJoinPool并行提交3个任务2,任务2和3长时间运行,任务1等待任务2。

标记2个线程t1和t2,其中t1执行task1,t2执行task2。

在我的理解中,工作窃取魔法发生在join()调用中,其中调用线程将从其自己的工作队列或其他工作线程的工作队列执行任务。因此我期望t1执行task1,看到join()调用然后窃取task3并执行它完成。

然而,在实践中,t1对join()调用没有任何特殊处理。 Task3仅在task1和task2完成后执行。为什么会这样?

1 个答案:

答案 0 :(得分:0)

在花了几个小时查看ForkJoinPool和ForkJoinTask的源代码之后,我发现了以下内容:

如果满足以下两个条件之一,

join()将导致线程查找并窃取任务:

  1. 正在加入的任务位于当前工作线程工作队列的顶部,在这种情况下,工作线程将继续执行该任务(见下文)

  2. 来自另一个工作线程的工作队列中的任务,但只有当该工作线程从当前工作线程中窃取任务时,当前工作线程才会窃取任务并执行它(见下文) )

  3. 对于第一种情况,我主要从ForkJoinTask.java中的doJoin()方法推断出它,下面是一个说明案例的工作测试:

    public static void main(String[] args){
        ForkJoinPool pool = new ForkJoinPool(2);
    
        ForkJoinTask task3 = ForkJoinTask.adapt(() -> {
            System.out.println("task 3 executing on thread " + Thread.currentThread());
            for(int i = 0; i < 10; ++i){
                System.out.println("task 3 doing work " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    
        ForkJoinTask task2 = ForkJoinTask.adapt(() -> {
            try {
                System.out.println("task 2 executing on thread " + Thread.currentThread());
                Thread.sleep(5000);
                System.out.println("task 2 finished");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        });
    
        ForkJoinTask task1 = ForkJoinTask.adapt(() -> {
            System.out.println("task 1 executing on thread " + Thread.currentThread());
            pool.submit(task3);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("task 1 joining task 3");
            task3.join();
            System.out.println("task 1 finished");
        });
    
        pool.submit(task2);
        pool.submit(task1);
    
        task1.join();
    }
    

    输出

    task 1 executing on thread Thread[ForkJoinPool-1-worker-2,5,main]
    task 2 executing on thread Thread[ForkJoinPool-1-worker-1,5,main]
    task 1 joining task 3
    task 3 executing on thread Thread[ForkJoinPool-1-worker-2,5,main]
    task 3 doing work 0
    task 3 doing work 1
    task 3 doing work 2
    task 3 doing work 3
    task 2 finished
    task 3 doing work 4
    task 3 doing work 5
    task 3 doing work 6
    task 3 doing work 7
    task 3 doing work 8
    task 3 doing work 9
    task 1 finished
    

    Task3和task1在同一个工作线程上执行,这是预期的,因为task3直接提交给thread2的工作队列,因此根据案例1,当task1调用join()时,应该执行它。

    我根据ForkJoinPool.java中的awaitJoin()方法推导出第二种情况,下面是一个说明案例的工作测试

    public static void main(String[] args){
        ForkJoinPool pool = new ForkJoinPool(2);
    
        ForkJoinTask task3 = ForkJoinTask.adapt(() -> {
            System.out.println("task 3 executing on thread " + Thread.currentThread());
            for(int i = 0; i < 10; ++i){
                System.out.println("task 3 doing work " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    
        ForkJoinTask task2 = ForkJoinTask.adapt(() -> {
            try {
                System.out.println("task 2 executing on thread " + Thread.currentThread());
                pool.submit(task3);
                Thread.sleep(5000);
                System.out.println("task 2 finished");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        });
    
        ForkJoinTask task1 = ForkJoinTask.adapt(() -> {
            System.out.println("task 1 executing on thread " + Thread.currentThread());
            pool.submit(task2);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("task 1 joining task 2");
            task2.join();
            System.out.println("task 1 finished");
        });
    
        pool.submit(task1);
    
        task1.join();
        task2.join();
        task3.join();
    }
    

    和输出

    task 1 executing on thread Thread[ForkJoinPool-1-worker-1,5,main]
    task 2 executing on thread Thread[ForkJoinPool-1-worker-2,5,main]
    task 1 joining task 2
    task 3 executing on thread Thread[ForkJoinPool-1-worker-1,5,main]
    task 3 doing work 0
    task 3 doing work 1
    task 3 doing work 2
    task 3 doing work 3
    task 2 finished
    task 3 doing work 4
    task 3 doing work 5
    task 3 doing work 6
    task 3 doing work 7
    task 3 doing work 8
    task 3 doing work 9
    task 1 finished
    
    当task1正在等待task2时,任务3在thread1上执行,这是可能的,因为task2被提交到thread1的工作队列,但由于thread2是空闲的,它偷了任务可以成为thread1的窃取者。当thread1看到来自task1的join()调用时,它会查看stealer(thread2)的工作队列并找到task3,接受并执行它。

    另请注意,task1仅在task3之后完成执行,这意味着一旦线程窃取了任务,它必须执行它才能完成。

    现在对于原始问题,我已经在非ForkJoinWorkerThread(主线程)中提交了task1和task2,因此工作线程中没有相互窃取,因此第二种情况不适用。此外,由于我在第二个任务(在thread2的工作队列中)调用了join(),因此第一个案例不适用,因此不会发生窃取。

    编辑: 这绝不是对java中F / J的回答,如果有任何问题请指出。事实上,挖掘所有这些细节只会产生更多问题:即为什么工作线程不会只执行任意任务并运行它?为什么它必须来自窃取者或自己的工作队列?如果你有答案,请发表评论/发帖。