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

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

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

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 的协程用法有何不同?据我所知,他们最大的优势是能够以顺序代码样式使用异步代码。但是我能够使用 来做到这一点,正如您从上面的代码示例中看到的那样。那么我在这里错过了什么?从切换到协程可以获得什么?ExecutorExecutor


答案 1

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

我还要承认,我只在大约6个月的时间里一直使用Kotlin的courotines和演员,但让我们继续吧。

异步 IO

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

另一方面,对于 Executor 服务,如果池中有 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)
}

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


答案 2

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

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

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

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

我们可以看到,选择调度程序管理任务的是挂起函数,而不是 ViewModel。我发现这要好得多,因为它将此逻辑封装到存储库中。

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

总而言之,Coroutines与Android组件的集成度要高得多,而不是更好,更干净的管理功能,是的,它们是轻量级的。即使我不认为这在Android上是一个杀手级的论点,拥有更多轻量级组件仍然是件好事。ExecutorService


推荐