huazi

huazi

"Notes on 'Exploring the Art of Android Development' - Chapter 11 - Threads and Thread Pools"

title: "Notes on 'Android Development Art Exploration' - Chapter 11 - Threads and Thread Pools"
date: 2018-05-25 16:51:21
tags:

  • Android
  • Notes
  • Threads
  • Thread Pools
    categories:
  • Android
  • Notes

AsyncTask encapsulates thread pools and handlers, mainly for the convenience of developers to update the UI in the background thread.
HandlerThread is a type of thread with a message loop, and it can use a Handler internally.
IntentService is a service that is encapsulated by the system for executing background tasks. IntentService uses HandlerThread to execute tasks, and it automatically exits the service after the tasks are completed. It is a type of service that is not easily killed by the system, ensuring the execution of tasks as much as possible.

In the operating system, a thread is the smallest unit scheduled by the operating system, and it is also a limited system resource. Threads cannot be created infinitely, and there is a corresponding overhead for creating threads. Threads cannot achieve absolute parallelism unless the number of threads is less than or equal to the number of CPU cores. The correct approach is to use thread pools, where a thread pool caches a certain number of threads, and using a thread pool can avoid the system overhead caused by frequent thread creation. Android's threads come from Java and are mainly derived from specific types of thread pools through Executors.

AsyncTask#

AsyncTask is a lightweight asynchronous task that can execute background tasks in a thread pool and then pass the execution progress and final results to the main thread.
AsyncTask encapsulates Thread and Handler. AsyncTask is not suitable for particularly time-consuming tasks, and it is recommended to use thread pools for such tasks.

Some limitations when using AsyncTask:

  1. An AsyncTask object can only call the execute method once, otherwise it will throw a runtime exception.
  2. AsyncTask defaults to executing tasks in a serial manner, but we can use the executeOnExecutor method to execute tasks in parallel.

After examining the source code, it is found that:
After version 26, AsyncTask can be created in a background thread, because the default constructor uses a handler created by MainLooper.

 public AsyncTask() {
     this((Looper) null);
 }
 
 public AsyncTask(@Nullable Looper callbackLooper) {
        mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
            ? getMainHandler()
            : new Handler(callbackLooper);
// Omitted other code
}


AsyncTask has two thread pools, SerialExecutor and THREAD_POOL_EXECUTOR. The SerialExecutor thread pool is used for task queuing, while the THREAD_POOL_EXECUTOR thread pool is used for actual execution. There is also an InternalHandler that switches the execution environment from the thread pool to the main thread.

Let's take a look at the implementation of SerialExecutor:

private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null) {
                scheduleNext();
            }
        }

        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }

From the above code, we can analyze the queuing process of AsyncTask. First, the system encapsulates the params parameter of AsyncTask into a FutureTask object. FutureTask is a concurrent class and acts as a Runnable here. Then, this FutureTask is handed over to the execute method of SerialExecutor to process. The execute method of SerialExecutor first inserts the FutureTask object into the task queue mTasks. If there is no active AsyncTask task at this time, the scheduleNext method of SerialExecutor will be called to execute the next task. After the AsyncTask completes the task, it will continue to execute other tasks until all tasks are completed.

HandlerThread#

HandlerThread inherits from Thread. It is a type of thread that can use a handler. In the run method, it creates a message queue through Looper.prepare and starts the message loop through Looper.loop().

A regular Thread is mainly used to execute a time-consuming task in the run method, while HandlerThread creates a message queue internally, and external entities need to notify the HandlerThread to execute specific tasks through the message mechanism of Handler. The specific use case of HandlerThread is IntentService. Since the run method of HandlerThread is an infinite loop, when HandlerThread is no longer needed, it can be terminated by using quit or quitSafely to stop the thread execution.

IntentService#

IntentService is a special type of Service. It inherits from Service and is an abstract class that must be created as a subclass to be used. IntentService can be used to execute background time-consuming tasks, and it automatically stops after the tasks are completed. Because it is a service, it has a higher priority than a simple thread, making IntentService suitable for high-priority background tasks. IntentService encapsulates HandlerThread and Handler.

For each background task execution, an IntentService must be started. Inside the IntentService, tasks are requested to be executed by HandlerThread through messages. The Looper in the Handler processes messages sequentially, which means that IntentService also executes background tasks sequentially. When multiple tasks exist at the same time, the background tasks will be queued and executed in the order they were initiated.

onHandleIntent can handle different tasks. After onHandleIntent finishes execution, IntentService will attempt to stop the service through stopSelf. When IntentService is destroyed, the looper is stopped to prevent thread leakage.

Thread Pools in Android#

Advantages of thread pools:

  1. Reusing threads in the thread pool to avoid the performance overhead of creating and destroying threads.
  2. Effectively controlling the maximum concurrency of the thread pool to avoid blocking caused by threads competing for resources.
  3. Being able to manage threads easily and providing functions such as scheduled execution and interval looping.

ThreadPoolExecutor is the actual implementation of a thread pool. Its constructor provides a series of parameters to configure the thread pool. The meanings of each parameter are as follows:

  • corePoolSize
    The core number of threads in the thread pool. By default, core threads will always be alive in the thread pool, even if they are idle. If the allowCoreThreadTimeOut of ThreadPoolExecutor is set to true, idle core threads will have a timeout strategy when waiting for new tasks to arrive. The timeout interval is specified by keepAliveTime. If the waiting time exceeds keepAliveTime, the core thread will be terminated.

  • maximumPoolSize
    The maximum number of threads that the thread pool can accommodate. When the number of active threads reaches this value, subsequent new tasks will be blocked.

  • keepAliveTime
    The idle timeout duration for non-core threads. If the ThreadPoolExecutor's allowCoreThreadTimeOut is set to true, keepAliveTime also applies to core threads.

  • unit
    The time unit for keepAliveTime, which is an enumeration.

  • workQueue
    The task queue in the thread pool. Runnable objects submitted through the execute method of the thread pool will be stored in this parameter.

  • threadFactory
    The thread factory provides the ability to create new threads for the thread pool. ThreadFactory is an interface with only one method: new Thread(Runnable r).

When executing tasks, ThreadPoolExecutor roughly follows the following principles:

  1. If the number of threads in the thread pool has not reached the core thread count, a core thread will be started to execute the task.
  2. If the number of threads in the thread pool has reached or exceeded the core thread count, the task will be inserted into the task queue and wait for execution.
  3. If in step 2, the task cannot be inserted into the task queue, it is often because the task queue is full. At this time, if the number of threads has not reached the maximum value specified by the thread pool, a non-core thread will be immediately started to execute the task.
  4. If the number of threads in step 3 has reached the maximum value specified by the thread pool, the task will be rejected. ThreadPoolExecutor will call the rejectExecution method of RejectedExecutionHandler to notify the caller.
Classification of Thread Pools#
  1. FixedThreadPool
    Created using the newFixedThreadPool method of Executors. It is a thread pool with a fixed number of threads, only consisting of core threads, and the core threads will not be terminated. It can quickly respond to external requests.

  2. CachedThreadPool
    Created using the newCachedThreadPool method of Executors. It is a thread pool with an indefinite number of threads, consisting only of non-core threads. The maximum number of threads is Integer.MAX_VALUE, and idle threads will be terminated after 60 seconds. It is suitable for executing a large number of tasks with low time consumption.

  3. ScheduledThreadPool
    Created using the newScheduledThreadPool method of Executors. The number of core threads in this thread pool is fixed, and there is no limit on non-core threads. When non-core threads are idle, they will be immediately terminated. It is suitable for executing scheduled tasks and tasks with periodic repetition.

  4. SingleThreadExecutor
    Created using the newSingleThreadExecutor method of Executors. There is only one core thread in the thread pool, ensuring that all tasks are executed in the same thread in order. This eliminates the need for thread synchronization between tasks.

In addition to the above four types, thread pools can be flexibly configured according to needs.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.