Java iş parçacıklarıyla eşzamansız çalışma

Tüm Android uygulamaları, kullanıcı arayüzü işlemlerini gerçekleştirmek için bir ana iş parçacığı kullanır. Bu ana iş parçacığından uzun süreli işlemlerin çağrılması, donmaya ve yanıt vermemeye neden olabilir. Örneğin, uygulamanız ana iş parçacığından ağ isteği yaparsa uygulamanızın kullanıcı arayüzü, ağ yanıtını alana kadar dondurulur. Java kullanıyorsanız ana iş parçacığı kullanıcı arayüzü güncellemelerini işlemeye devam ederken uzun süreli işlemleri işlemek için ek arka plan iş parçacıkları oluşturabilirsiniz.

Bu kılavuzda, Java Programlama Dili kullanan geliştiricilerin Android uygulamasında birden fazla iş parçacığı oluşturmak ve kullanmak için ileti dizisi havuzu nasıl kullanabileceği gösterilmektedir. Bu kılavuzda, iş parçacığı üzerinde çalışacak kodun nasıl tanımlanacağı ve bu iş parçacığından biri ile ana iş parçacığı arasında nasıl iletişim kurulacağı da gösterilmektedir.

Eşzamanlılık kitaplıkları

İş parçacığı işlemenin temellerini ve temel mekanizmalarını anlamak önemlidir. Bununla birlikte, bu kavramlar üzerinden daha üst düzey soyutlamalar sunan ve iş parçacıkları arasında veri aktarmak için kullanıma hazır yardımcı programlar sunan birçok popüler kitaplık vardır. Bu kitaplıklar arasında, Java Programlama Dili kullanıcıları için Guava ve RxJava ile Kotlin kullanıcıları için önerdiğimiz Coroutines yer alır.

İş parçacığı kuralları aynı kalsa da pratikte uygulamanız ve geliştirme ekibiniz için en uygun olanı seçmeniz gerekir.

Örneklere genel bakış

Uygulama mimarisi rehberine bağlı olarak bu konudaki örnekler bir ağ isteğinde bulunur ve sonucu ana iş parçacığına döndürür. Uygulama, bu sonucu ekranda gösterebilir.

Daha ayrıntılı olarak belirtmek gerekirse, ViewModel, ağ isteğini tetiklemek için ana iş parçacığındaki veri katmanını çağırır. Veri katmanı, ağ isteğinin yürütülmesini ana iş parçacığının dışına taşımak ve bir geri çağırma kullanarak sonucu ana iş parçacığına geri göndermekten sorumludur.

Ağ isteğinin yürütülmesini ana iş parçacığının dışına taşımak için uygulamamızda başka iş parçacıkları oluşturmamız gerekir.

Birden fazla iş parçacığı oluşturma

İleti dizisi havuzu, görevleri bir sıradan paralel olarak çalıştıran iş parçacıklarından oluşan yönetilen bir koleksiyondur. Yeni görevler, bu iş parçacıkları boşta kaldıkça mevcut iş parçacıklarında yürütülür. İş parçacığı havuzuna görev göndermek için ExecutorService arayüzünü kullanın. ExecutorService öğesinin Android uygulama bileşeni olan Hizmetler ile hiçbir ilgisi yoktur.

İş parçacıkları oluşturmak pahalı olduğundan, uygulamanız başlatılırken yalnızca bir kez iş parçacığı havuzu oluşturmanız gerekir. ExecutorService örneğini Application sınıfınıza veya bir bağımlılık ekleme kapsayıcısına kaydettiğinizden emin olun. Aşağıdaki örnek, arka plan görevlerini çalıştırmak için kullanabileceğimiz dört iş parçacığı içeren bir iş parçacığı havuzu oluşturur.

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

Beklenen iş yüküne bağlı olarak bir iş parçacığı havuzunu yapılandırmanın başka yolları da vardır. Daha fazla bilgi için İleti dizisi havuzunu yapılandırma sayfasına bakın.

Arka plandaki bir iş parçacığında çalıştır

Ana iş parçacığında bir ağ isteği yapmak, iş parçacığının yanıt alana kadar beklemesine veya engellenmesine neden olur. İş parçacığı engellendiği için işletim sistemi onDraw() çağıramaz ve uygulamanız donar. Bu da Uygulama Yanıt Vermiyor (ANR) iletişim kutusuna neden olabilir. Bunun yerine, bu işlemi bir arka plan iş parçacığında çalıştıralım.

İsteğinde bulunma

İlk olarak LoginRepository sınıfımıza göz atalım ve ağ isteğini nasıl yaptıklarına bakalım:

// 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() eşzamanlı ve çağrıda bulunan mesaj dizisini engelliyor. Ağ isteğinin yanıtını modellemek için kendi Result sınıfımız vardır.

İsteği tetikleyin

ViewModel, kullanıcı aşağıdaki gibi bir düğmeye dokunduğunda ağ isteğini tetikler:

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

Önceki kodda LoginViewModel, ağ isteğinde bulunurken ana iş parçacığını engeller. Yürütmeyi bir arka plan iş parçacığına taşımak için örneklediğimiz iş parçacığı havuzunu kullanabiliriz.

Bağımlılık yerleştirme işlemini işleme

Öncelikle, bağımlılık ekleme ilkelerine uygun olarak LoginRepository, kodu yürüttüğü ve iş parçacıklarını yönetmediği için ExecutorService yerine Yürütücü örneğini alır:

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

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

Yürütücü'nün execute() yöntemi bir Çalıştırılabilir alır. Runnable, çağrıldığında bir iş parçacığı içinde yürütülen run() yöntemine sahip bir Tekli Soyut Yöntem (SAM) arayüzüdür.

Arka planda yürütün

Yürütmeyi arka plan iş parçacığına taşıyan ve şimdilik yanıtı yoksayan makeLoginRequest() adlı başka bir işlev oluşturalım:

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() yönteminde, arka plan iş parçacığında çalıştırmak istediğimiz kod bloğuyla (bizim örneğimizde eşzamanlı ağ isteği yöntemi) yeni bir Runnable oluşturuyoruz. ExecutorService, dahili olarak Runnable politikasını yönetir ve mevcut bir iş parçacığında yürütür.

Dikkat edilmesi gereken noktalar

Uygulamanızdaki tüm iş parçacıkları, ana iş parçacığı da dahil olmak üzere diğer iş parçacıklarına paralel olarak çalışabilir. Bu nedenle, kodunuzun iş parçacığı için güvenli olduğundan emin olmanız gerekir. Örneğimizde, iş parçacıkları arasında paylaşılan değişkenlere yazmaktan, bunun yerine sabit veriler iletmekten kaçındığımıza dikkat edin. Her iş parçacığı kendi veri örneğiyle çalıştığı ve senkronizasyon karmaşıklığından kaçındığımız için bu iyi bir uygulamadır.

İleti dizileri arasında durum paylaşmanız gerekiyorsa ileti dizilerinden erişimi, kilitler gibi senkronizasyon mekanizmalarını kullanarak yönetmeye dikkat etmelisiniz. Bu, bu kılavuzun kapsamı dışındadır. Genel olarak, mümkün olduğunda iş parçacıkları arasında değişken durumu paylaşmaktan kaçınmanız gerekir.

Ana ileti dizisiyle iletişim kurma

Önceki adımda, ağ isteği yanıtını yoksaydık. Sonucu ekranda görüntülemek için LoginViewModel ürününün bunu bilmesi gerekir. Bunu geri çağırmaları kullanarak yapabiliriz.

makeLoginRequest() işlevinin, eşzamansız olarak değer döndürebilmesi için bir geri çağırmayı parametre olarak alması gerekir. Ağ isteği tamamlandığında veya bir hata meydana geldiğinde sonuçla birlikte geri çağırma çağrılır. Kotlin'de daha yüksek düzeyli bir işlev kullanabiliriz. Ancak Java'da aynı işleve sahip olmak için yeni bir geri çağırma arayüzü oluşturmamız gerekir:

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 işlevinin geri çağırmayı şimdi uygulaması gerekiyor. Sonuca bağlı olarak farklı mantık yürütebilir:

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

Bu örnekte, geri çağırma bir arka plan iş parçacığı olan çağrı iş parçacığında yürütülür. Bu, ana iş parçacığına geri dönene kadar kullanıcı arayüzü katmanını değiştiremeyeceğiniz veya doğrudan iletişim kuramayacağınız anlamına gelir.

İşleyicileri kullan

Farklı bir iş parçacığında gerçekleştirilecek işlemi sıraya almak için İşleyici kullanabilirsiniz. İşlemin gerçekleştirileceği iş parçacığını belirtmek üzere Handler öğesini, iş parçacığı için bir Looper kullanarak oluşturun. Looper, ilişkili bir iş parçacığı için mesaj döngüsünü çalıştıran bir nesnedir. Handler oluşturduktan sonra karşılık gelen iş parçacığında bir kod bloğu çalıştırmak için post(Runnable) yöntemini kullanabilirsiniz.

Looper, ana iş parçacığının Looper değerini alan bir yardımcı işlev (getMainLooper()) içerir. Handler oluşturmak için bu Looper öğesini kullanarak ana iş parçacığında kod çalıştırabilirsiniz. Bu işlemi sıkça yaptığınız için Handler öğesinin bir örneğini ExecutorService dosyasını kaydettiğiniz yere kaydedebilirsiniz:

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

İşleyiciyi depoya eklemek size daha fazla esneklik sağladığından iyi bir uygulamadır. Örneğin, görevleri ayrı bir iş parçacığında planlamak için ileride farklı bir Handler iletebilirsiniz. Her zaman aynı iş parçacığına tekrar iletişim kuruyorsanız Handler öğesini, aşağıdaki örnekte gösterildiği gibi depo oluşturucuya aktarabilirsiniz.

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

Alternatif olarak, daha fazla esneklik istiyorsanız her işleve bir Handler aktarabilirsiniz:

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

Bu örnekte, Repository'nin makeLoginRequest çağrısına iletilen geri çağırma ana iş parçacığında yürütülür. Bu, kullanıcı arayüzünü doğrudan geri çağırmadan değiştirebileceğiniz veya kullanıcı arayüzüyle iletişim kurmak için LiveData.setValue() öğesini kullanabileceğiniz anlamına gelir.

İş parçacığı havuzunu yapılandırma

Önceki örnek kodda gösterildiği gibi, önceden tanımlanmış ayarlarla Yürütücü yardımcı işlevlerinden birini kullanarak bir iş parçacığı havuzu oluşturabilirsiniz. Alternatif olarak, iş parçacığı havuzunun ayrıntılarını özelleştirmek isterseniz doğrudan ThreadPoolExecutor öğesini kullanarak bir örnek oluşturabilirsiniz. Aşağıdaki ayrıntıları yapılandırabilirsiniz:

  • Başlangıç ve maksimum havuz boyutu.
  • Hayatta kalma süresini ve zaman birimini koruyun. Canlı tutma süresi, bir iş parçacığının kapanmadan önce boşta kalabileceği maksimum süredir.
  • Runnable görevi içeren bir giriş sırası. Bu sıra, BlockQueue arayüzünü uygulamalıdır. Uygulamanızın gereksinimlerini karşılamak için mevcut sıra uygulamaları arasından seçim yapabilirsiniz. Daha fazla bilgi edinmek için ThreadPoolExecutor için sınıfa genel bakışı inceleyin.

Toplam işlemci çekirdeği sayısına, bir saniyelik "canlı tutma süresine" ve bir giriş sırasına göre iş parçacığı havuzu boyutunu belirten bir örneği burada bulabilirsiniz.

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