Threading در اندروید — از صفر تا صد

۷۱۰ بازدید
آخرین به‌روزرسانی: ۰۳ مهر ۱۴۰۲
زمان مطالعه: ۱۰ دقیقه
Threading در اندروید — از صفر تا صد

هر توسعه‌دهنده اندرویدی به هر حال در نقطه‌ای از زمان نیاز خواهد داشت که با نخ‌ها (Threads) در اندروید کار کند. به صورت پیش‌فرض Thread-ها سه کار را انجام می‌دهند: آغاز می‌شوند، کاری را انجام می‌دهند و خاتمه می‌یابند. این فرایند برای اجرای کارهای کوچک مناسب است، اما برای وظایفی طولانی که «نخ» (Thread) باید به صورت پیوسته کاری را اجرا کند مناسب نیستند. در این مقاله به بررسی روش کار با Threading در اندروید می‌پردازیم.

از آنجا که Thread-ها پس از انجام یک کار خاتمه می‌یابند، باید نوعی حلقه برای زنده نگه داشتن آن‌ها در اختیار داشته باشیم. همچنین باید بتوانیم در زمان نیاز هم آن‌ها را خاتمه ببخشیم. علاوه بر آن باید نوعی صف نیز داشته باشیم که  حلقه موصوف بتواند کارها را از روی آن بردارد. ضمناً نیازمند نوعی Thread دیگر نیز هستیم که بسته‌های کاری را ایجاد کرده و آن‌ها را به صف اجرایی وارد کند. انجام همه این کارها و نگهداری حالت‌های آن موجب ایجاد پیچیدگی زیادی می‌شود. با این حال اندروید کلاس‌های مختلفی برای کار با Thread-ها ارائه کرده است.

کلاس‌های Thread در اندروید

زمانی که یک اپلیکیشن اجرا می‌شود، اندروید پردازش لینوکس مخصوص آن را ایجاد می‌کند. علاوه بر آن، سیستم یک نخ اجرایی نیز برای آن اپلیکیشن علاوه بر «نخ اصلی» (main thread) و «نخ UI» نیز (UI thread) می‌سازد. نخ اصلی چیزی به جز یک نخ مدیریت کننده نیست. نخ اصلی مسئول مدیریت رویدادهای رخ داده در همه جای اپلیکیشن مانند callback-های مرتبط با اطلاعات چرخه عمری یا callback-های رویدادهای ورودی است. همچنین می‌تواند رویدادهای رسیده از اپلیکیشن‌های دیگر را مدیریت کند.

هر بلوک کد که نیازمند اجرا باشد، به صف کاری وارد می‌شود و سپس از سوی نخ اصلی سرویس می‌گیرد. از آنجا که نخ اصلی کار زیادی انجام می‌دهد، بهتر است کارهای طولانی روی نخ‌های دیگر قرار گیرد تا نخ UI از وظایف رندرینگ خود باز نماند. این نکته را همواره در نظر داشته باشید که نخ اصلی نباید کارهای طولانی انجام دهد که موجب مسدود شدن آن شود و در نهایت عدم پاسخگویی اپلیکیشن (ANR) را در پی داشته باشد.

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

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

کلاس‌های نخ مختلف موجود به شرح زیر هستند:

  • AsyncTask - به گذاشتن یک کار روی نخ UI و برداشتن از روی آن کمک می‌کند.
  • HandlerThread - نخی برای callback-ها است.
  • ThreadPoolExecutor - بسیاری از کارهای موازی را اجرا می‌کند.
  • IntentService - به برداشتن اینتنت‌ها از روی نخ UI کمک می‌کند.

AsyncTask

AsyncTask ما را قادر می‌سازد که به درستی و به سهولت از نخ UI استفاده کنیم. این کلاس به ما امکان می‌دهد که عملیات پس‌زمینه را اجرا کرده و نتایج را روی نخ UI منتشر کنیم. این کار بدون استفاده از نخ‌ها یا «دستگیره‌ها» (handlers) انجام می‌یابد.

AsyncTask به عنوان یک کلاس کمکی برای Thread (+) و Handler (+) طراحی شده است و یک فریمورک نخ بندی ژنریک تشکیل نمی‌دهد. بهتر است AsyncTask برای عملیات کوتاه‌مدت مورد استفاده قرار گیرد که نهایتاً چند ثانیه طول می‌کشند. اگر لازم است که نخ‌ها برای دوره‌های زمانی بلندتری اجرا شوند، قویاً توصیه می‌شود از API-های مختلف ارائه‌شده از سوی پکیج java.util.concurrent مانند Executor ،ThreadPoolExecutor و FutureTask استفاده کنیم.

زمانی که یک وظیفه ناهمگام اجرا می‌شود، این وظیفه در چهار گام انجام می‌یابد:

  1. ()onPreExecute – روی نخ UI پیش از اجرای وظیفه فراخوانی می‌شود. این گام به طور معمول برای انجام کاری پیش از آغاز شدن وظیفه اجرا می‌شود. برای نمونه با استفاده از آن می‌توان یک نمودار پیشروی را در رابط کاربری نمایش داد.
  2. doInBackground(Params…) – روی نخ پس‌زمینه پس از پایان یافتن اجرای ()onPreExecute آغازمی شود. در این گام محاسبات پس‌زمینه‌ای که ممکن است مدت زمان زیادی طول بکشند اجرا می‌شود. پارامترهای وظیفه ناهمگام به این گام ارسال می‌شوند. نتیجه محاسبات باید از سوی این گام بازگشت یابند و نتیجه را به ()onPreExecute ارسال کند. در این گام از (..)publishProgress نیز برای انتشار یک یا چند واحد از پیشروی استفاده می‌شود.
  3. onProgressUpdate(Progress…) – روی نخ UI و پس از یک فراخوانی به (..)publishProgress اجرا می‌شود. این متد برای نمایش هر نوع از پیشروی در رابط کاربری همزمان با اجرای محاسبات در نخ پس‌زمینه استفاده می‌شود. برای نمونه از آن می‌توان برای انیمیت کردن یک نوار پیشروی یا نمایش لاگ‌ها در یک فیلد متنی استفاده کرد.
  4. onPostExecute(Result) – روی نخ UI پس از پایان محاسبات پس‌زمینه آغاز می‌شود. نتیجه محاسبات پس‌زمینه به صورت یک پارامتر به این گام ارسال می‌شود.

هر وظیفه‌ای می‌تواند هر زمان با فراخوانی cancel(boolean…) لغو شود. اجرای این کار باید همراه با بررسی این نکته همراه باشد که وظیفه از قبل لغو شده یا در حال اجرا است.

پیاده‌سازی

1private class AsyncTaskRunner extends AsyncTask<String, String, String> {
2@Override  protected void onPreExecute() {
3  progressDialog.show();
4 }
5@Override  protected String doInBackground(String... params) {          . doSomething();
6  publishProgress("Sleeping..."); // Calls onProgressUpdate()
7  return resp;
8 }
9@Override   protected void onPostExecute(String result) {
10  // execution of result of Long time consuming operation            . progressDialog.dismiss();
11  updateUIWithResult() ;
12 }
13@Override  protected void onProgressUpdate(String... text) {
14 updateProgressUI();
15 }
16}

چه زمانی از AsyncTask استفاده کنیم؟

AsyncTask یک راه‌حل عالی برای کارهای کوتاه‌مدت است که به سرعت پایان می‌یابند و نیازمند به‌روزرسانی مکرر UI هستند. با این حال AsyncTask در مواردی که لازم باشد وظیفه‌ای برای اجرا پس از چرخه عمر اکتیویتی/فرگمان به تأخیر بیفتد به درستی عمل نمی‌کند. لازم است اشاره کنیم که حتی چیزی به سادگی چرخش یک صفحه نیز ممکن است موجب تخریب اکتیویتی شود.

ترتیب اجرا

به صورت پیش‌فرض، همه AsyncTask-ها روی یک نخ قرار می‌گیرند و به روش ترتیبی از یک صف پیام منفرد اجرا می‌شوند. اجرای همگام بر روی وظایف منفرد تأثیر می‌گذارد. اگر بخواهیم وظایف به صورت موازی اجرا شوند، می‌توانیم از THREAD_POOL_EXECUTOR استفاده کنیم.

HandlerThread

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

  • Looper  (+) – نخ را زنده نگه می‌دارد و صف پیام را نگهداری می‌کند.
  • MessageQueue (+) – این کلاس فهرستی از پیام‌هایی که باید از سوی Looper ارسال شوند را نگهداری می‌کند.
  • Handler (+) – به ما امکان می‌دهد که اشیای پیام مرتبط با یک MessageQueue نخ را ارسال و پردازش کنیم.

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

دو روش اصلی برای ایجاد نخ‌های دستگیره وجود دارد:

  1. یک نخ دستگیره جدید ایجاد کنید و یک looper به دست آورید. سپس یک دستگیره جدید با انتساب looper نخ دستگیره ایجاد شده بسازید و وظایف خود را روی این دستگیره ارسال کنید.
  2. نخ دستگیره را با ایجاد کلاس CustomHandlerThread بسط دهید. سپس یک دستگیره برای پردازش وظیفه ایجاد کنید. این رویکرد را زمانی اجرا می‌کنیم که وظیفه‌ای که قرار است اجرا کنیم را بشناسیم و صرفاً نیازمند ارسال پارامتر باشیم. به عنوان مثال ممکن است بخواهید یک کلاس HandlerThread تصاویری را دانلود و یا وظایف دیگری مرتبط با شبکه اجرا کند.
1HandlerThread handlerThread = new HandlerThread("TesHandlerThread");
2handlerThread.start();
3Looper looper = handlerThread.getLooper();
4Handler handler = new Handler(looper);
5handler.post(new Runnable(){});

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

نکته: زمانی که کارتان با نخ پس‌زمینه پایان یافت، ()handlerThread.quit را روی متد ()onDestroy مربوط به اکتیویتی فراخوانی کنید.

امکان ارسال به‌روزرسانی‌ها به نخ UI با استفاده از «انتشار» (Broadcast) لوکال و یا از طریق ایجاد یک دستگیره با looper اصلی وجود دارد:

1Handler mainHandler = new Handler(context.getMainLooper()); 
2 mainHandler.post(myRunnable);

چه زمانی از نخ‌های دستگیره استفاده کنیم؟

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

ThreadPoolExecutor

در این بخش به بررسی «استخر نخ» (Thread Pool) می‌پردازیم.

استخر نخ چیست؟

منظور از استخر نخ در واقع یک مجموعه از نخ‌ها است که در انتظار ارائه یک وظیفه هستند. وظیفه‌ای که به این نخ‌ها انتساب می‌یابد به صورت موازی اجرا خواهد شد. از آنجا که این وظایف به صورت موازی اجرا می‌شوند، ممکن است بخواهیم مطمئن شویم که کدمان از نظر نخ ایمن است. یک استخر نخ به طور عمده به حل دو مشکل زیر کمک می‌کند:

  • بهبود عملکرد در زمان اجرای یک مجموعه بزرگ از وظایف ناهمگام به دلیل کاهش سربار هر وظیفه.
  • ابزاری برای کران‌دار کردن و مدیریت منابع (شامل نخ‌ها) در زمان اجرای مجموعه‌ای از وظایف.

مثال زیر را در نظر بگیرید:

اگر 40 تصویر BMP داشته باشیم که بخواهیم دیکد کنیم و هر bitmap برای دیکد شدن به 4 میلی‌ثانیه زمان نیاز داشته باشد. اگر این کار را روی یک نخ منفرد اجرا کنیم، در مجموع 160 میلی‌ثانیه برای دیکد کردن همه بیت‌مپ‌ها نیاز داریم. با این حال اگر این کار را با 10 نخ انجام دهیم، هر یک چهار بیت‌مپ را دیکد می‌کند و از این رو زمان مورد نیاز برای دیکد کردن این 40 بیت‌مپ برابر با تنها 16 میلی‌ثانیه خواهد بود.

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

ThreadPoolExecutor چیست؟

ThreadPoolExecutor یک کلاس است که AbstractExecutorService (+) را بسط می‌دهد. ThreadPoolExecutor وظیفه مراقبت از همه نخ‌ها را بر عهده دارد:

  • وظایف را به نخ‌ها تحویل می‌دهد.
  • آن‌ها را زنده نگاه می‌دارد.
  • نخ‌ها را خاتمه می‌بخشد.

طرز کار ThreadPoolExecutor در پس‌زمینه این گونه است که وظایفی که باید اجرا شوند، در صف کار نگهداری می‌شوند. هر وظیفه در زمانی که یک نخ در استخر نخ آزاد یا موجود می‌شود، از صف کار به آن نخ انتساب می‌یابد.

Runnable

Runnable یک اینترفیس است که باید از سوی یک کلاس پیاده‌سازی شود. وهله‌های این کلاس به منظور اجرای یک نخ مورد استفاده قرار می‌گیرند. به بیان ساده Runnable یک وظیفه یا دستور است که باید اجرا شود. از این اینترفیس به طور مکرر برای اجرای کد در نخ‌های مختلف استفاده می‌شود.

1Runnable mRunnable = new Runnable() {
2    @Override
3    public void run() {
4        // Do some work
5    }
6};

Executor

Executor نیز یک اینترفیس است که برای جداسازی تحویل یک وظیفه از اجرای وظیفه مورد استفاده قرار می‌گیرد. هدف آن اجرای یک Runnable است.

1Executor mExecutor = Executors.newSingleThreadExecutor(); mExecutor.execute(mRunnable);

ExecutorService

یک Executor است که به مدیریت وظایف ناهمگام می‌پردازد.

1ExecutorService mExecutorService = Executors.newFixedThreadPool(10); mExecutorService.execute(mRunnable);

ThreadPoolExecutor

ThreadPoolExecutor یک ExecutorService است که وظایف را به نخ استخرها انتساب می‌دهد. وجود تعداد زیادی نخ در اغلب موارد چندان خوب نیست، زیرا CPU تنها تعداد مشخصی از نخ‌ها را می‌تواند به صورت موازی اجرا کند. زمانی که تعداد نخ‌ها از این تعداد تجاوز کند، CPU باید محاسبات پرهزینه‌ای اجرا کند تا در مورد این که کدام نخ باید بر اساس اولویت انتساب یابد، تصمیم بگیرد.

ما در زمان ایجاد وهله‌ای از ThreadPoolExecutor می‌توانیم تعداد نخ‌های اولیه و تعداد نخ‌های بیشینه آن را تعیین کنیم از آنجا که بارِ کاری در استخر نخ متغیر است، تعداد نخ‌های زنده برای تطبیق با این وضعیت تغییر می‌یابد. به طور معمول پیشنهاد می‌شود که نخ‌ها بر مبنای تعداد هسته‌های موجود تخصیص یابند. این کار به صورت زیر انجام می‌شود:

1int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

نکته: این دستور لزوماً تعداد واقعی هسته‌های فیزیکی روی دستگاه را بازگشت نمی‌دهد. ممکن است CPU برخی هسته‌ها را برای ذخیره باتری غیر فعال کرده باشد و یا مورد دیگری وجود داشته باشد.

1ThreadPoolExecutor(
2   int corePoolSize,    // Initial pool size
3   int maximumPoolSize, // Max pool size
4   long keepAliveTime,  // Time idle thread waits before terminating
5   TimeUnit unit        // Sets the Time Unit for keepAliveTime
6   BlockingQueue<Runnable> workQueue)  // Work Queue

این پارامترها به این معنی هستند:

  1. corePoolSize – کمینه تعداد نخ‌هایی که در استخر نگهداری می‌شوند. در ابتدا صفر نخ در استخر وجود دارند. اما زمانی که وظایف به صف اضافه می‌شوند، نخ‌های جدید ایجاد می‌شوند. اگر تعداد نخ‌های اجرایی از corePoolSize کمتر باشد، Executor همواره ترجیح می‌دهد که به جای صف‌بندی، نخ جدیدی اضافه کند.
  2. maximumPoolSize – بیشینه تعداد نخ‌های مجاز در استخر هستند. اگر این مقدار از corePoolSize تجاوز کند، و تعداد کنونی نخ‌ها بزرگ‌تر یا برابر با corePoolSize باشد، نخ‌های کاری جدید تنها در صورتی ایجاد می‌شوند که صف پر باشد.
  3. keepAliveTime – زمانی که تعداد نخ‌ها بزرگ‌تر از تعداد هسته‌ها باشد، نخ‌های غیر هسته (مازاد نخ‌های بیکار) منتظر وظیفه جدید می‌مانند و اگر در طی زمان تعیین‌شده از سوی این پارامتر وظیفه‌ای دریافت نکنند، خاتمه خواهند یافت.
  4. Unit – واحد زمانی برای keepAliveTime است.
  5. workQueue – صف کاری است که تنها وظایف runnable را نگهداری می‌کند. این صف باید از نوع BlockingQueue (+) باشد.

چه زمانی از ThreadPoolExecutor استفاده کنیم؟

ThreadPoolExecutor یک چارچوب اجرای وظیفه قدرتمند است. از آن می‌توان در زمان وجود تعداد بالایی از وظایف که باید به صورت موازی اجرا شوند استفاده کرد چون ThreadPoolExecutor از اضافه کردن وظیفه به صف، لغو وظیفه و اولویت‌بندی وظایف پشتیبانی می‌کند.

IntentService

IntentService یک زیرکلاس به ارث رسیده از Service است. برای این که IntentService را بشناسیم باید ابتدا Service را بشناسیم. Service یک کامپوننت بسیار مهم در برنامه‌نویسی اندروید محسوب می‌شود. برخی اوقات ممکن است وظیفه‌ای داشته باشیم که باید حتی پس از بسته شدن اپلیکیشن نیز اجرا شود. در این حالت از Service استفاده می‌کنیم. Service می‌تواند از سوی ()startService اجرا شده و با ()stopService متوقف شود و برای مدتی طولانی در پس‌زمینه اجرا شود. همچنین یک سرویس را می‌توان با فراخوانی ()stopSelf در درونش لغو کرد.

در ادامه برخی از متدهای override شده که برای اجرای عملیات مختلف مفید هستند را می‌بینید:

  • ()onCreate – تنها یک بار فراخوانی خواهد شد تا این که سرویس متوقف شود.
  • ()onStartCommand – این تابع پس از ()onCreate برای نخستین بار فراخوانی می‌شود، اما می‌تواند مستقیماً هر زمان که کامپوننتی ()startService را با اینتنت فراخوانی می‌کند، از بار دوم به بعد مورد فراخوانی قرار گیرد.
  • ()onDestroy – در زمان توقف سرویس فراخوانی می‌شود.

گردش نرمال یک سرویس به صورت زیر است:

onCreate() -> onStartCommand() -> onDestroy()

اگر به بحث IntentService بازگردیم، باید بگوییم که Service به صورت نرمال آغاز می‌شود، یعنی ()startService را از نخ اصلی فراخوانی می‌کنیم. این سرویس به جای ()onStartCommand هر اینتنت که در ()onHandleIntent باشد را مدیریت می‌کند. همچنین از یک نخ کاری استفاده می‌کند و زمانی که کار پایان یابد خود را متوقف می‌کند. برای استفاده از آن باید IntentService را بسط داده و ()onHandleIntent را پیاده‌سازی کنید.

نکته: IntentService روی یک نخ کاری منفرد اجرا می‌شود، در حالی که Service روی نخ اصلی اجرا می‌شود. هر بار تنها یک درخواست پردازش می‌شوند.

IntentService تابع همه محدودیت‌های اجرایی پس‌زمینه است که از سوی اندروید 8.0 (سطح API 26) تعیین شده است. در اغلب موارد بهتر است از JobIntentService (+) استفاده کنید که در زمان اجرا روی اندروید 8 و بالاتر از job-ها به جای سرویس‌ها استفاده می‌کند.

چه زمانی از IntentService استفاده کنیم؟

IntentService درخواست‌های ناهمگام را بسته به تقاضا مدیریت می‌کند. در صورتی که لازم نیست سرویستان درخواست‌های چندگانه را به صورت همزمان مدیریت کند، این بهترین گزینه است. بدین ترتیب به پایان این مقاله می‌رسیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۵ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming
۲ دیدگاه برای «Threading در اندروید — از صفر تا صد»

با سلام و عرض وقت بخیر
اولاً تشکر از زحمات شما که مطلب مفیدی را به اشتراک گذاشته اید. میخواستم در صورت امکان از مطالب شما با ذکر منبع در جزوه آموزشی خودم استفاده کنم اگر مقدور هست و اجازه میدهید این مطالب را با ذکر منبع استفاده نمایم.
با تشکر

با سلام؛

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

با تشکر از همراهی شما با مجله فرادرس

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *