我怎样才能重写这个主线程 - 工作线程同步

时间:2018-04-11 00:12:46

标签: java concurrency parallel-processing synchronization java.util.concurrent

我有一个像这样的程序

public class Test implements Runnable
{
    public        int local_counter
    public static int global_counter
    // Barrier waits for as many threads as we launch + main thread
    public static CyclicBarrier thread_barrier = new CyclicBarrier (n_threads + 1);

    /* Constructors etc. */

    public void run()
    {
        for (int i=0; i<100; i++)
        {
            thread_barrier.await();
            local_counter = 0;
            for(int j=0 ; j = 20 ; j++)
                local_counter++;
            thread_barrier.await();
        }
    }

    public void main()
    {
        /* Create and launch some threads, stored on thread_array */
        for(int i=0 ; i<100 ; i++)
        {
            thread_barrier.await();
            thread_barrier.await();

            for (int t=1; t<thread_array.length; t++)
            {
                global_counter += thread_array[t].local_counter;
            }
        }
    }
}

基本上,我有几个带有自己的本地计数器的线程,我正在这样做(在一个循环中)

        |----|           |           |----|
        |main|           |           |pool|
        |----|           |           |----|
                         |

-------------------------------------------------------
barrier (get local counters before they're overwritten)
-------------------------------------------------------
                         |
                         |   1. reset local counter
                         |   2. do some computations
                         |      involving local counter
                         |
-------------------------------------------------------
             barrier (synchronize all threads)
-------------------------------------------------------
                         |
1. update global counter |
   using each thread's   |
   local counter         |

这应该都很好,花花公子,但事实证明这并不能很好地扩展。在16个物理节点集群上,6-8个线程之后的加速可以忽略不计,所以我必须摆脱其中一个等待。我已经尝试过可以扩展的CyclicBarrier,可以做多少的Semaphores,以及一个自定义库(jbarrier),它可以运行得很好,直到有比物理内核更多的线程,此时它的性能比顺序版本差。但是,如果不停止所有线程两次,我就无法想出办法。

编辑:虽然我很感谢您对我的计划中任何其他可能的瓶颈的所有见解和洞察力,但我正在寻找有关此特定问题的答案。如果需要,我可以提供更具体的例子

3 个答案:

答案 0 :(得分:3)

一些修复:假设你的线程数组[0]应该参与全局计数器总和,你对线程的迭代应该是(int t = 0; ...)。我们可以猜测它是一个测试数组,而不是线程。 local_counter应该是volatile,否则你可能看不到测试线程和主线程的真值。

好的,现在,你有一个适当的2阶段周期,afaict。其他任何东西,比如移相器或1个循环屏障,每个循环都有一个新的倒计时锁定,只是同一主题的变化:让众多线程同意让主要恢复,并让主要一次恢复多个线程。

更薄的实现可能涉及重入锁定,到达测试线程的计数器,在所有测试线程上恢复测试的条件以及恢复主线程的条件。当--count == 0时到达的测试线程应该发出主要恢复状态的信号。所有测试线程都等待测试恢复条件。主要应该在测试恢复条件下将计数器重置为N和signalAll,然后等待主要条件。线程(测试和主要)每个循环只等待一次。

最后,如果最终目标是由任何线程更新的总和,你应该看看LongAdder(如果不是AtomicLong)来执行长时间的添加,而不必停止所有线程(他们打架并添加,不涉及主要)。

否则,您可以让线程将其材料传递到main读取的阻塞队列。这样做有太多的味道;我很难理解为什么要挂起所有线程来收集数据。这就是全部。问题过于简单,我们没有足够的约束来证明你在做什么。

不要担心CyclicBarrier,它是通过可重入锁定,计数器和将signalAll()跳转到所有等待线程的条件来实现的。这是严密的编码,afaict。如果你想要无锁版本,你将面临浪费cpu时间的繁忙的自旋循环,特别是当你担心线程多于核心时进行扩展时。

与此同时,你有可能实际上有8个超线程的核心看起来像是16个cpu吗?

清理后,您的代码如下:

package tests;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.stream.Stream;

public class Test implements Runnable {
    static final int n_threads = 8;
    static final long LOOPS = 10000;
    public static int global_counter;
    public static CyclicBarrier thread_barrier = new CyclicBarrier(n_threads + 1);

    public volatile int local_counter;

    @Override
    public void run() {
        try {
            runImpl();
        } catch (InterruptedException | BrokenBarrierException e) {
            //
        }
    }

    void runImpl() throws InterruptedException, BrokenBarrierException {
        for (int i = 0; i < LOOPS; i++) {
            thread_barrier.await();
            local_counter = 0;
            for (int j=0; j<20; j++)
                local_counter++;
            thread_barrier.await();
        }
    }

    public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
        Test[] ra = new Test[n_threads];
        Thread[] ta = new Thread[n_threads];
        for(int i=0; i<n_threads; i++)
            (ta[i] = new Thread(ra[i]=new Test()).start();

        long nanos = System.nanoTime();
        for (int i = 0; i < LOOPS; i++) {
            thread_barrier.await();
            thread_barrier.await();

            for (int t=0; t<ra.length; t++) {
                global_counter += ra[t].local_counter;
            }
        }

        System.out.println(global_counter+", "+1e-6*(System.nanoTime()-nanos)+" ms");

        Stream.of(ta).forEach(t -> t.interrupt());
    }
}

我的带1锁的版本如下:

package tests;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Stream;

public class TwoPhaseCycle implements Runnable {
    static final boolean DEBUG = false;
    static final int N = 8;
    static final int LOOPS = 10000;

    static ReentrantLock lock = new ReentrantLock();
    static Condition testResume = lock.newCondition();
    static volatile long cycle = -1;
    static Condition mainResume = lock.newCondition();
    static volatile int testLeft = 0;

    static void p(Object msg) {
        System.out.println(Thread.currentThread().getName()+"] "+msg);
    }

    //-----
    volatile int local_counter;

    @Override
    public void run() {
        try {
            runImpl();
        } catch (InterruptedException e) {
            p("interrupted; ending.");
        }
    }

    public void runImpl() throws InterruptedException {
        lock.lock();
        try {
            if(DEBUG) p("waiting for 1st testResumed");
            while(cycle<0) {
                testResume.await();
            }
        } finally {
            lock.unlock();
        }

        long localCycle = 0;//for (int i = 0; i < LOOPS; i++) {
        while(true) {
            if(DEBUG) p("working");
            local_counter = 0;
            for (int j = 0; j<20; j++)
                local_counter++;
            localCycle++;

            lock.lock();
            try {
                if(DEBUG) p("done");
                if(--testLeft <=0)
                    mainResume.signalAll(); //could have been just .signal() since only main is waiting, but safety first.

                if(DEBUG) p("waiting for cycle "+localCycle+" testResumed");
                while(cycle < localCycle) {
                    testResume.await();
                }
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TwoPhaseCycle[] ra = new TwoPhaseCycle[N];
        Thread[] ta = new Thread[N];
        for(int i=0; i<N; i++)
            (ta[i] = new Thread(ra[i]=new TwoPhaseCycle(), "\t\t\t\t\t\t\t\t".substring(0, i%8)+"\tT"+i)).start();

        long nanos = System.nanoTime();

        int global_counter = 0;
        for (int i=0; i<LOOPS; i++) {
            lock.lock();
            try {
                if(DEBUG) p("gathering");
                for (int t=0; t<ra.length; t++) {
                    global_counter += ra[t].local_counter;
                }
                testLeft = N;
                cycle = i;
                if(DEBUG) p("resuming cycle "+cycle+" tests");
                testResume.signalAll();

                if(DEBUG) p("waiting for main resume");
                while(testLeft>0) {
                    mainResume.await();
                }
            } finally {
                lock.unlock();
            }
        }

        System.out.println(global_counter+", "+1e-6*(System.nanoTime()-nanos)+" ms");

        p(global_counter);
        Stream.of(ta).forEach(t -> t.interrupt());
    }
}

当然,这绝不是一个稳定的微基准,但趋势表明它更快。希望你喜欢。 (我放弃了一些最喜欢的调试技巧,值得调试真......)

答案 1 :(得分:2)

好。我不确定完全理解,但我认为你的主要问题是你试图重新使用一组预定义的线程。你应该让Java来处理这个问题(这就是executors / fork-join池的用途)。要解决您的问题,拆分/进程/合并(或map / reduce)似乎对我来说是合适的。从java 8开始,它是一种非常简单的实现方法(感谢stream / fork-join池/可完成的未来API)。我在这里提出两种选择:

Java 8 Stream

对我来说,你的问题似乎可以恢复到地图/减少问题。如果您可以使用Java 8流,则可以将性能问题委派给它。我要做什么:
 1.创建一个并行流,包含您的处理输入(您甚至可以使用方法动态生成输入)。请注意,您可以实现自己的Spliterator,以完全控制输入的浏览和分割(网格上的单元格?)。  2.使用地图处理输入  3.使用reduce方法合并所有先前计算的结果。

简单示例(基于您的示例):

// Create a pool with wanted number of threads
    final ForkJoinPool pool = new ForkJoinPool(4);
    // We give the entire procedure to the thread pool
    final int result = pool.submit(() -> {
        // Generate a hundred counters, initialized on 0 value
        return IntStream.generate(() -> 0)
                .limit(100)
                // Specify we want it processed in a parallel way
                .parallel()
                // The map will register processing method
                .map(in -> incrementMultipleTimes(in, 20))
                // We ask the merge of processing results
                .reduce((first, second) -> first + second)
                .orElseThrow(() -> new IllegalArgumentException("Empty dataset"));
    })
            // Wait for the overall result
            .get();

    System.out.println("RESULT: " + result);

    pool.shutdown();
    pool.awaitTermination(10, TimeUnit.SECONDS);

有些事情需要注意:
 1.默认情况下,并行流在JVM公共fork-join池上执行任务,这可能会限制执行程序的数量。但是有办法使用你自己的游泳池:see this answer  2.如果配置良好,我认为这是最好的方法,因为并行逻辑已由JDK开发人员自己处理。

移相

如果您不能使用java8功能(或者我误解了您的问题,或者您真的想自己处理低级别管理),我可以给您的最后一条线索是:Phaser对象。 正如文档中所述,它是循环屏障和倒计时锁存器的可重复使用的混合。我多次使用它。使用它是一件复杂的事情,但它也非常强大。它可以用作循环障碍,所以我认为它适合你的情况。

答案 2 :(得分:0)

你真的可以考虑关注其(CyclicBarrierdocumentation中的“官方”示例:

 class Solver {
   final int N;
   final float[][] data;
   final CyclicBarrier barrier;

   class Worker implements Runnable {
     int myRow;
     Worker(int row) { myRow = row; }
     public void run() {
       while (!done()) {
         processRow(myRow);

         try {
           barrier.await();
         } catch (InterruptedException ex) {
           return;
         } catch (BrokenBarrierException ex) {
           return;
         }
       }
     }
   }

   public Solver(float[][] matrix) {
     data = matrix;
     N = matrix.length;
     barrier = new CyclicBarrier(N,
                                 new Runnable() {
                                   public void run() {
                                     mergeRows(...);
                                   }
                                 });
     for (int i = 0; i < N; ++i)
       new Thread(new Worker(i)).start();

     waitUntilDone();
   }
 }

在你的情况下

  • processRow()将生成部分生成(任务分为N个部分,工作人员可以在初始化时获取其编号,或者只使用barrier.await()返回的数字(在这种情况下是工作人员)应以等待开始)
  • mergeRows(),在建造时传递到屏障的匿名Runnable,是整整一代准备就绪的地方,你可以在屏幕上打印它或者什么东西(也许交换一些'currentGen '和'nextGen'缓冲区)。当此方法返回时(或者更准确地说是run()),工作程序中的barrier.await()调用也会返回并开始计算下一代(或者不是,请参阅下一个项目符号点)
  • done()决定线程何时退出(而不是生成新一代)。它可以是一个“真正的”方法,但static volatile boolean变量也可以使用
  • waitUntilDone()可能是所有线程的循环,join() - 它们。或者只是在程序退出时等待你可以触发的东西(来自'mergeRows')