জাভা থ্রেডের সাথে অ্যাসিঙ্ক্রোনাস কাজ

সমস্ত অ্যান্ড্রয়েড অ্যাপ্লিকেশানগুলি UI অপারেশনগুলি পরিচালনা করতে একটি প্রধান থ্রেড ব্যবহার করে৷ এই প্রধান থ্রেড থেকে দীর্ঘ-চলমান ক্রিয়াকলাপগুলিকে কল করা হিমায়িত এবং প্রতিক্রিয়াহীনতার দিকে পরিচালিত করতে পারে। উদাহরণস্বরূপ, যদি আপনার অ্যাপটি প্রধান থ্রেড থেকে একটি নেটওয়ার্ক অনুরোধ করে, তাহলে আপনার অ্যাপের UI হিমায়িত থাকে যতক্ষণ না এটি নেটওয়ার্ক প্রতিক্রিয়া পায়। আপনি যদি জাভা ব্যবহার করেন, আপনি দীর্ঘ-চলমান ক্রিয়াকলাপগুলি পরিচালনা করতে অতিরিক্ত পটভূমি থ্রেড তৈরি করতে পারেন যখন মূল থ্রেডটি UI আপডেটগুলি পরিচালনা করতে থাকে।

এই নির্দেশিকাটি দেখায় যে কীভাবে জাভা প্রোগ্রামিং ল্যাঙ্গুয়েজ ব্যবহারকারী বিকাশকারীরা একটি অ্যান্ড্রয়েড অ্যাপে একাধিক থ্রেড সেট আপ করতে এবং ব্যবহার করতে একটি থ্রেড পুল ব্যবহার করতে পারে৷ এই নির্দেশিকাটি আপনাকে দেখায় যে কীভাবে একটি থ্রেডে চালানোর জন্য কোড সংজ্ঞায়িত করতে হয় এবং কীভাবে এই থ্রেডগুলির মধ্যে একটি এবং প্রধান থ্রেডের মধ্যে যোগাযোগ করতে হয়।

কনকারেন্সি লাইব্রেরি

থ্রেডিংয়ের মূল বিষয়গুলি এবং এর অন্তর্নিহিত প্রক্রিয়াগুলি বোঝা গুরুত্বপূর্ণ। যাইহোক, অনেক জনপ্রিয় লাইব্রেরি রয়েছে যেগুলি এই ধারণাগুলির উপর উচ্চ-স্তরের বিমূর্ততা এবং থ্রেডগুলির মধ্যে ডেটা পাস করার জন্য ব্যবহারের জন্য প্রস্তুত ইউটিলিটিগুলি অফার করে। এই লাইব্রেরিতে জাভা প্রোগ্রামিং ল্যাঙ্গুয়েজ ব্যবহারকারীদের জন্য Guava এবং RxJava এবং Coroutines অন্তর্ভুক্ত, যা আমরা Kotlin ব্যবহারকারীদের জন্য সুপারিশ করি।

অনুশীলনে, আপনার অ্যাপ এবং আপনার ডেভেলপমেন্ট টিমের জন্য সবচেয়ে ভালো কাজ করে এমন একটি বেছে নেওয়া উচিত, যদিও থ্রেডিংয়ের নিয়ম একই থাকে।

উদাহরণ ওভারভিউ

অ্যাপ আর্কিটেকচারের গাইডের উপর ভিত্তি করে, এই বিষয়ের উদাহরণগুলি একটি নেটওয়ার্ক অনুরোধ করে এবং ফলাফলটি মূল থ্রেডে ফেরত দেয়, যেখানে অ্যাপটি সেই ফলাফলটি স্ক্রিনে প্রদর্শন করতে পারে।

বিশেষত, ViewModel নেটওয়ার্ক অনুরোধ ট্রিগার করতে প্রধান থ্রেডে ডেটা স্তরকে কল করে। ডাটা লেয়ার প্রধান থ্রেড থেকে নেটওয়ার্ক অনুরোধের কার্য সম্পাদন এবং একটি কলব্যাক ব্যবহার করে ফলাফলটি মূল থ্রেডে পোস্ট করার দায়িত্বে রয়েছে।

নেটওয়ার্ক রিকোয়েস্টের এক্সিকিউশনকে মূল থ্রেড থেকে সরিয়ে দিতে, আমাদের অ্যাপে অন্যান্য থ্রেড তৈরি করতে হবে।

একাধিক থ্রেড তৈরি করুন

একটি থ্রেড পুল হল থ্রেডের একটি পরিচালিত সংগ্রহ যা একটি সারি থেকে সমান্তরালভাবে কাজ চালায়। বিদ্যমান থ্রেডগুলিতে নতুন কাজগুলি কার্যকর করা হয় কারণ সেই থ্রেডগুলি নিষ্ক্রিয় হয়ে যায়। একটি থ্রেড পুলে একটি টাস্ক পাঠাতে, ExecutorService ইন্টারফেস ব্যবহার করুন। মনে রাখবেন যে ExecutorService পরিষেবাগুলির সাথে অ্যান্ড্রয়েড অ্যাপ্লিকেশন উপাদানটির কোনও সম্পর্ক নেই৷

থ্রেড তৈরি করা ব্যয়বহুল, তাই আপনার অ্যাপ শুরু হওয়ার সাথে সাথে আপনার শুধুমাত্র একবার একটি থ্রেড পুল তৈরি করা উচিত। ExecutorService এর উদাহরণ আপনার Application ক্লাসে বা একটি নির্ভরতা ইনজেকশন কন্টেইনারে সংরক্ষণ করতে ভুলবেন না। নিম্নলিখিত উদাহরণটি চারটি থ্রেডের একটি থ্রেড পুল তৈরি করে যা আমরা ব্যাকগ্রাউন্ডের কাজগুলি চালানোর জন্য ব্যবহার করতে পারি।

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

এক্সিকিউটরের 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 তৈরি করি—আমাদের ক্ষেত্রে, সিঙ্ক্রোনাস নেটওয়ার্ক রিকোয়েস্ট মেথড। অভ্যন্তরীণভাবে, ExecutorService Runnable পরিচালনা করে এবং এটি একটি উপলব্ধ থ্রেডে কার্যকর করে।

বিবেচনা

আপনার অ্যাপের যেকোনো থ্রেড প্রধান থ্রেড সহ অন্যান্য থ্রেডের সমান্তরালে চলতে পারে, তাই আপনার কোডটি থ্রেড-নিরাপদ তা নিশ্চিত করা উচিত। লক্ষ্য করুন যে আমাদের উদাহরণে আমরা থ্রেডগুলির মধ্যে ভাগ করা ভেরিয়েবলগুলিতে লেখা এড়াই, পরিবর্তে অপরিবর্তনীয় ডেটা পাস করি। এটি একটি ভাল অনুশীলন, কারণ প্রতিটি থ্রেড ডেটার নিজস্ব উদাহরণের সাথে কাজ করে এবং আমরা সিঙ্ক্রোনাইজেশনের জটিলতা এড়াই।

আপনি যদি থ্রেডগুলির মধ্যে স্থিতি ভাগ করতে চান তবে আপনাকে অবশ্যই লকগুলির মতো সিঙ্ক্রোনাইজেশন প্রক্রিয়া ব্যবহার করে থ্রেডগুলি থেকে অ্যাক্সেস পরিচালনা করতে সতর্ক থাকতে হবে। এটি এই গাইডের সুযোগের বাইরে। সাধারণভাবে আপনার যখনই সম্ভব থ্রেডগুলির মধ্যে পরিবর্তনযোগ্য অবস্থা ভাগ করা এড়ানো উচিত।

মূল থ্রেডের সাথে যোগাযোগ করুন

পূর্ববর্তী ধাপে, আমরা নেটওয়ার্ক অনুরোধের প্রতিক্রিয়া উপেক্ষা করেছি। স্ক্রিনে ফলাফল প্রদর্শন করতে, LoginViewModel এটি সম্পর্কে জানতে হবে। আমরা কলব্যাক ব্যবহার করে তা করতে পারি।

ফাংশন makeLoginRequest() একটি প্যারামিটার হিসাবে একটি কলব্যাক গ্রহণ করা উচিত যাতে এটি অ্যাসিঙ্ক্রোনাসভাবে একটি মান ফেরত দিতে পারে। যখনই নেটওয়ার্ক অনুরোধ সম্পূর্ণ হয় বা একটি ব্যর্থতা ঘটে তখন ফলাফল সহ কলব্যাক বলা হয়। কোটলিনে, আমরা একটি উচ্চ-ক্রম ফাংশন ব্যবহার করতে পারি। যাইহোক, জাভাতে, একই কার্যকারিতা পাওয়ার জন্য আমাদের একটি নতুন কলব্যাক ইন্টারফেস তৈরি করতে হবে:

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

এই উদাহরণে, কলব্যাকটি কলিং থ্রেডে কার্যকর করা হয়, যা একটি ব্যাকগ্রাউন্ড থ্রেড। এর মানে হল যে আপনি মূল থ্রেডে ফিরে না যাওয়া পর্যন্ত আপনি UI স্তরের সাথে সরাসরি পরিবর্তন বা যোগাযোগ করতে পারবেন না।

হ্যান্ডলার ব্যবহার করুন

আপনি একটি হ্যান্ডলার ব্যবহার করে একটি ভিন্ন থ্রেডে সঞ্চালিত একটি ক্রিয়া সারিবদ্ধ করতে পারেন। যে থ্রেডটিতে অ্যাকশন চালানো হবে তা নির্দিষ্ট করতে, থ্রেডের জন্য একটি লুপার ব্যবহার করে Handler তৈরি করুন। একটি Looper একটি বস্তু যা একটি সম্পর্কিত থ্রেডের জন্য বার্তা লুপ চালায়। একবার আপনি একটি Handler তৈরি করার পরে, আপনি সংশ্লিষ্ট থ্রেডে কোডের একটি ব্লক চালানোর জন্য পোস্ট(চালানযোগ্য) পদ্ধতি ব্যবহার করতে পারেন।

Looper একটি সহায়ক ফাংশন অন্তর্ভুক্ত করে, getMainLooper() , যা মূল থ্রেডের Looper পুনরুদ্ধার করে। আপনি একটি Handler তৈরি করতে এই Looper ব্যবহার করে মূল থ্রেডে কোড চালাতে পারেন। যেহেতু এটি এমন কিছু যা আপনি প্রায়শই করতে পারেন, আপনি যে জায়গায় ExecutorService সংরক্ষণ করেছেন সেখানে Handler একটি উদাহরণও সংরক্ষণ করতে পারেন:

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() ব্যবহার করতে পারেন৷

একটি থ্রেড পুল কনফিগার করুন

আপনি পূর্বনির্ধারিত সেটিংস সহ এক্সিকিউটর সহায়ক ফাংশনগুলির একটি ব্যবহার করে একটি থ্রেড পুল তৈরি করতে পারেন, যেমনটি পূর্ববর্তী উদাহরণ কোডে দেখানো হয়েছে। বিকল্পভাবে, আপনি যদি থ্রেড পুলের বিবরণ কাস্টমাইজ করতে চান, আপনি সরাসরি ThreadPoolExecutor ব্যবহার করে একটি উদাহরণ তৈরি করতে পারেন। আপনি নিম্নলিখিত বিবরণ কনফিগার করতে পারেন:

  • প্রাথমিক এবং সর্বোচ্চ পুলের আকার।
  • জীবিত সময় এবং সময় ইউনিট রাখুন . একটি থ্রেড বন্ধ হওয়ার আগে নিষ্ক্রিয় থাকতে পারে এমন সর্বোচ্চ সময়কে জীবিত রাখুন।
  • একটি ইনপুট সারি যা Runnable কাজগুলিকে ধারণ করে৷ এই সারিতে অবশ্যই ব্লকিং কিউ ইন্টারফেস প্রয়োগ করতে হবে। আপনার অ্যাপের প্রয়োজনীয়তা মেলানোর জন্য, আপনি উপলব্ধ সারি বাস্তবায়ন থেকে বেছে নিতে পারেন। আরও জানতে, ThreadPoolExecutor- এর ক্লাস ওভারভিউ দেখুন।

এখানে একটি উদাহরণ রয়েছে যা প্রসেসর কোরের মোট সংখ্যা, এক সেকেন্ডের জীবিত সময় এবং একটি ইনপুট সারির উপর ভিত্তি করে থ্রেড পুলের আকার নির্দিষ্ট করে।

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