کار ناهمزمان با موضوعات جاوا

همه برنامه های اندروید از یک رشته اصلی برای مدیریت عملیات رابط کاربری استفاده می کنند. فراخوانی عملیات طولانی مدت از این رشته اصلی می تواند منجر به فریز و عدم پاسخگویی شود. برای مثال، اگر برنامه شما یک درخواست شبکه از رشته اصلی ارسال کند، رابط کاربری برنامه شما تا زمانی که پاسخ شبکه را دریافت نکند، ثابت می‌شود. اگر از جاوا استفاده می‌کنید، می‌توانید رشته‌های پس‌زمینه اضافی برای مدیریت عملیات طولانی‌مدت ایجاد کنید، در حالی که رشته اصلی به مدیریت به‌روزرسانی‌های رابط کاربری ادامه می‌دهد.

این راهنما نشان می‌دهد که چگونه توسعه‌دهندگانی که از زبان برنامه‌نویسی جاوا استفاده می‌کنند، می‌توانند از یک Thread Pool برای راه‌اندازی و استفاده از چندین رشته در یک برنامه Android استفاده کنند. این راهنما همچنین به شما نشان می دهد که چگونه کدی را برای اجرا در یک رشته تعریف کنید و چگونه بین یکی از این رشته ها و رشته اصلی ارتباط برقرار کنید.

کتابخانه های همزمان

درک اصول اولیه threading و مکانیسم های زیربنایی آن بسیار مهم است. با این حال، بسیاری از کتابخانه های محبوب وجود دارند که انتزاعات سطح بالاتری را در مورد این مفاهیم و ابزارهای آماده برای استفاده برای انتقال داده ها بین رشته ها ارائه می دهند. این کتابخانه ها شامل Guava و RxJava برای کاربران زبان برنامه نویسی جاوا و Coroutines هستند که ما به کاربران Kotlin توصیه می کنیم.

در عمل، باید برنامه‌ای را انتخاب کنید که برای برنامه‌تان و تیم توسعه‌دهی شما بهترین کارآمد باشد، اگرچه قوانین رشته‌بندی یکسان باقی می‌ماند.

بررسی اجمالی نمونه ها

بر اساس راهنمای معماری برنامه ، مثال‌های موجود در این مبحث درخواست شبکه می‌کنند و نتیجه را به رشته اصلی بازمی‌گردانند، جایی که برنامه ممکن است آن نتیجه را روی صفحه نمایش دهد.

به طور خاص، ViewModel لایه داده را در رشته اصلی فراخوانی می کند تا درخواست شبکه را راه اندازی کند. لایه داده وظیفه دارد اجرای درخواست شبکه را به خارج از رشته اصلی منتقل کند و نتیجه را با استفاده از یک callback به رشته اصلی ارسال کند.

برای انتقال اجرای درخواست شبکه به خارج از رشته اصلی، باید رشته های دیگری را در برنامه خود ایجاد کنیم.

چندین رشته ایجاد کنید

Thread Pool مجموعه ای مدیریت شده از رشته ها است که وظایف را به صورت موازی از یک صف اجرا می کند. با بیکار شدن آن رشته ها، کارهای جدید روی رشته های موجود اجرا می شوند. برای ارسال یک کار به یک Thread Pool، از رابط ExecutorService استفاده کنید. توجه داشته باشید که ExecutorService هیچ ارتباطی با Services ، جزء برنامه Android ندارد.

ایجاد رشته ها گران است، بنابراین شما باید تنها یک بار با شروع اولیه برنامه، یک Thread Pool ایجاد کنید. حتماً نمونه ExecutorService را در کلاس Application یا در ظرف تزریق وابستگی ذخیره کنید. مثال زیر یک Thread Pool از چهار رشته ایجاد می کند که می توانیم از آنها برای اجرای وظایف پس زمینه استفاده کنیم.

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

روش های دیگری نیز وجود دارد که می توانید بسته به حجم کاری مورد انتظار، یک Thread Pool را پیکربندی کنید. برای اطلاعات بیشتر پیکربندی یک مخزن رشته را ببینید.

در یک رشته پس زمینه اجرا کنید

ایجاد یک درخواست شبکه در رشته اصلی باعث می شود که رشته منتظر بماند یا مسدود شود تا زمانی که پاسخی دریافت کند. از آنجایی که رشته مسدود شده است، سیستم عامل نمی‌تواند onDraw() را فراخوانی کند، و برنامه شما ثابت می‌شود، که به طور بالقوه منجر به یک گفتگوی Application Not Responding (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 در هنگام درخواست شبکه، موضوع اصلی را مسدود می کند. می‌توانیم از thread pool که نمونه‌سازی کرده‌ایم برای انتقال اجرا به یک رشته پس‌زمینه استفاده کنیم.

تزریق وابستگی را کنترل کنید

ابتدا، با پیروی از اصول تزریق وابستگی ، LoginRepository یک نمونه از Executor را در مقابل ExecutorService می گیرد، زیرا کد را اجرا می کند و رشته ها را مدیریت نمی کند:

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

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

متد execute() Executor یک Runnable می گیرد. Runnable یک اینترفیس Single Abstract Method (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 باید در مورد آن بداند. ما می توانیم این کار را با استفاده از callbacks انجام دهیم.

تابع 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 اکنون باید callback را اجرا کند. بسته به نتیجه می تواند منطق متفاوتی را انجام دهد:

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

در این مثال، callback در رشته فراخوانی که یک رشته پس زمینه است، اجرا می شود. این بدان معناست که تا زمانی که به رشته اصلی برگردید نمی‌توانید لایه رابط کاربری را تغییر دهید یا مستقیماً با آن ارتباط برقرار کنید.

از هندلرها استفاده کنید

شما می توانید از یک Handler برای قرار دادن یک عمل در یک رشته مختلف استفاده کنید. برای تعیین رشته ای که روی آن اکشن اجرا شود، Handler را با استفاده از یک Looper برای رشته بسازید. 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);
            }
        });
    }
}

در این مثال، callback ارسال شده به فراخوانی makeLoginRequest مخزن در رشته اصلی اجرا می شود. این بدان معناست که می توانید مستقیماً رابط کاربری را از callback تغییر دهید یا از LiveData.setValue() برای برقراری ارتباط با UI استفاده کنید.

یک Thread Pool را پیکربندی کنید

همانطور که در کد مثال قبلی نشان داده شده است، می توانید با استفاده از یکی از توابع کمکی Executor با تنظیمات از پیش تعریف شده، یک Thread Pool ایجاد کنید. از طرف دیگر، اگر می‌خواهید جزئیات Thread Pool را سفارشی کنید، می‌توانید با استفاده از ThreadPoolExecutor مستقیماً یک نمونه ایجاد کنید. می توانید جزئیات زیر را پیکربندی کنید:

  • اندازه اولیه و حداکثر استخر.
  • واحد زمان و زمان را زنده نگه دارید . زمان زنده نگه داشتن حداکثر مدت زمانی است که یک رشته قبل از خاموش شدن می تواند بیکار بماند.
  • یک صف ورودی که وظایف Runnable را نگه می دارد. این صف باید رابط BlockingQueue را پیاده سازی کند. برای مطابقت با الزامات برنامه خود، می توانید از میان اجرای صف موجود انتخاب کنید. برای کسب اطلاعات بیشتر، نمای کلی کلاس برای ThreadPoolExecutor را ببینید.

در اینجا یک مثال آورده شده است که اندازه thread pool را بر اساس تعداد کل هسته های پردازنده، زمان نگه داشتن زنده یک ثانیه و یک صف ورودی مشخص می کند.

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