[英]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.