I'm an Android developer switching from Java to Kotlin, and I am planning to use the coroutines to handle asynchronous code since it looks very promising.
Back in Java, to handle asynchronous code I was using the Executor
class to execute a time-consuming piece of code in another thread, away from the UI thread. I had an AppExecutors
class that I injected in my xxxRepository
classes to manage a set of Executor
. It looked like this :
public class AppExecutors
{
private static class DiskIOThreadExecutor implements Executor
{
private final Executor mDiskIO;
public DiskIOThreadExecutor()
{
mDiskIO = Executors.newSingleThreadExecutor();
}
@Override
public void execute(@NonNull Runnable command)
{
mDiskIO.execute(command);
}
}
private static class MainThreadExecutor implements Executor
{
private Handler mainThreadHandler = new Handler(Looper.getMainLooper());
@Override
public void execute(@NonNull Runnable command)
{
mainThreadHandler.post(command);
}
}
private static volatile AppExecutors INSTANCE;
private final DiskIOThreadExecutor diskIo;
private final MainThreadExecutor mainThread;
private AppExecutors()
{
diskIo = new DiskIOThreadExecutor();
mainThread = new MainThreadExecutor();
}
public static AppExecutors getInstance()
{
if(INSTANCE == null)
{
synchronized(AppExecutors.class)
{
if(INSTANCE == null)
{
INSTANCE = new AppExecutors();
}
}
}
return INSTANCE;
}
public Executor diskIo()
{
return diskIo;
}
public Executor mainThread()
{
return mainThread;
}
}
Then I was able to write some code like this in my xxxRepository
:
executors.diskIo().execute(() ->
{
try
{
LicensedUserOutput license = gson.fromJson(Prefs.getString(Constants.SHAREDPREF_LICENSEINFOS, ""), LicensedUserOutput.class);
/**
* gson.fromJson("") returns null instead of throwing an exception as reported here :
* https://github.com/google/gson/issues/457
*/
if(license != null)
{
executors.mainThread().execute(() -> callback.onUserLicenseLoaded(license));
}
else
{
executors.mainThread().execute(() -> callback.onError());
}
}
catch(JsonSyntaxException e)
{
e.printStackTrace();
executors.mainThread().execute(() -> callback.onError());
}
});
It worked very good and Google even has something similar in their many Github Android repo examples.
So I was using callbacks. But now I am tired of the nested callbacks and I want to get rid of them. To do so, I could write in my xxxViewModel
for example :
executors.diskIo().execute(() ->
{
int result1 = repo.fetch();
String result2 = repo2.fetch(result1);
executors.mainThread().execute(() -> myLiveData.setValue(result2));
});
How is that USAGE different from Kotlin's coroutines' usage ? From what I saw, their biggest advantage is to be able to use asynchronous code in a sequential code style. But I am able to do just that with Executor
, as you can see from the code sample right above.
So what am I missing here ? What would I gain to switch from Executor
to Coroutines ?
The main difference between them is that while Async/await has a specified return value, Coroutines leans more towards updating existing data.
One important difference between threads and coroutines is that threads are typically preemptively scheduled while coroutines are not. Because threads can be rescheduled at any instant and can execute concurrently, programs using threads must be careful about locking.
The reason is coroutines makes it easier to write async code and operators just feels more natural to use. As a bonus, Flow operators are all kotlin Extension Functions, which means either you, or libraries, can easily add operators and they will not feel weird to use (in RxJava observable.
Okay, so coroutines are more often compared to threads rather than the tasks that you run on a given thread pool. An Executor is slightly different in that you have something that is managing threads and queueing tasks up to be executed on those threads.
I will also confess that I have only been using Kotlin's courotines and actors solidly for about 6 months, but let us continue.
Async IO
So, I think that one big difference is that running your task in a coroutine will allow you to achieve concurrency on a single thread for an IO task if that task is a truly async IO task that properly yields control while the IO task is still completing. You can achieve very light weight concurrent reads/writes with coroutines in this way. You could launch 10 000 coroutines all reading from disk at the same time on 1 thread and it would happen concurrently. You can read more on async IO here async io wiki
For an Executor service on the other hand, if you had 1 thread in your pool, your multiple IO tasks would execute and block in series on that thread. Even if you were using an async library.
Structured Concurrency
With coroutines and coroutine scope, you get something called structured concurrency. This means that you have to do far less book keeping about the various background tasks you are running so that you can properly do clean up of those tasks if you enter into some error path. With your executor, you would need to keep track of your futures and do the cleanup yourself. Here is a really good article written by one of the kotlin team leads to fully explain this subtlety. Structured Concurrency
Interaction with Actors
Another, probably more niche advantage is that with coroutines, producers and consumers, you can interact with Actors. Actors encapsulate state, and achieve thread safe concurrency through communication rather than through the traditional synchronized tools. Using all of these you can achieve a very light weight and highly concurrent state with very little thread overhead. Executors just do not offer the ability to interact with synchronized state in something like an Actor with say for example 10 000 threads or even 1000 threads. You could happily launch 100 000 coroutines, and if the tasks are suspending and yield control at suitable points, you can achieve some excellent things. You can read more here Shared Mutable state
Light Weight
And finally, just to demonstrate how light weight coroutine concurrency is, I would challenge you to do something like this on an executor and see what the total elapsed time is (this completed in 1160 milliseconds on my machine):
fun main() = runBlocking {
val start = System.currentTimeMillis()
val jobs = List(10_000){
launch {
delay(1000) // delays for 1000 millis
print(".")
}
}
jobs.forEach { it.join() }
val end = System.currentTimeMillis()
println()
println(end-start)
}
There are probably other things, but as I said, I am still learning.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With