In this post I’m introducing ThreadPoster – my new open source library for explicit and unit-testable multi-threading in Android.
The source code, the code examples and the technical documentation are hosted on GitHub. Check them out to get a quick impression of the library. In this article I’ll discuss the more general topics of motivation and philosophy behind ThreadPoster, and contrast it with other popular solutions.
I’m releasing ThreadPoster to the public now, but the approach encapsulated in this library is not new. I’ve been using it for the past two years in my personal and professional projects. In fact, even the library itself is not new – I uploaded the code to GitHub and had the first version on Bintray about a year ago. Since then it has been lying there quietly, waiting for me to make a final go/no-go decision.
It took me a whole year until I decided to make this announcement because I wanted to be absolutely sure that I don’t just throw another half-baked solution out there. Now I’m confident that ThreadPoster is production ready.
Multi-Threading Landscape in Android
When it comes to multi-threading in Android, there is really no shortage of options to choose from. Therefore, at this point, you might be asking yourself: why do we need yet another multi-threading library?
That’s totally appropriate question to ask.
To answer the aforementioned question, I’d like to start by reviewing the “multi-threading landscape” in Android development. I’ll list what I think to be the most widespread solutions in the following sections and explain why I find each one of them non-optimal.
I’d like to emphasize that the discussion below contains my personal opinions, not objective facts. Keep that in mind.
Android framework provides many different multi-threading constructs. I should probably be politically correct here and tell you that they are non-optimal, etc., but I will tell you what I really think: they are terrible.
They are terrible today and they always were. In fact, in my opinion, all of them were destined to be terrible from the very onset, the first design stages, long before they were released to the public.
This category includes AsyncTask, the combo of Handler and HandlerThread, IntentService, Loaders and, probably, a bunch of other approaches.
When I decided to become an Android developer, I read about all of these approaches in the official documentation. From there I learned to use AsyncTask to execute simple network requests, to use Handler to manage Bluetooth connection, to query ContentProviders using Loaders and to “offload tasks from an application’s main thread” to IntentService.
I will never get back the countless hours I spent on deciphering the official docs, implementing these ridiculous solutions, debugging them and, finally, refactoring all of them out of my application and setting myself free. However, I can tell you about that experience in a hope that you’ll not repeat my mistake.
The main problem with Android native multi-threading constructs is that they are too complex and there are too many of them. It seems like the authors tried to make the multi-threading easier, but, instead, ended up creating a bunch of limited, complicated and, sometimes, error-prone solutions.
After I got disillusioned with Android’s multi-threading constructs, I decided to use Thread class directly. So, for quite some time I constructed and started my background threads manually.
As a result of switching to Threads, multi-threading became more explicit and bugs became easier to find and fix. I still used Handler to return execution to UI thread (because, AFAIK, there is simply no other way), but that’s all. All other background processing was taken care of by simple Threads.
And you know what? It was much better experience than working with the aforementioned Android APIs.
Dealing with Threads required some understanding of multi-threading concepts, but it was easier to learn that once rather 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 can’t be abstracted completely.
At some point, I had to implement quite complex piece of multi-threaded logic. I still could get away with direct Threads management, but it would be painful and error-prone. 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 is still not optimal due to two major drawbacks.
Let’s discuss these drawbacks in details.
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 the complexity is in the fact that ThreadPoolExecutor 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 very basic and there are almost no constraints.
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 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 be thinking that I’m exaggerating now. Not at all. For example, let’s review ThreadPoolExecutor’s configuration that backs the notorious AsyncTask.
It started with some seemingly random configuration that was tweaked in 2009. This tweak looks reasonable, but it’s unclear how the parameters for the new configuration were chosen. Then, in 2013, these parameters were changed once again. How the new values were chosen still remained a mystery.
After years of development and two tweaks to configuration, I’d expect this ThreadPoolExecutor to work seamlessly. However, AsyncTask is misconfigured to this day.
This misconfiguration 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, I’d expect the configuration to be tweaked once again, but Google’s solution was simpler – they simply backed off and switched to serial execution.
And there is more to it. Recently I learned about ModernAsyncTask class in support library. The configuration of its ThreadPoolExecutor is basically the previous configuration of the original AsyncTask (the one that was changed in 2013). Go figure.
I’m not telling you all of this to make fun of Google, but to convince you that if even the engineers who built Android couldn’t properly configure ThreadPoolExecutor after years of experience, then asking regular developers to do that would be insane.
Especially taking into account that in absolute majority of cases it’s not really required.
Executor’s design violates Liskov Substitution Principle
Liskov Substitution Principle states that if a piece of logic works with some class A, then it should also work with all sub-classes of A. In other words, all sub-classes must ensure full functional and semantic compatibility with their parent. Such a compatibility requires much more than just inheriting public API.
The documentation of Executor interface, which is implemented by ThreadPoolExecutor, states:
This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc.
Unfortunately, it’s impossible to fully abstract out the details of threading and scheduling.
For example, heavily concurrent code that works seamlessly with an unbounded ThreadPoolExecutor isn’t guaranteed to work if Executor is switched to serial execution on a single thread. In fact, it would be surprising if such a change would result in anything but a deadlock.
Since Executor interface provides no guarantees about interoperability of its implementations, it violates LSP by design. The aforementioned example of official Android settings application crash is an indirect consequence of this violation.
I don’t know whether LSP violation was an educated trade-off made by original Java language designers, or it was yet another mistake. However, regardless of the historical decisions, any code that interfaces with ThreadPoolExecutor is at risk of being silently broken. It can happen either initially, or as a result of further development or maintenance, and this error can be extremely difficult to debug.
Therefore, even though ThreadPoolExecutor covered all bases for me personally, in light of the drawbacks described above, my advice to Android developers would be to avoid using it directly.
Sure, if you want to go down the rabbit hole and really learn how to use ThreadPoolExecutor – go for it. However, in my experience, most developers greatly underestimate the difficulty of this task. After all, the authors of Android couldn’t get it right by themselves.
I don’t use RxJava and it’s not a secret that I don’t like it. Most of my issues with this framework are not related to multi-threading which makes them irrelevant in this context.
As far as I can tell, as of today, when it comes to multi-threading most Android developers choose between Android native multi-threading constructs and RxJava. Given these two alternatives, Rx is much, much better choice. However, I myself had already been past Android native multi-threading constructs when I learned about RxJava.
Some developers claim that Rx abstracts out the messy concurrency details, but I simply don’t believe that it’s possible. Concurrency is a fundamental and very difficult concept. In my opinion, you can’t abstract it out and the aforementioned description of Executor’s violation of LSP is an example of what happens if you’d try to.
That said, you can easily “hide” concurrency complexity 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 multi-threading, 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. Especially 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 and deeply respect, including Jake Wharton by himself, disagree with my opinion. I get a lot of heat from the community members when I express it. Therefore, take this opinion with a grain of salt.
I don’t use Kotlin (yet?), but, even if I would, I wouldn’t dare to jump on coroutines train any time soon.
As I already said multiple times, concurrency is extremely challenging topic. In my opinion, all attempts to simplify and abstract the concurrency in Android failed. Moreover, I don’t think that it’s theoretically possible to do that.
Coroutines is yet another attempt to make concurrency look simpler than it is. More importantly, coroutines kind of “invert” concurrency and try to make concurrent code look non-concurrent. As far as I’m concerned, that’s really bad move.
See, I have several years of experience with Java concurrency. I invested a lot of time into self-education, made lots of mistakes and now, finally, I can say that I understand concurrency somewhat. I wouldn’t dare to call myself concurrency expert though. At some point in the past I thought that I know everything about concurrency, but then reality exposed my arrogance. So, the best I can say about myself after several years of experience is that I know concurrency somewhat.
And here come coroutines and suggest a different mental model. The official documentation states:
Coroutines simplify asynchronous programming by putting the complications into libraries. The logic of the program can be expressed sequentially in a coroutine, and the underlying library will figure out the asynchrony for us. The library can wrap relevant parts of the user code into callbacks, subscribe to relevant events, schedule execution on different threads (or even different machines!), and the code remains as simple as if it was sequentially executed.
So, “simplify asynchronous programming” and “the code remains as simple as if it was sequentially executed”. Yep, and Santa Claus exists.
Now, I might be terribly wrong with my skepticism. Maybe coroutines will become the next big shift in software development. Well, maybe. However, I’m not going to take this risk. Not with concurrency.
Therefore, I’ll wait until coroutines are stable, then wait another one-two years, then read some non-trivial code that uses them and gather other non-hyped feedbacks, and only then decide whether it’s worth trying them in professional projects.
Now that I explained why I find other popular approaches to multi-threading in Android non-optimal, I can finally introduce 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 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. Let’s discuss each one of them in details.
I don’t believe that multi-threading (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 multi-threading provides the following benefits:
- Multi-threading becomes easy to spot. If you use ThreadPoster consistently, you can even find all “multi-threading boundaries” by searching for usages of ThreadPoster’s constructs.
- Explicit multi-threading 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 multi-threading you can handle practically all requirements.
All in all, I wanted myself and my colleagues to be actively aware of the fact that we write multi-threaded code and be mindful about that.
The second pillar of ThreadPoster’s philosophy is simple setup.
As I already said, Android applications almost never have special constraints in context of multi-threading. If you write functionally correct multi-threaded 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, multi-threading in Android is very common.
You can build feature complete and functional web frontends and backends without ever doing manual multi-threading, but I had to use it in all my Android projects except for the simplest tutorials. I try to use libraries that take care of concurrency for me as much as I can, but there is always something that requires a special treatment.
Given the prominence of multi-threading in Android and the fact that there are no special constraints, I want multi-threading 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. The only requirement is that you make ThreadPoster’s constructs global and share the same instance among all the clients.
While there are several alternative approaches to make an object global, 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 multi-threaded code for quite some time.
However, unit testing is hard and multi-threading is hard. The intersection of these two topics results in what might very well be one of the most challenging practices in software development – multi-threaded unit testing. This practice is so complicated and rare, that there is even a widespread misconception that it’s simply impossible.
Let me make myself clear here, I’m talking about real multi-threaded unit testing. Substituting multi-threading with serial execution on a single thread is not rare, but I never found too much confidence in this practice. Therefore, I’ve been searching for ways to make unit tests run in real multi-threaded environment, similar to production code.
This goal was especially challenging in Android due to its special UI thread.
ThreadPoster achieves that goal, or, at the very least, comes close enough to make multi-threaded unit testing in Android feasible under certain 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 “easy” multi-threading
As I told you multiple times, I don’t believe that multi-threading can be easy. However, its complexity can be hidden in some circumstances.
For example, AsyncTask allows new developers to write concurrent code without understanding concurrency at all. All they need to do is to put logic A in method B and logic X in method Y. In many simple cases, this approach might work.
ThreadPoster could theoretically allow for a similar approach if I’d write the documentation in a certain way, but I won’t do that. Therefore, usage of ThreadPoster requires at least basic understanding of Android multi-threading concepts.
No guarantee of thread-safety from unit tests
It’s important to understand that no amount of unit testing can prove that the code doesn’t contain concurrency bugs. The best multi-threaded unit tests can do is to show you that your multi-threaded code is functionally correct in general. However, even if all unit tests written with ThreadPoster pass, you can still have race conditions, deadlocks, livelocks, etc.
That’s the fundamental feature of concurrency. Since it involves randomness, it can’t be proven correct empirically with 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 multi-threaded unit testing – it can’t prove that the code is free of concurrency bugs, but it has a chance to find such a bug.
You will probably be running your tests suite thousands of 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 – test that failed once for seemingly no reason. When you see flaky test it should make you launch a full blown investigation into the possible root causes which will, hopefully, lead you to find the concurrency bug.
This happened to me once and it was huge.
So, if that’s the fundamental concurrency characteristic and ThreadPoster can even be useful in this context, why it’s in the drawbacks section then? Well, I just want to warn you against false sense of safety.
Proper unit testing should result in you being very confident about the quality of tested classes. However, if you do multi-threaded unit testing, you should also remember that there can be concurrency bugs in the code even if all tests are green.
Long unit tests execution times
Due to the fact that ThreadPoster allows for truly multi-threaded unit testing, unit tests that use its test-doubles are slower than the regular unit tests.
On my machine, each test case that involves multi-threading takes on the order of 10ms. It doesn’t sound too bad, but if you have 1000 multi-threaded test cases (which might happen in sufficiently big application), 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 multi-threaded test cases, but it’s something to keep in mind nonetheless.
I’m also actively thinking about the ways to improve tests performance. While multi-threaded unit tests will always take longer to execute, I’m pretty sure that the current tests performance can be improved.
So, that’s all I wanted to say about multi-threading in Android and my new library called ThreadPoster. At more than 3000 words, this post ended up being much longer than library’s source code itself. It’s a bit ironic.
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 almost trivial when you read it is my definition of a good code. However, be assured that lots of thought went into each of the few lines in this library.
I intend to keep the library minimalist in the future too and will only add functionality that will be absolutely essential.
How can you know whether ThreadPoster is for you then?
Well, you’ve read so far, so I’d say that it’s a good sign of your interest. In addition, if you could relate when I said that all Android concurrency constructs are terrible, and then you didn’t get mad when I bashed RxJava, and you don’t use coroutines – you should definitely give ThreadPoster a try.
As always, don’t hesitate to leave you comments and questions below.