For the past couple of years, I’ve been using ThreadPoster library for multithreading in Android application written in Java. It’s simple, explicit and unit-testable approach that spares you a lot of headache.
You can find the source code, the examples and the technical documentation of ThreadPoster on GitHub.
In this post, I’ll describe what ThreadPoster is and why I wrote this library. In addition, I’ll contrast ThreadPoster with other popular approaches to multithreading in Android and explain why I believe it to be a better alternative.
Multithreading Landscape in Android
When it comes to multithreading in Android, we have many alternatives to choose from: bare Thread class, thread pools, AsyncTask, futures, RxJava, Kotlin Coroutines, etc. There is really no shortage of options in this regard. So, why I decided to write one more library then?
To answer this question, I’d like to start by discussing other approaches to multithreading and explain why I find each one of them non-optimal.
Android framework provides a bunch of different multithreading constructs. This set includes the notorious AsyncTask, the combo of Handler and HandlerThread, IntentService, Loaders and, probably, some other approaches.
When I made my first steps in Android development, I spent countless hours on learning these solutions, fixing all kinds of bugs and, finally, refactoring all of these classes out of my applications. Once popular AsyncTask framework has already been deprecated, but don’t make a mistake – all of these components are poorly designed and over-complicated.
To put it bluntly: in my opinion, all “Android native” multithreading constructs are terrible and you should really stay away of them.
Once I had gotten burned by Android’s multithreading constructs, I decided to use the standard Thread class directly. As a result, my multithreaded code became more explicit and bugs became easier to find and fix. I still used Handler to return execution to UI thread (because there is no other way to do that), but any background work was delegated directly to Thread objects.
Dealing with Thread’s required understanding of basic multithreading concepts, but they were easier to learn than deciphering the official Android docs for each of the aforementioned Android classes. In addition, this understanding is universal and, as will be discussed later, it’s required even when you use other approaches because concurrency is a complex topic.
At some point, I had to implement a complex piece of highly-concurrent logic. I still could get away with bare Threads, but it would be non-optimal. So I started using ThreadPoolExecutor.
ThreadPoolExecutor turned out to be very powerful and versatile approach to multi-threading and it supported all my needs and use cases. However, after some time, I realized that this approach has two major drawbacks. Let’s discuss these drawbacks in detail.
ThreadPoolExecutor is too Complex for Most Android Applications
ThreadPoolExecutors are like light sabers in terms of power and complexity: properly trained Jedis can deflect laser beams with their light sabers, but I’d probably cut my hands off if I’d try to use one myself.
The root of ThreadPoolExecutor’s complexity lies in the fact that this API should be as universal as Java itself. Whether you want to build a scalable backend infrastructure, write a program for embedded micro-controller, or develop Android application, ThreadPoolExecutor should accommodate your needs and constraints in all these environments.
However, the needs in Android are relatively basic. You don’t need to handle thousands of simultaneous connections like backend servers do, and you don’t need to guard against DDOS attacks. You don’t have severe resources and environment limitations like some embedded systems do. In most cases, you don’t even need to protect your app from excessive load because OS will just kill it if it misbehaves (you’ve got crash reporting set up, right?).
Therefore, in Android, the power of ThreadPoolExecutor is mostly an excessive complexity that is rarely needed and often causes big problems.
You might think that I’m exaggerating now. Not at all. For example, consider ThreadPoolExecutor’s configuration that backs the notorious AsyncTask. It started with some seemingly random configuration that was tweaked in 2009. Then, in 2013, these parameters were changed once again. After two adjustments of thread pool’s configuration, you’d expect it to be configured as required, but this configuration gave rise to a host of issues over the years, including crashing the official Android settings application. At this point, given the severity of the bug, you’d expect the configuration to be tweaked once again, but Google’s solution was simpler – they simply backed off and switched to serial execution.
I’m not telling you all of this to make fun of Google, but to show you that even the engineers who built Android couldn’t properly configure ThreadPoolExecutor for years. In light of this fact, asking regular developers to do that would be insane. Especially taking into account that in absolute majority of cases it’s not even required.
I don’t use RxJava and it’s not a secret that I don’t like it. Some developers claim that Rx abstracts out the messy details of concurrency, but I don’t think that’s possible. Concurrency is a fundamental and very difficult concept, which just can’t be abstracted out completely.
That said, you can easily “hide” the complexity of concurrency and “pretend” that it’s easier than it really is. This might work for some simple scenarios, but, sooner or later, the reality will hit you in the face in the form of bugs that will be exceptionally hard to find and fix.
Joel Spolsky called this concept The Law of Leaky Abstractions:
All non-trivial abstractions, to some degree, are leaky.
And abstracting concurrency is hell of a non-trivial task.
My problem with RxJava, in context of multithreading is that it adds much of its own complexity into the picture while providing very questionable benefits. It make the concurrency easy in simple cases, but requires you to think about both concurrency and its own concepts when dealing with complicated tasks. RxJava-based code is also very non-intuitive and challenging to debugging.
RxJava looks good when compared to Android native constructs, but that’s it.
To give the devil its due, I’d like to note that many developers whom I hold in high regard, including Jake Wharton, disagree with my opinion. Therefore, take this opinion with a grain of salt.
In my opinion, if you write Android applications in Kotlin, Coroutines should be your default choice for concurrency stuff. Not because Coroutines are “good”, but because they’re the de facto standard in Kotlin world.
However, Coroutines is exceptionally complex concurrency framework which has very steep learning curve and is very tricky to utilize correctly even for experienced developers. That’s why some teams decide to pass on Coroutines even if they work on Kotlin-only projects.
The main problem with Coroutines, of course, is the fact that you can’t use them in projects written in Java. Therefore, even though Kotlin has been around for 4 years and many Android developers think that Java is dead, there are still many projects for which Coroutines is simply not an option.
Now that I explained why I find other popular approaches to multithreading in Android non-optimal, I can finally discuss ThreadPoster library.
Mind you that I didn’t develop this library intentionally. I just solved my issues and addressed various concerns raised by my colleagues over a long period of time. Then I noticed that this approach resulted in a bunch of classes that I basically copy-paste between projects. Extracting them into a library seemed like a natural thing to do, so I did.
When I think about the philosophy behind ThreadPoster, I can see three main pillars described below.
I don’t believe that multithreading (and concurrency in general) can be simplified or abstracted. It’s too fundamental and complex. Therefore, I prefer to have it explicit in the codebase.
Explicit multithreading provides the following benefits:
- Multithreading becomes easy to spot. If you use ThreadPoster consistently, you can even find all “multithreading boundaries” by searching for usages of ThreadPoster’s constructs.
- Explicit multithreading is easier to reason about from the first principles because there are no additional operators involved. It’s very handy if you need to look for concurrency bugs.
- With explicit multithreading, you can handle all requirements and there are almost no limitations.
All in all, I wanted myself and my colleagues to be actively aware of the fact that we write multithreaded code and be mindful about it.
The second pillar of ThreadPoster’s philosophy is simple setup.
As I already said, Android applications rarely ever have special constraints in context of multithreading. If you write functionally correct multithreaded code without concurrency bugs, it will work. It’s not the case in e.g. web backends where you need to be very mindful about your resources once you reach a certain scale.
In addition, multithreading in Android is very common. You can build feature complete and functional web frontends and backends without ever doing manual multithreading, but in Android you run into multithreading right from the beginning. Of course, you can use libraries that take care of concurrency and expose simple callbacks, but there is still always something that requires you to roll up your sleeves and handle it yourself.
Given the prominence of multithreading in Android and the fact that there are no special constraints, I wanted multithreading solution to be as simple to set up as possible. ThreadPoster is almost trivially simple in this context because it requires no setup at all.
To use ThreadPoster you just need to make its constructs global and share the same instances among all clients. There are several alternative approaches to achieve this goal, but I strongly recommend to use proper dependency injection architectural pattern to achieve that.
I’m a big proponent of unit testing and test driven development. In fact, I don’t even bother to make a distinction between them. As far as I’m concerned, TDD is the only way to adopt unit testing culture in a professional setting. Therefore, I’ve been searching for ways to unit test multithreaded code for quite some time.
However, both unit testing and multithreading are challenging topics. The intersection of these two topics results in what might very well be one of the most challenging practices in software development: multithreaded unit testing. This practice is so complicated, that many developers believe that it’s just impossible.
Let me make myself clear here. I’m talking about real multithreaded unit testing. Replacing production multithreaded execution with simpler execution schemes in unit tests can work in certain situations, but I never found too much confidence in this practice. Therefore, I’ve been searching for ways to make unit tests run in truly multithreaded environment, similar to production code. This goal was especially challenging in Android due to its special UI thread.
ThreadPoster makes multithreaded unit testing in Android feasible under certain (simple) constraints.
I have absolutely no intention to try and convince you that ThreadPoster is perfect. It’s not.
In the previous section I summarized the philosophy behind this library and listed its benefits. Now I’d like to list the drawbacks that I’m aware of. This will allow you to understand the trade-offs and make an educated decision regarding ThreadPoster adoption.
No “simple” multithreading
As I told you multiple times, I don’t believe that multithreading can be simple. However, its complexity can be hidden in some circumstances. For example, AsyncTask allows new developers to write concurrent code without understanding concurrency at all. They just implement some methods and magic happens. In simple cases, this approach can work alright.
With ThreadPoster, by making concurrency explicit, I took another approach. Therefore, if you want to use ThreadPoster, you’ll need to understand what multithreading is about.
No proof of thread-safety in unit tests
It’s important to understand that no amount of unit testing can prove that the code doesn’t contain concurrency bugs. Therefore, even if all unit tests written with ThreadPoster pass, you can still have race conditions, deadlocks, livelocks, etc. At best, multithreaded unit tests can show you that your multithreaded code is functionally correct.
That’s not something special to ThreadPoster, but a fundamental feature of concurrency. Since it involves randomness, it can’t be proven correct using empiric unit tests. However, let’s recall what Albert Einstein (supposedly) said:
No amount of experimentation can ever prove me right; a single experiment can prove me wrong.Albert Einstein
The same applies to multithreaded unit testing: it can’t prove that the code is free of concurrency bugs, but it has a chance to find such a bug. Let me elaborate on this point.
You will probably be running your test suite many times during the life-time of the project. If there is a concurrency bug in your code and you unit test with ThreadPoster, you have a chance of hitting the bug in the test. It will manifest itself as “flaky” test (i.e. test that fails occasionally for seemingly no reason). So, if you use ThreadPoster and notice a flaky test, that might be an indication of concurrency bug.
Proper unit testing should result in you being very confident about the quality of tested classes. However, if you write multithreaded unit tests, also remember that concurrency bugs can hide in your code even if all tests pass.
Long unit tests execution times
Due to the fact that ThreadPoster allows for truly multithreaded unit testing, unit tests that use its test-doubles are slower than the regular unit tests. On my machine, each test case that involves multithreading takes on the order of 10ms. It doesn’t sound too bad, but if you have 1000 multithreaded test cases (which might happen in big projects), then it adds 10s to the execution time of your test suite.
In my opinion that’s not a deal breaker because most application wouldn’t get to this amount of multithreaded test cases, but it’s something to keep in mind nonetheless.
So, that’s all I wanted to say about multithreading in Android and my library called ThreadPoster.
If you take a look at ThreadPoster’s source code, you’ll probably get a feeling that it’s trivially simple. Except for its test-doubles, there is no “clever” code in there. It’s not a coincidence. Code that looks simple is my definition of a good code.
As always, don’t hesitate to leave you comments and questions below.