UI অপারেশন ম্যানেজ করার জন্য সব Android অ্যাপ একটি প্রধান থ্রেড ব্যবহার করে। এই মূল থ্রেড থেকে দীর্ঘ সময় ধরে চলা অপারেশন কল করলে, অ্যাপ ফ্রিজ হয়ে যেতে পারে এবং কাজ করা বন্ধ করে দিতে পারে। যেমন, আপনার অ্যাপ যদি প্রধান থ্রেড থেকে নেটওয়ার্ক অনুরোধ করে, তাহলে নেটওয়ার্কের উত্তর না পাওয়া পর্যন্ত আপনার অ্যাপের UI ফ্রিজ হয়ে যায়। আপনি Java ব্যবহার করলে, প্রধান থ্রেড UI আপডেট ম্যানেজ করা চালিয়ে যাওয়ার সময়, দীর্ঘ সময় ধরে চলা অপারেশন ম্যানেজ করতে অতিরিক্ত ব্যাকগ্রাউন্ড থ্রেড তৈরি করতে পারবেন।
এই নির্দেশিকা দেখায় যে কীভাবে জাভা প্রোগ্রামিং ভাষা ব্যবহার করে ডেভেলপাররা একটি থ্রেড পুল ব্যবহার করে একটি Android অ্যাপে একাধিক থ্রেড সেট আপ এবং ব্যবহার করতে পারেন। এই নির্দেশিকা আপনাকে আরও দেখায় যে কীভাবে একটি থ্রেডে চালানোর জন্য কোড সংজ্ঞায়িত করতে হয় এবং কীভাবে এই থ্রেডগুলির একটি এবং প্রধান থ্রেডের মধ্যে যোগাযোগ করতে হয়।
কনকারেন্সি লাইব্রেরি
থ্রেডিংয়ের প্রাথমিক ধারণা এবং এর অন্তর্নিহিত মেকানিজম বোঝা গুরুত্বপূর্ণ। তবে, অনেক জনপ্রিয় লাইব্রেরি আছে যেগুলি এই কনসেপ্টের উপর উচ্চ-স্তরের অ্যাবস্ট্রাকশন এবং থ্রেডের মধ্যে ডেটা পাস করার জন্য রেডি-টু-ইউজ ইউটিলিটি অফার করে। এইসব লাইব্রেরিতে জাভা প্রোগ্রামিং ল্যাঙ্গুয়েজ ব্যবহারকারীদের জন্য Guava ও RxJava এবং Kotlin ব্যবহারকারীদের জন্য Coroutines অন্তর্ভুক্ত, যা আমরা সাজেস্ট করি।
বাস্তবে, আপনার অ্যাপ ও ডেভেলপমেন্ট টিমের জন্য যেটি সবচেয়ে ভাল কাজ করে সেটিই বেছে নেওয়া উচিত, যদিও থ্রেডিংয়ের নিয়ম একই থাকে।
উদাহরণ ওভারভিউ
অ্যাপ আর্কিটেকচার সংক্রান্ত নির্দেশিকা অনুযায়ী, এই বিষয়ের উদাহরণগুলি নেটওয়ার্ক রিকোয়েস্ট করে এবং মূল থ্রেডে ফলাফল দেখায়, যেখানে অ্যাপটি স্ক্রিনে সেই ফলাফল দেখাতে পারে।
বিশেষ করে, নেটওয়ার্ক অনুরোধ ট্রিগার করতে ViewModel প্রধান থ্রেডে ডেটা লেয়ারকে কল করে। ডেটা লেয়ারের কাজ হল নেটওয়ার্ক অনুরোধের এক্সিকিউশনকে মূল থ্রেড থেকে সরিয়ে দেওয়া এবং কলব্যাক ব্যবহার করে ফলাফল মূল থ্রেডে পোস্ট করা।
নেটওয়ার্ক অনুরোধের এক্সিকিউশন প্রধান থ্রেড থেকে সরিয়ে নিতে, আমাদের অ্যাপে অন্য থ্রেড তৈরি করতে হবে।
একাধিক থ্রেড তৈরি করুন
থ্রেড পুল হল থ্রেডের একটি ম্যানেজ করা সংগ্রহ যা একটি সারি থেকে সমান্তরালভাবে
টাস্ক রান করে। আগে থেকে থাকা থ্রেডগুলি নিষ্ক্রিয় হয়ে গেলে,
সেগুলিতে নতুন টাস্ক কার্যকর করা হয়। থ্রেড পুলে কোনও টাস্ক পাঠাতে, ExecutorService ইন্টারফেস ব্যবহার করুন। মনে রাখবেন, ExecutorService-এর সাথে Android অ্যাপ্লিকেশন কম্পোনেন্ট,
পরিষেবা-এর কোনও সম্পর্ক নেই।
থ্রেড তৈরি করা ব্যয়বহুল, তাই আপনার অ্যাপ শুরু করার সময়
থ্রেড পুল একবারই তৈরি করা উচিত। 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;
}
...
}
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() ফাংশনের প্যারামিটার হিসেবে কলব্যাক নেওয়া উচিত যাতে
এটি অ্যাসিঙ্ক্রোনাসভাবে ভ্যালু রিটার্ন করতে পারে। নেটওয়ার্ক অনুরোধ সম্পূর্ণ হলে বা কোনও সমস্যা হলে
ফলাফল সহ কলব্যাক কল করা হয়। 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
}
}
});
}
}
এই উদাহরণে, কলিং থ্রেডে কলব্যাক এক্সিকিউট করা হয়, যা একটি ব্যাকগ্রাউন্ড থ্রেড। এর অর্থ হল, আপনি মূল থ্রেডে ফিরে না আসা পর্যন্ত 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);
}
});
}
}
এই উদাহরণে, Repository-র makeLoginRequest
কলে পাস করা কলব্যাক মূল থ্রেডে এক্সিকিউট করা হয়। এর অর্থ হল, আপনি সরাসরি কলব্যাক থেকে UI পরিবর্তন করতে পারবেন
অথবা UI-এর সাথে যোগাযোগ করতে LiveData.setValue() ব্যবহার করতে পারবেন।
থ্রেড পুল কনফিগার করুন
আগের উদাহরণ কোডে যেমন দেখানো হয়েছে, আপনি আগে থেকে নির্দিষ্ট সেটিংস সহ Executor হেল্পার ফাংশনের মধ্যে একটি ব্যবহার করে থ্রেড পুল তৈরি করতে পারবেন। অন্যথায়, আপনি যদি থ্রেড পুলের বিবরণ কাস্টমাইজ করতে চান, তাহলে সরাসরি ThreadPoolExecutor ব্যবহার করে একটি ইনস্ট্যান্স তৈরি করতে পারেন। আপনি নিম্নলিখিত বিবরণ কনফিগার করতে পারবেন:
- প্রাথমিক ও সর্বাধিক পুল সাইজ।
- কিপ অ্যালাইভ টাইম এবং সময়ের ইউনিট। কিপ অ্যালাইভ টাইম হল সর্বাধিক সময়সীমা যার জন্য একটি থ্রেড বন্ধ হওয়ার আগে নিষ্ক্রিয় থাকতে পারে।
- ইনপুট কিউ যাতে
Runnableটি টাস্ক আছে। এই কিউকে অবশ্যই BlockingQueue ইন্টারফেস প্রয়োগ করতে হবে। আপনার অ্যাপের প্রয়োজনীয়তার সাথে ম্যাচ করতে, আপনি উপলভ্য কিউ ইমপ্লিমেন্টেশন থেকে বেছে নিতে পারবেন। আরও জানতে, 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
);
...
}