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

时间:2019-04-11 16:15:15

标签: java android kotlin coroutine 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;
    }
}

然后,我可以在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());
            }
        });

效果很好,Google在许多Github Android回购示例中甚至都有类似的东西。

所以我正在使用回调。但是现在我对嵌套的回调很烦,我想摆脱它们。为此,我可以在xxxViewModel中输入例如:

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

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

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

2 个答案:

答案 0 :(得分:2)

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

我也将承认,我仅仅一直在牢固使用科特林的天狼星和演员大约6个月,但让我们继续。

异步IO

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

上了解有关异步IO的更多信息

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

结构化并发

有了协程和协程作用域,您将获得一种称为结构化并发的东西。这意味着您不必为正在运行的各种后台任务做很多记账工作,这样,如果您输入一些错误路径,就可以正确清理这些任务。与执行人一起,您将需要跟踪自己的期货并自己进行清理。这是Kotlin团队的其中一位领导撰写的一篇非常好的文章,可以充分解释这种微妙之处。 Structured Concurrency

与演员的互动

另一个可能更利基的优势是,有了协程,生产者和消费者,您就可以与Actor互动。 Actor封装状态,并通过通信而不是通过传统的同步工具实现线程安全的并发。使用所有这些,您可以实现非常轻量级和高度并发的状态,而线程开销却很小。执行器只是不提供与Actor之类的同步状态进行交互的功能,例如具有10 000个线程甚至1000个线程的Actor。您可以愉快地启动10万个协程,如果任务在适当的位置暂停并进行产量控制,那么您可以实现一些出色的事情。您可以在这里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)
}

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

答案 1 :(得分:-3)

好的,我在应用中使用协同程序时自己找到了答案。提醒一下,我一直在寻找用法的不同之处。我能够使用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()方法中此范围内的所有协程。

最后,协程与ExecutorService相比,与Android组件的集成更多,更好,更干净的管理功能,是的,它们是轻量级的。即使我不认为这是Android上的杀手argument,拥有更多轻量级的组件仍然很好。