Using Multi-Core Processors Wisely
It’s well-known that multi-core processors are common to all new devices. Our Android apps can get the best of those processors by executing multiple threads in parallel. For instance, if the device has eight cores, we can execute eight tasks at the same time in eight different threads, with each thread using a different CPU. But, in practice, this is not entirely true.
Threading
Android has many ways to execute tasks using multiple threads: Thread class (Tip: Don’t use it!), AsyncTask, Executor, IntentService, Handler, Loader, and more.
Our team always tries to use the official Android classes or we let the third-party libraries manage the threading. For some specific cases we use the Executor in Java’s concurrent library.
Executors and Processor Cores
Java’s concurrent library has many tools to handle multi-threading. This library was made by experts and maintained with bug fixes for every new Java version over the years.
ThreadPoolExecutor is the most used class in the concurrent library. It’s very powerful. You can do things like set the maximum and minimum number of active threads, set how long the threads will be active, set priorities, and use custom queues for tasks.
One common way to create an Executor for ordinary tasks is to set the number of threads based on the number of available processors. For example:
executor = Executors
.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1)
That creates a ThreadPoolExecutor with a specific number of threads. In this case, a processor with four cores will use five threads, a processor with eight cores will use nine threads, and so on. Here’s the tricky part, the documentation of the method availableProcessors() says:
This value may change during a particular invocation of the virtual machine. Applications that are sensitive to the number of available processors should therefore occasionally poll this property and adjust their resource usage appropriately.
That means it could return a number lower than the real number of cores. This is because sometimes the cores are “sleeping” to reduce battery consumption, and our app or activity creates an Executor with a number of threads that is optimal for that moment but could be suboptimal a few minutes later.
A Real Use Case
The user receives a push message when the device is sleeping. He touches the notification. The OS opens our app using a small number of cores to reduce battery consumption (since the device was sleeping), but the user can continue using the app for some minutes or send it to background where it could continue executing tasks. At the same time the processor could wake up more cores, but our app will continue using a small number of threads. It doesn’t look good to only use two threads in a processor with 16 cores.
Luckily the ThreadPoolExecutor has a method to set the number of threads after it was created without affecting the current tasks being enqueued or executed.
setCorePoolSize(int)
To use an optimal number of threads, we can set this number before any task is enqueued, forcing the Executor to increase or decrease the threads based on the current number of active cores. It’s up to you whether to put this in a common static method or extend the Executor class.
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() + 1);
executor.execute(task);
Learn More from Other Experts
For multi-threading, we always use the classes provided by Android or the third-party libraries. For some specific cases a ThreadPoolExecutor is a great alternative, but make sure it adapts to the state of the device processor.
Also, learn from the experts. Colt McAnlis is a developer advocate at Google (ex-Blizzard multi-threaded rendering dev) and has many videos explaining the deep secrets about threading in Android. Some of our favorites include:
For more insights on design and development, subscribe to BPXL Craft and follow Black Pixel on Twitter.
Black Pixel is a creative digital products agency. Learn more at blackpixel.com.