Kotlin 的协程与 Android 中的 Java Executor 有何不同?



我是一个从Java切换到Kotlin的Android开发人员,我计划使用协程来处理异步代码,因为它看起来很有前途。

回到 Java,为了处理异步代码,我使用Executor类在另一个线程中执行一段耗时的代码,远离 UI 线程。我有一个AppExecutors类,我在我的xxxRepository类中注入了它来管理一组Executor。它看起来像这样:

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;
}
}

然后我能够在我的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());
}
});

它工作得很好,谷歌甚至在他们的许多Github Android存储库示例中都有类似的东西。

所以我使用回调。但是现在我厌倦了嵌套回调,我想摆脱它们。为此,我可以在我的xxxViewModel中写下:

executors.diskIo().execute(() -> 
{
int result1 = repo.fetch();
String result2 = repo2.fetch(result1);
executors.mainThread().execute(() -> myLiveData.setValue(result2));
});

用法与 Kotlin 的协程用法有何不同?从我所看到的,它们最大的优势是能够以顺序代码样式使用异步代码。但是我能够用Executor做到这一点,正如您从上面的代码示例中看到的那样。 那么我在这里错过了什么?从Executor切换到协程会得到什么?

好的,因此协程通常与线程进行比较,而不是在给定线程池上运行的任务。执行器略有不同,因为您有一些东西可以管理线程并将任务排队以在这些线程上执行。

我还要承认,我只用了 Kotlin 的 courotines 和演员大约 6 个月,但让我们继续吧。

异步 IO

因此,我认为一个很大的区别是,如果该任务是真正的异步 IO 任务,并且在 IO 任务仍在完成时正确产生控制,则在协程中运行任务将允许您在单个线程上实现 IO 任务的并发性。通过这种方式,您可以使用协程实现非常轻量级的并发读/写。您可以在 1 个线程上同时启动 10000 个协程,所有协程都从磁盘读取,并且会同时发生。你可以在这里阅读更多关于异步IO的信息 async io wiki

另一方面,对于执行程序服务,如果池中有 1 个线程,则多个 IO 任务将在该线程上串联执行和阻止。即使您使用的是异步库。

结构化并发

使用协程和协程作用域,您可以获得称为结构化并发的东西。这意味着您必须对正在运行的各种后台任务执行更少的簿记,以便在输入某些错误路径时可以正确清理这些任务。有了您的遗嘱执行人,您需要跟踪您的未来并自己进行清理。这是一篇由 kotlin 团队之一撰写的非常好的文章,可以充分解释这种微妙之处。结构化并发

与演员互动

另一个可能更利基的优势是,通过协程、生产者和使用者,您可以与 Actor 进行交互。Actor封装状态,并通过通信而不是传统的同步工具实现线程安全并发。使用所有这些,您可以实现非常轻量级和高度并发的状态,而线程开销非常小。执行器只是不提供与同步状态交互的能力,例如 Actor 例如 10 000 个线程甚至 1000 个线程。 你可以愉快地启动 100 000 个协程,如果任务在适当的点暂停并产生控制,你可以实现一些出色的事情。您可以在此处阅读更多信息 共享可变状态

重量 轻

最后,为了演示轻量级协程并发性,我将挑战您在执行器上执行类似操作,并查看总运行时间是多少(这在我的机器上在 1160 毫秒内完成):

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)
}

可能还有其他事情,但正如我所说,我还在学习。

好的,我在应用程序中使用协程时自己找到了答案。提醒一下,我正在寻找用法的差异。我能够使用Executor按顺序执行异步代码,并且我到处都看到这是协程的最大优势,那么切换到协程的最大好处是什么?

首先,您可以从我的上一个示例中看到,选择异步任务运行的线程的是xxxViewModel。在我看来,这是一个设计缺陷。ViewModel 不应该知道这一点,更不应该负责选择线程。

现在有了协程,我可以写这样的东西:

// ViewModel
viewModelScope.launch {
repository.insert(Title(title = "Hola", id = 1))
myLiveData.value = "coroutines are great"
}
// Repository
suspend fun insert(title: Title)
{
withContext(Dispatchers.IO)
{
dao.insertTitle(title)
}
}

我们可以看到,是暂停函数选择调度程序正在管理任务,而不是视图模型。我发现这更好,因为它将此逻辑封装到存储库中。

此外,协程取消比ExecutorService取消容易得多。ExecutorService并不是真的为了取消而生的。它有一个shutdown()的方法,但它会 取消ExecutorService的所有任务 ,而不仅仅是我们需要取消的任务。如果我们ExecutorService的范围大于视图模型的范围,我们就完蛋了。 使用协程,它非常简单,您甚至不必关心它。如果使用viewModelScope(您应该使用),它将自行取消视图模型的onCleared()方法中此范围内的所有协程。

总之,协程与 Android 组件的集成比ExecutorService多得多,管理功能更好、更干净,是的,它们是轻量级的。即使我不认为这是Android上的杀手锏,拥有更多轻量级组件仍然是件好事。