Using a Thread Pool in Android

Android上使用线程池

In my last post Using HanderThread in Android, I showed how to offload short blocking tasks to a worker thread. While HandlerThread is good for tasks running in a sequential manner, there are cases where background tasks do not have dependencies on each other. To get these tasks done as quickly as possible, you might want to exploit the powerful multi-core processor on the mobile device and run the tasks concurrently on more than one worker thread.

A thread pool is a good fit for this scenario. Thread pool is a single FIFO task queue with a group of worker threads. The producers (E.g. the UI thread) sends tasks to the task queue. Whenever any worker threads in the thread pool become available, they remove the tasks from the front of the queue and start running them.

Comparing with starting a random number of individual worker threads, using a thread pool prevent the overhead of killing and recreating threads every time a worker thread is needed. It also gives you fine control over the number of threads and their lifecycle. E.g. ThreadPoolExecutor allows you to specify how many core threads, how many max threads the pool should create and the keep alive time for the idle threads.

Android supports Java’s Executor framework which offers the following classes for using a thread pool.

  • Executor: an interface which has a execute method. It is designed to decouple task submission from running.
  • Callable: An Interface similar to runnable but allow a result to be returned.
  • Future: Like a promise in JavaScript. It represents the result for an asynchronous task.
  • ExecutorService: an interface which extends Executor interface. It is used to manage threads in the threads pool.
  • ThreadPoolExecutor: a class that implements ExecutorService which gives fine control on the thread pool (Eg, core pool size, max pool size, keep alive time, etc.)
  • ScheduledThreadPoolExecutor: a class that extends ThreadPoolExecutor. It can schedule tasks after a given delay or periodically.
  • Executors: a class that offers factory and utility methods for the aforementioned classes.
  • ExecutorCompletionService: a class that arranges submitted task to be placed on a queue for accessing results.

Basic Thread Pool

The simplest way of creating a thread pool is to use one of the factory methods from Executors class.

static final int DEFAULT_THREAD_POOL_SIZE = 4;

ExecutorService executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE);

ExecutorService executorService = Executors.newCachedThreadPool();

ExecutorService executorService = Executors.newSingleThreadExecutor();

newFixedThreadPool creates a thread pool with a a fixed number of thread in the pool specified by the user. The user can call `setCorePoolSized(int)` later to resize the thread pool.

newCachedThreadPool creates a new thread when there is a task in the queue. When there is no tasks in the queue for 60 seconds, the idle threads will be terminated.

newSingleThreadExecutor creates a thread pool with only one thread.

To add a task to the thread pool, call one of the following methods.

executorService.execute(new Runnable(){
  @Override
  public void run(){
    callBlockingFunction();
  }
});

Future future = executorService.submit(new Callable(){
  @Override
  public Object call() throws Exception {
    callBlockingFunction();
    return null;
  }
});

The second method returns a future object. It can be used to retrieve the result from the callable by calling future.get() or cancel the task by calling future.cancel(boolean mayInterruptIfRunning).

Advanced Thread Pool

If you want to have finer control over the thread pool, ThreadPoolExecutor class can be used. In the following example, I first find the available processors of the phone. The thread pool is configured to have core size as the NUMBER_OF_CORES, the maximum core size as the NUMBER_OF_CORES x 2, idle threads’ keep-alive time as 1 second, task queue as a LinkedBlockingQueue object and a custom thread factory.

int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
int KEEP_ALIVE_TIME = 1;
TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>();

ExecutorService executorService = new ThreadPoolExecutor(NUMBER_OF_CORES, 
                                                          NUMBER_OF_CORES*2, 
                                                          KEEP_ALIVE_TIME, 
                                                          KEEP_ALIVE_TIME_UNIT, 
                                                          taskQueue, 
                                                          new BackgroundThreadFactory());
                                                          
private static class BackgroundThreadFactory implements ThreadFactory {
  private static int sTag = 1;

  @Override
  public Thread newThread(Runnable runnable) {
      Thread thread = new Thread(runnable);
      thread.setName("CustomThread" + sTag);
      thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND);

      // A exception handler is created to log the exception from threads
      thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
          @Override
          public void uncaughtException(Thread thread, Throwable ex) {
              Log.e(Util.LOG_TAG, thread.getName() + " encountered an error: " + ex.getMessage());
          }
      });
      return thread;
  }
}

Cancel Tasks

To stop the tasks in the task queue from execution, we just need to clear the task queue. To allow the running threads to be stopped, store all future objects in a list and call cancel on every object which is not done.

// Add a callable to the queue, which will be executed by the next available thread in the pool
public void addCallable(Callable callable){
    Future future = mExecutorService.submit(callable);
    mRunningTaskList.add(future);
}

/* Remove all tasks in the queue and stop all running threads
 * Notify UI thread about the cancellation
 */
public void cancelAllTasks() {
    synchronized (this) {
        mTaskQueue.clear();
        for (Future task : mRunningTaskList) {
            if (!task.isDone()) {
                task.cancel(true);
            }
        }
        mRunningTaskList.clear();
    }
    sendMessageToUiThread(Util.createMessage(Util.MESSAGE_ID, "All tasks in the thread pool are cancelled"));
}

Handle Activity Lifecycle

One thing the thread pool framework does not handle is the Android activity lifecycle. If you want your thread pool to survive the activity lifecycle and reconnect to your activity after it is re-created (E.g. after an orientation change), it needs to be created and maintained outside the activity.

In my example, I made a static singleton class called CustomThreadPoolManager. It has a private constructor. It creates an instance of itself and return that single instance in the static getInstance method. It also holds a weak reference to the Activity. The reference is later used to communicate with the UI thread (see the next section).

public class CustomThreadPoolManager {

    private static CustomThreadPoolManager sInstance = null;
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
    private static final int KEEP_ALIVE_TIME = 1;
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT;

    private final ExecutorService mExecutorService;
    private final BlockingQueue<Runnable> mTaskQueue;
    private List<Future> mRunningTaskList;

    private WeakReference<UiThreadCallback> uiThreadCallbackWeakReference;

    // The class is used as a singleton
    static {
        KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
        sInstance = new CustomThreadPoolManager();
    }

    // Made constructor private to avoid the class being initiated from outside
    private CustomThreadPoolManager() {
        // initialize a queue for the thread pool. New tasks will be added to this queue
        mTaskQueue = new LinkedBlockingQueue<Runnable>();
        mRunningTaskList = new ArrayList<>();
        mExecutorService = new ThreadPoolExecutor(NUMBER_OF_CORES, 
                                                NUMBER_OF_CORES*2, 
                                                KEEP_ALIVE_TIME, 
                                                KEEP_ALIVE_TIME_UNIT, 
                                                mTaskQueue, 
                                                new BackgroundThreadFactory());
    }

    public static CustomThreadPoolManager getsInstance() {
        return sInstance;
    }

    ...

    // Keep a weak reference to the UI thread, so we can send messages to the UI thread
    public void setUiThreadCallback(UiThreadCallback uiThreadCallback) {
        this.uiThreadCallbackWeakReference = new WeakReference<UiThreadCallback>(uiThreadCallback);
    }

    ...

}

In the Activity, get the thread pool singleton instance by calling the getInstance static method. Set the activity to the CustomThreadPoolManager. As CustomThreadPoolManager keeps the reference to the Activity as a weak reference, you don’t need to worry about leaking the Activity.

public class MainActivity extends AppCompatActivity implements UiThreadCallback {
    private CustomThreadPoolManager mCustomThreadPoolManager;
    ...
     @Override
    protected void onStart() {
        super.onStart();
        // get the thread pool manager instance
        mCustomThreadPoolManager = CustomThreadPoolManager.getsInstance();
        // CustomThreadPoolManager stores activity as a weak reference. No need to unregister.
        mCustomThreadPoolManager.setUiThreadCallback(this);
    }
    // onClick handler for Send 4 Tasks button
    public void send4tasksToThreadPool(View view) {
        for(int i = 0; i < 4; i++) {
            CustomCallable callable = new CustomCallable();
            callable.setCustomThreadPoolManager(mCustomThreadPoolManager);
            mCustomThreadPoolManager.addCallable(callable);
        }
    }
    ...
}

Communicate with UI Thread

When each task finishes, you may need to send some data back to the UI thread. A safe way of doing this is to send a message to the handler of the UI thread. First, extend Handler class and define what the UI thread should do when a message is received.

UiHandler mUiHandler;
...

@Override
protected void onStart() {
    super.onStart();

    // Initialize the handler for UI thread to handle message from worker threads
    mUiHandler = new UiHandler(Looper.getMainLooper(), mDisplayTextView);
    ...
}
...
// Send message from worker thread to the UI thread
@Override
public void publishToUiThread(Message message) {
    // add the message from worker thread to UI thread's message queue
    if(mUiHandler != null){
        mUiHandler.sendMessage(message);
    }
}
...
// UI handler class, declared as static so it doesn't have implicit
// reference to activity context. This helps to avoid memory leak.
private static class UiHandler extends Handler {
    private WeakReference<TextView> mWeakRefDisplay;

    public UiHandler(Looper looper, TextView display) {
        super(looper);
        this.mWeakRefDisplay = new WeakReference<TextView>(display);
    }

    // This method will run on UI thread
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what){
            // Our communication protocol for passing a string to the UI thread
            case Util.MESSAGE_ID:
                Bundle bundle = msg.getData();
                String messageText = bundle.getString(Util.MESSAGE_BODY, Util.EMPTY_MESSAGE);
                if(null != mWeakRefDisplay) {
                    TextView refDisplay = mWeakRefDisplay.get();
                    if(null != refDisplay) {
                        mWeakRefDisplay.get().append(Util.getReadableTime() + " " + messageText + "\n");
                    }
                }
                break;
            default:
                break;
        }
    }
}

In the CustomThreadPoolManager, use the Activity’s weak reference to send the message to the UI thread.

public void sendMessageToUiThread(Message message){
    if(null != uiThreadCallbackWeakReference) {
        final UiThreadCallback uiThreadCallback = uiThreadCallbackWeakReference.get();
        if(null != uiThreadCallback) {
            uiThreadCallback.publishToUiThread(message);
        }
    }
}

In the CustomCallable, as it has reference to the CustomThreadPoolManager, it can send the message by calling CustomThreadPoolManager’s sendMessageToUiThread method.

@Override
public Object call() throws Exception {
    try {
        // check if thread is interrupted before lengthy operation
        if (Thread.interrupted()) throw new InterruptedException();
    
        // In real world project, you might do some blocking IO operation
        // In this example, I just let the thread sleep for 3 second
        Thread.sleep(3000);
    
        // After work is finished, send a message to CustomThreadPoolManager
        Message message = Util.createMessage(Util.MESSAGE_ID, "Thread " +
                String.valueOf(Thread.currentThread().getId()) + " " +
                String.valueOf(Thread.currentThread().getName()) + " completed");
    
        if(null != mCustomThreadPoolManagerWeakReference) {
                final CustomThreadPoolManager customThreadPoolManager = mCustomThreadPoolManagerWeakReference.get();
                if(null != customThreadPoolManager) {
                    customThreadPoolManager.sendMessageToUiThread(message);
                }
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return null;
}

Source Code

The full source code for the example used in this post is available on Github.

也可本站下载一份 代码拷贝

参考链接


Using a Thread Pool in Android

发布者

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注