ExecutorService + Future

0

Napisałem program, który tworzy trzy listy i sprawdza ile czasu to zajmuje. Wygląda tak:

public class Main {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        createThreeLists();
    }

    public static void createThreeLists() {
        long start = System.currentTimeMillis();

        List<String> converted1 = createList();
        List<String> converted2 = createList();
        List<String> converted3 = createList();

        long stop = System.currentTimeMillis();
        System.out.println("Time: " + (stop - start));
    }

    private static List<String> createList() {
        List<String> toReturn = new ArrayList<>();

        for (int i = 0; i < 10000000; i++)
            toReturn.add("heheszki");

        return toReturn;
    }
}

Wykonanie teko programu zajmuje na moim komputerze ~750ms

A tak wygląda program z użyciem ExecutorService i Future:

public class Main {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        createThreeArrays();
    }

    public static void createThreeArrays() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();

        ExecutorService executor = Executors.newFixedThreadPool(3);
        Future<List<String>> f1 = executor.submit(getCallable());
        Future<List<String>> f2 = executor.submit(getCallable());
        Future<List<String>> f3 = executor.submit(getCallable());

        List<String> converted1 = f1.get();
        List<String> converted2 = f2.get();
        List<String> converted3 = f3.get();

        long stop = System.currentTimeMillis();
        System.out.println("Time: " + (stop - start));
    }

    private static Callable<List<String>> getCallable() {
        Callable<List<String>> callable = () -> {
            List<String> toReturn = new ArrayList<>();

            for (int i = 0; i < 10000000; i++)
                toReturn.add("heheszki");

            return toReturn;
        };

        return callable ;
    }
}

Jego wykonanie zajmuje dwa razy więcej czasu.

Czy użycie Futurenie powinno przyspieszyć jego działania?

1

Czy użycie Futurenie powinno przyspieszyć jego działania?

Dlaczego ?

Zdajesz sobie sprawę że stworzenie executora, wątków kosztuje ?

0

Przy takim krótkim czasie to raczej zysku nie będzie bo overhead za duży. Odpal coś co sie liczy kilka minut.

0

Nic już tutaj mądrego nie dopowiem, bo koledzy praktycznie wyczerpali temat. Może chyba tylko tyle, że po to właśnie są Executory, żeby uniknąć overheadu tworzenia wątków, tylko je reużywać. A sam overhead na tworzenie wątków już sam widzisz w różnicy czasu dla drugiego przypadku.

2

U mnie podobnie się zachowuje. Wersja z Executorem działa 2 - 3 razy dłużej. Teoria z narzutem na tworzenie wątku jest trochę mało wiarygodna, bo jak zmniejszę rozmiar zadania 10x to czasy też zmniejszą się mniej więcej 10x, ale zostaje zachowana proporcja, że wersja z Executorem działa 2 - 3 razy dłużej. Czyli przy mniejszych licznikach pętli narzut na tworzenie wątku jest mniejszy? Wątpię.

Aktualizacja:
Moim zdaniem wąskim gardłem jest GC, a narzut na tworzenie wątku jest niewielki - pojedyncze milisekundy na co najwyżej.

Z ciekawości, zrobiłem programy testowe, które mniej obciążają GC - nie tworzą gigantycznych tablic:

public class Main1 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();

        int converted1 = createResult();
        int converted2 = createResult();
        int converted3 = createResult();

        System.out.println(converted1 + converted2 + converted3);

        long stop = System.currentTimeMillis();
        System.out.println("Time: " + (stop - start));
    }

    private static Integer createResult() {
        int result = 0;

        for (int i = 0; i < 12345678; i++) {
            result = (result * 5) + String.valueOf(i).hashCode();
        }

        return result;
    }
}
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;

public class Main2 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();

        ExecutorService executor = Executors.newFixedThreadPool(3);
        List<Future<Integer>> futures = executor.invokeAll(
                Arrays.asList(createResult(), createResult(), createResult()));

        int converted1 = futures.get(0).get();
        int converted2 = futures.get(1).get();
        int converted3 = futures.get(2).get();

        System.out.println(converted1 + converted2 + converted3);

        long stop = System.currentTimeMillis();
        System.out.println("Time: " + (stop - start));

        executor.shutdown();
    }

    private static Callable<Integer> createResult() {
        return () -> {
            int result = 0;

            for (int i = 0; i < 12345678; i++) {
                result = (result * 5) + String.valueOf(i).hashCode();
            }

            return result;
        };
    }
}

Teraz kod z Executorem jest znacznie szybszy od jednowątkowego.

0

Executor#invokeAll jest blokujący (czeka aż wszystkie zadania się wykonają), Executor#submit - nie, więc w oryginalnym teście program czeka najpierw na wykonanie pierwszego zadania, a może ono się wykonywać najdłużej (edit- w sumie na jedno wychodzi). Poza tym dobrze zrobić najpierw jakieś puste iteracje na rozgrzewkę. Nie podano też liczby rdzeni. W tym pierwszym teście sama alokacja pamięci pewnie jest wąskim gardłem.
Ps. O jakim narzucie piszecie, przecież tu jest jednorazowe utworzenie 3 wątków. I co ma robić GC, skoro wszystko jest "trzymane" do końca programu?

1

I co ma robić GC, skoro wszystko jest "trzymane" do końca programu?

  1. Generacyjny GC przerzuca obiekty między generacjami.
  2. ArrayLista podczas wzrostu tworzy nowe (coraz to większe) tablice do przechowywania danych - GC próbuje zawsze najpierw wrzucić nowy obiekt do jak najświeższej generacji, a to może go zachęcić do kolejnych odśmieceń.
  3. Obciążanie GC z wielu wątków jednocześnie może prowadzić do nieoczekiwanych konsekwencji - jak wykazały moje eksperymenty.

W ramach eksperymentu odpaliłem programy podane przez autora z G1 GC i wtedy działały z taką samą prędkością. Domyślny GC lepiej działał z wersją jednowątkową - taki sam rezultat jak podał autor.

0

Skoro już testujemy na staticach:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class Challenge001 {
    private static ExecutorService commonService = Executors.newFixedThreadPool(3);

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long sum = 0;
        long current;

        for(int i=0; i<5; i++){
            syncTest();
            asyncTest();
            createThreeListsAsyncCommonPool();
        }

        System.out.println("SyncTest");
        for(int i=0; i<10; i++){
            current = syncTest();
            sum = sum + current;

            System.out.println(i + "=" + current);
        }
        System.out.println("sum=" + sum);
        sum = 0;

        System.out.println("asyncTest");
        for(int i=0; i<10; i++){
            current = asyncTest();
            sum = sum + current;

            System.out.println(i + "=" + current);
        }
        System.out.println("sum=" + sum);
        sum = 0;

        System.out.println("asyncCommonTest");
        for(int i=0; i<10; i++){
            current = asyncCommonTest();
            sum = sum + current;

            System.out.println(i + "=" + current);
        }
        System.out.println("sum=" + sum);
        sum = 0;

    }

    public static long asyncCommonTest() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        createThreeListsAsyncCommonPool();

        long stop = System.currentTimeMillis();

        return (stop-start);
    }

    public static long syncTest(){
        long start = System.currentTimeMillis();
        createThreeListsSync();

        long stop = System.currentTimeMillis();

        return (stop-start);
    }

    public static long asyncTest() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        createThreeListsAsync();

        long stop = System.currentTimeMillis();

        return (stop-start);
    }

    public static void createThreeListsAsync() throws ExecutionException, InterruptedException {
        Callable<List<String>> callable = new ListCallable();
        ExecutorService service = Executors.newFixedThreadPool(3);
        Future<List<String>> f1 = service.submit(callable);
        Future<List<String>> f2 = service.submit(callable);
        Future<List<String>> f3 = service.submit(callable);

        f1.get();
        f2.get();
        f3.get();
    }

    public static void createThreeListsAsyncCommonPool() throws ExecutionException, InterruptedException {
        Callable<List<String>> callable = new ListCallable();
        Future<List<String>> f1 = commonService.submit(callable);
        Future<List<String>> f2 = commonService.submit(callable);
        Future<List<String>> f3 = commonService.submit(callable);

        f1.get();
        f2.get();
        f3.get();
    }

    public static void createThreeListsSync() {
        List<String> converted1 = createList();
        List<String> converted2 = createList();
        List<String> converted3 = createList();
    }

    private static List<String> createList() {
        List<String> toReturn = new ArrayList<>();

        for (int i = 0; i < 10000000; i++)
            toReturn.add("heheszki");

        return toReturn;
    }

    private static class ListCallable implements Callable<List<String>> {

        @Override
        public List<String> call() throws Exception {
            List<String> result = new ArrayList<>();
            for (int i = 0; i < 10000000; i++)
                result.add("heheszki");


            return result;
        }
    }
}

Okład na tworzenie wątków jest rzeczywiście względnie mały, czasami jak GC się odpali to i asyncCommonTest może być wolniejszy od asyncTesta. Tyle tylko, że oba są dużo szybsze od normalnej implementacji (u mnie średnio 900-1000 ms).

Przy czym NIE masz gwarancji - dużo może zależeć od runtime'u. Future ze swojej natury gwarantują współbieżność (concurrency), ale nie zrównoleglenie (parallel).

1 użytkowników online, w tym zalogowanych: 0, gości: 1