簡體   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