Controlling the level of concurrency

So far, we've carefully avoided being too specific about what exactly happens when we invoke AsyncTask's execute method. We know that doInBackground will execute off the main thread, but what exactly does that mean?

The original goal of AsyncTask was to help developers avoid blocking the main thread. In its initial form at API level 3, AsyncTasks were queued and executed serially (that is, one after the other) on a single background thread, guaranteeing that they would complete in the order they were started.

This changed in API level 4 to use a pool of up to 128 threads to execute multiple AsyncTasks concurrently with each other—a level of concurrency of up to 128. At first glance, this seems like a good thing, since a common use case for AsyncTask is to perform blocking I/O, where the thread spends much of its time idly waiting for data.

However, as we saw in Chapter 1, Building Responsive Android Applications, there are many issues that commonly arise in concurrent programming, and indeed, the Android team realized that by executing AsyncTasks concurrently by default, they were exposing developers to potential programming problems (for example, when executed concurrently, there are no guarantees that AsyncTasks will complete in the same order they were started).

As a result, a further change was made at API level 11, switching back to serial execution by default, and introducing a new method that gives concurrency control back to the app developer:

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params… params)

From API level 11 onwards, we can start AsyncTasks with executeOnExecutor, and in doing so, choose the level of concurrency for ourselves by supplying an Executor object.

Executor is an interface from the java.util.concurrent package of the JDK, and was first introduced in Java 5. Its purpose is to present a way to submit tasks for execution without spelling out precisely how or when the execution will be carried out.

Implementations of Executor may run tasks sequentially using a single thread, use a limited pool of threads to control the level of concurrency, or even directly create a new thread for each task.

The AsyncTask class provides two Executor interfaces that allow you to choose between the concurrency levels described earlier in this section:

  • SERIAL_EXECUTOR: This Executor queues tasks and uses a single background thread to run them to completion, each in turn in the order they were submitted.
  • THREAD_POOL_EXECUTOR: This Executor runs tasks using a pool of threads for efficiency (starting a new thread comes with some overhead cost that can be avoided through pooling and reuse). THREAD_POOL_EXECUTOR is an instance of the JDK class ThreadPoolExecutor, which uses a pool of threads that grows and shrinks with demand. In the case of AsyncTask, the pool is configured to maintain at least five threads, and expands up to 128 threads.

To execute AsyncTask using a specific executor, we invoke the executeOnExecutor method, supplying a reference to the executor we want to use, for example:

task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);

As the default behavior of execute since API level 11 is to run AsyncTasks serially on a single background thread, the following two statements are equivalent:

task.execute(params);
task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, params);

Besides the default executors provided by AsyncTask, we can choose to create our own. For example, we might want to allow some concurrency by operating off a small pool of threads, and allow many tasks to be queued if all threads are currently busy.

This is easily achieved by configuring our own instance of ThreadPoolExecutor as a static member of one of our own classes—for example, our Activity class. Here's how we might configure an executor with a pool of four to eight threads and an effectively infinite queue:

private static final Queue<Runnable> QUEUE =
  new LinkedBlockingQueue<Runnable>();
public static final Executor MY_EXECUTOR =
  new ThreadPoolExecutor(4, 8, 1, TimeUnit.MINUTES, QUEUE);

The parameters to the constructor indicate the core pool size (4), the maximum pool size (8), the time for which idle additional threads may live in the pool before being removed (1), the unit of time (minutes), and the queue to use when the pool threads are busy.

Using our own Executor is then as simple as invoking our AsyncTask as follows:

task.executeOnExecutor(MY_EXECUTOR, params);
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.19.212