Praca asynchroniczna z wątkami Javy

Wszystkie aplikacje na Androida używają wątku głównego do obsługi operacji interfejsu użytkownika. Połączenia od dłuższego czasu Operacje wykonywane w tym wątku głównym mogą powodować blokady i brak odpowiedzi. Dla: na przykład jeśli aplikacja wysyła żądanie sieciowe z wątku głównego, interfejs aplikacji Urządzenie jest zablokowane do momentu otrzymania odpowiedzi sieciowej. Jeśli używasz Javy, tworzyć dodatkowe wątki w tle do obsługi długotrwałych operacji, wątki główne nadal obsługują aktualizacje interfejsu.

Z tego przewodnika dowiesz się, jak za pomocą języka programowania Java pula wątków (w języku angielskim) do konfigurowania i używania wielu wątków w aplikacji na Androida. Ten przewodnik pokazuje również, jak zdefiniować kod do uruchomienia w wątku i jak komunikować się między tymi wątkami a wątkiem głównym.

Biblioteki równoczesności

Znajomość podstaw tworzenia wątków i poznania podstaw ich działania jest niezbędna mechanizmów ochrony danych. Istnieje jednak wiele popularnych bibliotek, które oferują abstrakcyjne rozwiązania w zakresie tych koncepcji i gotowe do użycia narzędzia do przekazywania danych. między wątkami. Te biblioteki to m.in. Guava i RxJava dla użytkowników języka programowania Java i Coroutines, które zalecamy użytkownikom Kotlin.

W praktyce należy wybrać taki, który najlepiej sprawdza się w przypadku Twojej aplikacji ale zasady tworzenia wątków są takie same.

Przegląd przykładów

Zgodnie z Przewodnikiem po architekturze aplikacji przykłady w tym temacie sprawiają, i zwraca wynik do wątku głównego, w którym aplikacja może wyświetlić ten wynik na ekranie.

W szczególności funkcja ViewModel wywołuje warstwę danych w wątku głównym, uruchamiać żądanie sieciowe. Za przenoszenie danych odpowiada warstwa danych Wykonanie żądania sieciowego poza wątkiem głównym i publikowanie wyniku z powrotem do wątku głównego za pomocą wywołania zwrotnego.

Aby przenieść wykonanie żądania sieciowego z wątku głównego, musimy wykonać tworzyć inne wątki w naszej aplikacji.

Utwórz wiele wątków

Pula wątków to zarządzana grupa wątków, w której są uruchamiane zadania równolegle z kolejki. Nowe zadania są wykonywane w istniejących wątkach, ponieważ wątki stają się nieaktywne. Aby wysłać zadanie do puli wątków, użyj ExecutorService. Zwróć uwagę, że ExecutorService nie ma nic do zrobienia. z usługami, komponentem aplikacji na Androida.

Tworzenie wątków jest drogie, więc pulę wątków należy utworzyć tylko raz, po uruchomieniu aplikacji. Pamiętaj, aby zapisać instancję ExecutorService w klasie Application lub w kontenerze do wstrzykiwania zależności. Poniższy przykład tworzy pulę wątków składającą się z 4 wątków, której możemy użyć do uruchamianie zadań w tle.

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

Istnieją inne sposoby konfigurowania puli wątków w zależności od oczekiwanych zadań. Więcej informacji znajdziesz w artykule Konfigurowanie puli wątków.

Wykonaj w wątku w tle

wysłanie żądania sieciowego w wątku głównym powoduje oczekiwanie wątku lub block, aż otrzyma odpowiedź. Ponieważ wątek jest zablokowany, system operacyjny nie może onDraw() może spowodować zawieszenie się aplikacji, co może doprowadzić do wystąpienia błędu Application Not Okno odpowiadania (ANR). Przeprowadźmy tę operację w tle w wątku.

Prześlij prośbę

Najpierw spójrzmy na nasze zajęcia LoginRepository i zobaczmy, jak sobie radzą żądanie sieciowe:

// Result.java
public abstract class Result<T> {
    private Result() {}

    public static final class Success<T> extends Result<T> {
        public T data;

        public Success(T data) {
            this.data = data;
        }
    }

    public static final class Error<T> extends Result<T> {
        public Exception exception;

        public Error(Exception exception) {
            this.exception = exception;
        }
    }
}

// LoginRepository.java
public class LoginRepository {

    private final String loginUrl = "https://example.com/login";
    private final LoginResponseParser responseParser;

    public LoginRepository(LoginResponseParser responseParser) {
        this.responseParser = responseParser;
    }

    public Result<LoginResponse> makeLoginRequest(String jsonBody) {
        try {
            URL url = new URL(loginUrl);
            HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
            httpConnection.setRequestMethod("POST");
            httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            httpConnection.setRequestProperty("Accept", "application/json");
            httpConnection.setDoOutput(true);
            httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));

            LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
            return new Result.Success<LoginResponse>(loginResponse);
        } catch (Exception e) {
            return new Result.Error<LoginResponse>(e);
        }
    }
}

Działanie makeLoginRequest() jest synchroniczne i blokuje wątek wywołujący. Aby modelować na żądanie sieciowe, mamy własną klasę Result.

Wyślij żądanie

ViewModel wyzwala żądanie sieciowe, gdy użytkownik kliknie np. przycisk:

public class LoginViewModel {

    private final LoginRepository loginRepository;

    public LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody);
    }
}

Za pomocą poprzedniego kodu LoginViewModel blokuje wątek główny podczas tworzenia żądania sieciowe. Do przenoszenia danych możemy użyć puli wątków, której mamy utworzyć do wątku w tle.

Obsługa wstrzykiwania zależności

Najpierw, zgodnie z zasadami wstrzykiwania zależności, LoginRepository korzysta z instancji Executor, a nie ExecutorService, ponieważ wykonywanie kodu bez zarządzania wątkami:

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

Metoda execute() wykonawcy korzysta z elementu Runnable (Możliwe do uruchomienia). Runnable to Interfejs pojedynczej metody abstrakcyjnej (SAM) z metodą run(), która jest wykonywana wątek po wywołaniu.

Wykonuj w tle

Utwórzmy kolejną funkcję o nazwie makeLoginRequest(), która przenosi do wątku w tle i na razie ignoruje odpowiedź:

public class LoginRepository {
    ...
    public void makeLoginRequest(final String jsonBody) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
            }
        });
    }

    public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
        ... // HttpURLConnection logic
    }
    ...
}

W metodzie execute() tworzymy nowy blok Runnable z blokiem kodu które chcemy wykonywać w wątku działającym w tle – w naszym przypadku metody żądania. Wewnętrznie ExecutorService zarządza tymi funkcjami: Runnable oraz wykonuje je w dostępnym wątku.

co należy wziąć pod uwagę

Dowolny wątek w aplikacji, w tym wątek główny, może działać równolegle z innymi wątkami. , dlatego upewnij się, że kod jest bezpieczny. Zwróć uwagę, że w naszym Unikamy zapisywania do zmiennych współdzielonych między wątkami, za pomocą trwałych danych. To dobra metoda, ponieważ każdy wątek współpracuje z własnych instancji danych i unikamy złożoności synchronizacji.

Jeśli musisz udostępniać stan między wątkami, musisz zachować ostrożność podczas zarządzania dostępem z wątków za pomocą mechanizmów synchronizacji, takich jak blokady. To jest poza tego przewodnika. Ogólnie należy unikać udostępniania stanu zmiennego i przekazywać je między wątkami.

Komunikuj się z wątkiem głównym

W poprzednim kroku zignorowaliśmy odpowiedź na żądanie sieciowe. Aby wyświetlić wynik na ekranie, aplikacja LoginViewModel musi o tym wiedzieć. Możemy to zrobić za pomocą wywołań zwrotnych.

Funkcja makeLoginRequest() powinna przyjmować wywołanie zwrotne jako parametr, może zwrócić wartość asynchronicznie. Wywołanie zwrotne z wynikiem jest wywoływane za każdym razem, gdy żądanie sieciowe zakończy się lub wystąpi błąd. W Kotlin możemy użyj funkcji wyższego rzędu. Jednak w Javie musimy utworzyć nowe wywołanie zwrotne interfejs, aby mieć te same funkcje:

interface RepositoryCallback<T> {
    void onComplete(Result<T> result);
}

public class LoginRepository {
    ...
    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    callback.onComplete(result);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    callback.onComplete(errorResult);
                }
            }
        });
    }
  ...
}

ViewModel musi teraz zaimplementować wywołanie zwrotne. Może działać w różny sposób, w zależności od wyniku:

public class LoginViewModel {
    ...
    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
            @Override
            public void onComplete(Result<LoginResponse> result) {
                if (result instanceof Result.Success) {
                    // Happy path
                } else {
                    // Show error in UI
                }
            }
        });
    }
}

W tym przykładzie wywołanie zwrotne jest wykonywane w wątku wywołującym, który jest w wątku w tle. Oznacza to, że nie można modyfikować ani komunikować się bezpośrednio z warstwą interfejsu, dopóki nie wrócisz do wątku głównego.

Korzystanie z modułów obsługi

Za pomocą modułu obsługi możesz umieścić w kolejce działanie, które ma zostać wykonane na innym urządzeniu. w wątku. Aby określić wątek, którego dotyczy działanie, utwórz Handler za pomocą pętla w wątku. Looper to obiekt uruchamiany pętlę wiadomości w powiązanym wątku. Po utworzeniu Handler może następnie użyć metody post(Runnable), aby uruchomić blok kodu w odpowiedni wątek.

Looper zawiera funkcję pomocniczą getMainLooper(), która pobiera Looper w wątku głównym. Możesz uruchomić kod w wątku głównym za pomocą tego Looper, aby utworzyć element Handler. Jest to coś, co często robisz, możesz również zapisać wystąpienie Handler w tym samym miejscu, w którym ExecutorService:

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}

Warto wstrzyknąć moduł obsługi do repozytorium, ponieważ zapewnia to większą elastyczność. Na przykład w przyszłości możesz zechcieć przekazać witrynę Handler, aby zaplanować zadania w osobnym wątku. Jeśli zawsze jesteś komunikując się z tym samym wątkiem, możesz przekazać element Handler jak w przykładzie poniżej.

public class LoginRepository {
    ...
    private final Handler resultHandler;

    public LoginRepository(LoginResponseParser responseParser, Executor executor,
            Handler resultHandler) {
        this.responseParser = responseParser;
        this.executor = executor;
        this.resultHandler = resultHandler;
    }

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
    ...
}

Jeśli potrzebujesz większej elastyczności, możesz przekazać Handler każdej osobie, funkcja:

public class LoginRepository {
    ...

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler,
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback, resultHandler);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback, resultHandler);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
}

W tym przykładzie wywołanie zwrotne przekazywane do repozytorium makeLoginRequest repozytorium jest wykonywane w wątku głównym. Oznacza to, że możesz bezpośrednio modyfikować interfejs, z wywołania zwrotnego lub użyj interfejsu LiveData.setValue() do komunikacji z interfejsem.

Konfigurowanie puli wątków

Pulę wątków możesz utworzyć przy użyciu jednej z funkcji pomocniczych Wykonawca. ze wstępnie zdefiniowanymi ustawieniami, tak jak w poprzednim przykładowym kodzie. Ewentualnie Jeśli chcesz dostosować szczegóły puli wątków, możesz utworzyć za pomocą instancji ThreadPoolExecutor. Możesz skonfigurować te opcje szczegóły:

  • Początkowy i maksymalny rozmiar puli.
  • Zachowaj czas aktywności i jednostkę czasu. Czas utrzymywania aktywności to maksymalny czas, przez jaki wątek może pozostawać nieaktywny, zanim zostanie zamknięty.
  • Kolejka wejściowa zawierająca Runnable zadania. Ta kolejka musi implementować Interfejs BlockQueue. Aby spełnić wymagania aplikacji, możesz: wybrać jedną z dostępnych implementacji kolejek. Aby dowiedzieć się więcej, zobacz te zajęcia omówienie obiektu ThreadPoolExecutor.

Oto przykład, który określa rozmiar puli wątków na podstawie łącznej liczby rdzeni procesora, czas utrzymywania aktywności wynoszący jedną sekundę i kolejkę wejściową.

public class MyApplication extends Application {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();

    // Sets the amount of time an idle thread waits before terminating
    private static final int KEEP_ALIVE_TIME = 1;
    // Sets the Time Unit to seconds
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // Creates a thread pool manager
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    );
    ...
}