[英]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流,则可以将性能问题委托给它。 我该怎么做:
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.