繁体   English   中英

我该如何重写此主线程-工作线程同步

[英]How can I rewrite this main thread - worker threads synchronization

我有一个程序,像这样

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),该库工作得很好,直到线程多于物理核心为止,此时它的性能比顺序版本差。 但是我只是想出了一种方法,不停止所有线程两次。

编辑:尽管我很感谢您对我的程序中任何其他可能存在的瓶颈的所有见解,但我正在寻找有关此特定问题的答案。 如果需要,我可以提供一个更具体的示例

一些修复:假设您的线程数组[0]应该参与全局计数器总和,那么您在线程上的迭代应该是for(int t = 0; ...)。 我们可以猜到它是一个Test数组,而不是线程。 local_counter应该是易失的,否则您可能看不到测试线程和主线程之间的真实值。

好的,现在,您有一个适当的2个周期,当然。 移相器或1个循环障碍物(在每个循环中都有新的倒数锁存器)之类的其他东西都只是同一主题的变体:让多个线程同意让主线程恢复,让主线程一次恢复多个线程。

较薄的实现可能涉及reentrantlock,到达的测试线程的计数器,在所有测试线程上恢复测试的条件以及恢复主线程的条件。 当--count == 0时到达的测试线程应发出主要恢复条件的信号。 所有测试线程都在等待测试恢复条件。 主机应在测试恢复条件下将计数器重置为N并用信号all,然后在主机条件下等待。 每个循环中的线程(测试线程和主线程)仅等待一次。

最后,如果最终目标是由任何线程更新的总和,则应查看LongAdder(如果不是AtomicLong)以一致的方式执行加法运算,而不必停止所有线程(它们相互竞争并加法,不涉及主线程)。

否则,您可以让线程将其材料传递到主线程读取的阻塞队列中。 这样做的味道太多了。 我很难理解为什么要挂起所有线程来收集数据。 仅此而已,问题被简单化了,我们没有足够的约束来证明您在做什么。

不用担心CyclicBarrier,它是通过可重入锁,计数器和将signalAll()触发所有等待线程的条件实现的。 这是紧密编码的,毫无疑问。 如果要使用无锁版本,将面临太多忙碌的自旋循环,浪费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());
    }
}

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

好。 我不确定是否完全理解,但是我认为您的主要问题是您尝试过多地使用预定义的线程集。 您应该让Java来解决这个问题(这就是执行程序/ fork-join池的作用)。 为了解决您的问题,拆分/处理/合并(或映射/缩小)对我来说似乎很合适。 从Java 8开始,这是一种非常简单的实现方法(感谢stream / fork-join池/可完成的将来API)。 我在这里提出2种替代方法:

Java 8流

对我来说,您的问题似乎可以恢复为映射/归约问题。 并且,如果可以使用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 Common fork-join池上执行任务,执行者数量可能受到限制。 但是有使用自己的池的方法: 请参阅此答案
2.如果配置合理,我认为这是最好的方法,因为JDK开发人员已自行处理了并行逻辑。

移相器

如果您不能使用java8功能(或者我误解了您的问题,或者您真的想亲自处理低级管理),那么我可以给您的最后一条线索是: Phaser对象。 如文档所述,它是循环屏障和倒数锁存器的可重用组合。 我已经使用了多次。 使用起来很复杂,但是功能也非常强大。 它可以用作循环屏障,所以我认为它适合您的情况。

您可以真正考虑遵循其( CyclicBarrier文档中的“官方”示例:

 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()返回的数字(在这种情况下,工作人员应以await开始)
  • mergeRows() (在传递给构造函数的屏障中的匿名Runnable中)是整代产品准备就绪的地方,您可以在屏幕上或其他内容上打印它(并交换一些“ currentGen”和“ nextGen”缓冲区)。 当此方法返回(或更确切地说, run() )时,workers中的barrier.await()调用也返回,并开始计算下一代(或不,请参见下一个要点)
  • done()决定何时退出线程(而不是产生新一代线程)。 它可以是“真实”方法,但static volatile boolean变量也可以使用
  • waitUntilDone()可能是所有线程的循环, join()它们进行处理。 或者只是等待程序退出时可以触发的内容(来自“ mergeRows”)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM