【问题标题】:How are Kotlin's Coroutines different from Java's Executor in Android?Kotlin 的 Coroutines 与 Android 中 Java 的 Executor 有何不同?
【发布时间】:2019-09-02 08:33:09
【问题描述】:

我是一名从 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 repo 示例中都有类似的东西。

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

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

            executors.mainThread().execute(() -> myLiveData.setValue(result2));
        });

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

【问题讨论】:

    标签: java android kotlin coroutine kotlin-coroutines


    【解决方案1】:

    好的,所以协程通常被比作线程,而不是在给定线程池上运行的任务。 Executor 稍有不同,因为您有一些东西可以管理线程并将任务排队等待在这些线程上执行。

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

    异步 IO

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

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

    结构化并发

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

    与演员的互动

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

    重量轻

    最后,为了演示轻量级的协程并发是多么的轻量级,我会挑战你在执行程序上做这样的事情,看看总经过的时间是多少(在我的机器上完成了 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)
    }
    

    可能还有其他的东西,但正如我所说,我还在学习。

    【讨论】:

    • 我认为你错过了“革命”点。 4-5 个线程可能是你几乎所有事情所需要的,并且 10000 个协程仍然会在后台运行在这 4-5 个线程上。但是,如果你想同时做 1000 件事情,如果你有一个 4-5 个线程池,并且你用一个执行器来做这件事,那么你一次只能做 4-5 个。如果他们每个人都需要 1 秒,那么您将在 10 秒后而不是不到 2 秒内完成 1000 个任务的堆栈。如果你不打算利用这些优势,那就坚持你的执行人。
    • @CharlyLafon 恐怕这不正确。 Android 开发并不意味着少于 10 个并发任务。如果我编写一个 android 游戏引擎,将游戏中的每一个实体都视为一个使用自己的协程更新并通过通道进行通信的 Actor,那会怎样?您正在使这个问题成为一个非常主观和单一用例的问题,这并不适合 IMO
    • @CharlyLafon 所以即使在您的特定用例中 - 您的磁盘 IO 执行程序中有多少线程?你提到了 5,所以让我们使用它。如果您想同时将 10 个文件的内容读入内存,使用 10 个协程而不是向该执行程序提交 10 个任务,它会在一半的时间内完成。除此之外,您认为什么是您问题的完美答案,或者您已经有了答案?
    • @CharlyLafon 在询问使用线程池和使用协程的执行器之间的区别时,没有“出售”之类的东西。有明显的差异提供了极好的优势。答案不是“没有区别”
    • @CharlyLafon 为进一步的 SO 问题提供一些指导,您以非主观方式提出了问题,但您对答案应用了主观标准。这种类型的对话不适合 SO 是关于定义明确的问题和明确的答案。
    【解决方案2】:

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

    首先,您可以从我的上一个示例中看到,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)
        }
    }
    

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-08-25
      • 1970-01-01
      • 2019-11-25
      • 2019-08-06
      • 1970-01-01
      • 1970-01-01
      • 2018-07-11
      • 2020-07-27
      相关资源
      最近更新 更多