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

Cha*_*fon 19 java android coroutine kotlin kotlin-coroutines

我是一名从 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我就可以在我的代码中写一些这样的代码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());
            }
        });
Run Code Online (Sandbox Code Playgroud)

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

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

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

            executors.mainThread().execute(() -> myLiveData.setValue(result2));
        });
Run Code Online (Sandbox Code Playgroud)

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

Lau*_*nce 17

好的,因此协程更常与线程相比,而不是您在给定线程池上运行的任务。Executor 略有不同,因为你有一些东西可以管理线程和排队要在这些线程上执行的任务。

我还要承认,我只扎实使用 Kotlin 的 courotines 和 actor 大约 6 个月,但让我们继续。

异步IO

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

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

结构化并发

使用协程和协程范围,您会得到一种称为结构化并发的东西。这意味着您必须对正在运行的各种后台任务做更少的记录,以便在进入某些错误路径时可以正确地清理这些任务。与您的遗嘱执行人一起,您需要跟踪您的期货并自己进行清理。这是 kotlin 团队的一位领导写的一篇非常好的文章,充分解释了这种微妙之处。结构化并发

与演员互动

另一个可能更小众的优势是,通过协程、生产者和消费者,您可以与 Actor 进行交互。Actors 封装状态,通过通信而不是通过传统的同步工具实现线程安全的并发。使用所有这些,您可以以很少的线程开销实现非常轻量级和高度并发的状态。Executors 只是不提供与类似 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)
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我认为你错过了“革命”点。4-5 个线程可能是大多数事情所需要的,并且 10 000 个协程仍将在这 4-5 个线程上在后台运行。但是,如果你想同时做 1000 件事情,如果你有 4-5 个线程池,并且你正在用一个 executor 来做这件事,那么你一次只能做 4-5 件事情。如果他们每个人都需要 1 秒,那么您将在 10 秒后而不是在不到 2 秒内完成那堆 1000 个任务。如果你不打算利用优势,那么坚持你的执行人。 (3认同)
  • @CharlyLafon 恐怕这不正确。Android 开发并不意味着少于 10 个并发任务。如果我编写一个 android 游戏引擎,将游戏中的每个实体视为一个 Actor,它会使用自己的协程进行更新并通过通道进行通信,该怎么办?您使这个问题成为一个非常主观且单一的用例问题,这并不是真正适合 IMO (3认同)
  • @CharlyLafon 在询问使用线程池和使用协程的 Executor 之间的区别时,没有“出售”这样的东西。有明显的差异提供了极好的优势。答案不是“没有区别” (2认同)
  • @CharlyLafon 为进一步的 SO 问题提供一些指导,您以非主观的方式提出问题,但您对答案应用了主观标准。这种类型的对话不适合 SO,SO 是关于明确定义的问题和明确定义的答案。 (2认同)

Cha*_*fon -5

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

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

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

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

我们可以看到,是挂起函数选择 Dispatcher 来管理任务,而不是 ViewModel。我发现这更好,因为它将这个逻辑封装到存储库中。

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

总之,协程与 Android 组件的集成程度更高ExecutorService,管理功能更好、更简洁,而且它们是轻量级的。即使我不认为这对 Android 来说是一个致命的论点,拥有更多轻量级组件仍然是件好事。