کار ناهمگام با رشته‌های جاوا

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

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

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

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

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

نمای کلی نمونه‌ها

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

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

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

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

مجموعه رشته مجموعه‌ای مدیریت‌شده از رشته‌ها است که تکالیف را به‌صورت موازی از یک صف اجرا می‌کند. تکالیف جدید در رشته‌های موجود اجرا می‌شوند، زیرا آن رشته‌ها غیرفعال می‌شوند. برای ارسال تکلیف به استخر رشته، از میانای ExecutorService استفاده کنید. توجه داشته باشید که ExecutorService هیچ ارتباطی با سرویس‌ها، مؤلفه برنامه Android، ندارد.

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

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

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

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

ایجاد درخواست شبکه در رشته اصلی باعث می‌شود رشته منتظر بماند یا مسدود شود تا زمانی که پاسخ دریافت کند. چون رشته مسدود شده است، سیستم‌عامل نمی‌تواند 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 نمونه‌ای از 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 یک میانای «روش انتزاعی تکی» (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
                }
            }
        });
    }
}

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

استفاده از گرداننده‌ها

می‌توانید از 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);
            }
        });
    }
}

در این مثال، فراخوانی برگشتی که به فراخوانی makeLoginRequest «مخزن» ارسال شده است در رشته اصلی اجرا می‌شود. این یعنی می‌توانید مستقیماً واسط کاربر را ازطریق فراخوانی برگشتی اصلاح کنید یا از 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
    );
    ...
}