자바 스레드를 사용한 비동기 작업

모든 Android 앱은 기본 스레드를 사용하여 UI 작업을 처리합니다. 장기 실행 통화 이 메인 스레드에서 작업을 실행하면 정지되고 응답하지 않을 수 있습니다. 대상 예를 들어 앱이 기본 스레드에서 네트워크를 요청하면 앱의 UI는 동결됩니다. Java를 사용하는 경우 추가 백그라운드 스레드를 만들어 장기 실행 작업을 처리하는 동안 기본 스레드가 UI 업데이트를 계속 처리합니다.

이 가이드에서는 Java 프로그래밍 언어를 사용하는 개발자가 스레드 풀을 사용하여 Android 앱에서 여러 스레드를 설정하고 사용할 수 있습니다. 이 가이드 또한 스레드에서 실행할 코드를 정의하는 방법과 기본 스레드 간에 오가는 것을 방지할 수 있습니다.

동시 실행 라이브러리

스레딩의 기본사항 및 기본 메커니즘을 이해하는 것이 중요합니다. 그러나 더 높은 수준의 성능을 제공하는 인기 있는 라이브러리가 많이 있습니다. 데이터를 전달하는 데 사용할 수 있는 유틸리티와 이러한 개념에 대한 추상화를 할 수 있습니다. 이러한 라이브러리에는 Guava 및 Java 프로그래밍 언어 사용자 및 코루틴을 위한 RxJava 이는 Kotlin 사용자에게 권장됩니다.

실제로는 자신의 앱에 가장 적합한 방식을 선택해야 합니다. 개발팀을 갖출 수 있지만 스레딩 규칙은 동일하게 유지됩니다.

예시 개요

앱 아키텍처 가이드를 기반으로 하는 이 주제의 예에서는 기본 스레드에 결과를 반환합니다. 그러면 앱이 기본 스레드를 통해 화면에 그 결과를 표시할 수 있습니다.

특히 ViewModel는 기본 스레드의 데이터 레이어를 호출하여 네트워크 요청을 트리거합니다 데이터 레이어는 기본 스레드 외부에서 네트워크 요청을 실행하고 결과를 다시 게시 기본 스레드에 전달됩니다.

네트워크 요청 실행을 기본 스레드 외부로 이동하려면 앱에 다른 스레드를 생성할 수 있습니다.

여러 스레드 만들기

스레드 풀은 작업을 실행하는 스레드의 관리형 컬렉션입니다. 병렬 처리되도록 해야 합니다 새 작업은 기존 스레드에서와 같이 실행됩니다. 스레드가 유휴 상태가 됩니다. 작업을 스레드 풀로 보내려면 다음을 사용합니다. ExecutorService 인터페이스 ExecutorService는 아무것도 할 필요가 없습니다. Android 애플리케이션 구성요소인 서비스를 사용합니다.

스레드를 만드는 것은 비용이 많이 들므로 스레드 풀을 한 번만 생성하면 됩니다. 앱이 초기화됩니다. ExecutorService의 인스턴스를 저장해야 합니다. Application 클래스 또는 종속 항목 삽입 컨테이너에서 찾을 수 있습니다. 다음 예에서는 4개의 스레드로 구성된 스레드 풀을 만들어 사용하는 데 사용할 수 있습니다. 백그라운드 작업을 실행할 수 있습니다

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

예상에 따라 스레드 풀을 구성할 수 있는 다른 방법이 있습니다. 워크로드도 사용할 수 있습니다 자세한 내용은 스레드 풀 구성을 참조하세요.

백그라운드 스레드에서 실행

기본 스레드에서 네트워크 요청을 하면 스레드가 대기합니다. 차단됩니다. 스레드가 차단되었기 때문에 OS는 onDraw()를 호출하면 앱이 정지되어 애플리케이션이 응답 (ANR) 대화상자 대신 백그라운드에서 이 작업을 실행해 보겠습니다. 스레드가 필요합니다.

요청하기

먼저 LoginRepository 클래스를 살펴보고 다음과 같습니다.

// 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);
        }
    }
}

makeLoginRequest()가 동기식이며 호출 스레드를 차단합니다. 이를 모델링하기 위해 자체 Result 클래스가 있습니다.

요청 트리거

ViewModel는 사용자가 탭할 때(예: 버튼:

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);
    }
}

이전 코드에서 LoginViewModel는 생성할 때 기본 스레드를 차단하고 있습니다. 사용할 수 있습니다. 인스턴스화한 스레드 풀을 사용하여 백그라운드 스레드에 전달합니다.

종속 항목 삽입 처리

먼저 종속 항목 삽입 원칙에 따라 LoginRepository ExecutorService가 아닌 Executor의 인스턴스를 사용합니다. 코드를 실행하고 스레드를 관리하지 않음:

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

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

Executor의 execute() 메서드는 Runnable을 사용합니다. Runnable는 단일 추상 메서드 (SAM) 인터페이스에서 실행되는 run() 메서드와 스레드를 호출합니다.

백그라운드에서 실행

실행을 백그라운드 스레드로 이동하고 현재 응답을 무시하는 makeLoginRequest()라는 또 다른 함수를 만들어 보겠습니다.

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
    }
    ...
}

execute() 메서드 내에서 코드 블록으로 새 Runnable를 만듭니다. 백그라운드 스레드에서 실행하려고 합니다. 이 경우에는 동기 네트워크가 요청 메서드를 사용할 수 있습니다. 내부적으로 ExecutorServiceRunnable를 관리하고 사용 가능한 스레드에서 실행합니다

고려사항

앱의 모든 스레드는 기본 스레드를 포함하여 다른 스레드와 동시에 실행될 수 있습니다. 코드가 스레드로부터 안전한지 확인해야 합니다. 스레드 간에 공유되는 변수에 쓰는 것을 피하고 변경할 수 없는 데이터를 대신 사용하는 것이 좋습니다 이는 각 스레드가 동기화의 복잡성을 피할 수 있습니다.

스레드 간에 상태를 공유해야 하는 경우 액세스 관리에 주의해야 합니다. 스레드로부터 요청을 받습니다. 다음의 외부입니다. 자세히 알아보도록 하겠습니다. 일반적으로 변경 가능한 상태는 공유하지 않는 것이 좋습니다. 할 수 있습니다.

기본 스레드와 통신

이전 단계에서는 네트워크 요청 응답을 무시했습니다. 이 LoginViewModel에서 알아야 합니다. 이를 위해서는 콜백을 사용합니다.

makeLoginRequest() 함수는 콜백을 매개변수로 사용해야 합니다. 값을 비동기식으로 반환할 수 있습니다. 결과를 포함하는 콜백은 네트워크 요청이 완료되거나 실패할 때마다 Kotlin에서는 고차 함수를 사용합니다. 그러나 Java에서는 새 콜백을 만들어야 합니다. 인터페이스를 사용하여 동일한 기능을 갖도록 합니다.

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은 이제 콜백을 구현해야 합니다. 그리고 다음과 같이 결과에 따라 서로 다른 로직을 실행할 수 있습니다.

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
                }
            }
        });
    }
}

이 예에서 콜백은 호출 스레드인 백그라운드 스레드에 한합니다. 즉, 새 버전의 API로 직접 수정하거나 통신할 수 없으며 기본 스레드로 다시 전환할 때까지 UI 레이어에 연결된 상태를 유지합니다.

핸들러 사용

Handler를 사용하여 다른 위치에서 수행할 작업을 대기열에 추가할 수 있습니다. 스레드가 필요합니다. 작업을 실행할 스레드를 지정하려면 스레드의 Looper를 사용하는 Handler입니다. Looper는 메시지 루프를 반환합니다. Handler를 만든 후에는 다음을 할 수 있습니다. 그런 다음 post(Runnable) 메서드를 사용하여 표시됩니다.

Looper getMainLooper()는 기본 스레드의 Looper입니다. 다음을 사용하여 기본 스레드에서 코드를 실행할 수 있습니다. Looper: Handler를 만듭니다. 자주 하는 일이므로 Handler의 인스턴스를 ExecutorService:

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

핸들러를 저장소에 삽입하는 것이 좋습니다. 더 유연하게 관리할 수 있습니다 예를 들어 나중에는 서로 다른 Handler를 사용하여 별도의 스레드에서 작업을 예약합니다. 항상 동일한 스레드로 다시 통신하려면 Handler를 저장소 생성자에 전달해야 합니다.

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);
            }
        });
    }
    ...
}

또는 더 많은 유연성을 원하는 경우 Handler를 각 객체에 전달할 수 있습니다. 함수:

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);
            }
        });
    }
}

이 예에서는 저장소의 makeLoginRequest 호출에 전달된 콜백이 기본 스레드에서 실행됩니다. 즉, UI의 UI를 직접 수정하여 콜백에서 호출하거나 LiveData.setValue()를 사용하여 UI와 통신합니다.

스레드 풀 구성

Executor 도우미 함수 중 하나를 사용하여 스레드 풀을 만들 수 있습니다. 사전 정의된 설정으로 사용할 수 있습니다. 이와 달리 스레드 풀의 세부정보를 맞춤설정하려면 ThreadPoolExecutor를 직접 사용하여 인스턴스 다음을 구성할 수 있습니다. 세부정보:

  • 초기 및 최대 풀 크기
  • 연결 유지 시간 및 시간 단위. 연결 유지 시간은 종료되기 전에 유휴 상태로 유지될 수 있습니다.
  • Runnable 작업을 보유하는 입력 큐. 이 큐는 BlockingQueue 인터페이스 앱의 요구사항에 맞도록 다음과 같은 방법을 사용할 수 있습니다. 사용할 수 있는 큐 구현 중에서 선택합니다. 자세한 내용은 ThreadPoolExecutor의 개요

다음은 애플리케이션의 총 개수에 따라 스레드 풀 크기를 지정하는 1초의 연결 유지 시간, 입력 큐가 있습니다.

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
    );
    ...
}