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

24

我是一名从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做到这一点。那么我错过了什么?如果我从Executor转换到协程,我会得到什么好处呢?

2个回答

20

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

我也要坦白承认,我只是连续使用 Kotlin 的 courotines 和 actors 大约6个月,但我们继续。

Async IO

因此,我认为一个重要的区别是,在协程中运行任务可以使您在单个线程上实现并发性,如果该任务是真正异步 IO 任务,并且在 IO 任务仍在完成时适当地让出控制。您可以通过这种方式使用协程轻松实现并发读/写。您可以启动10,000个协程,同时从1个线程上的磁盘读取,并且它将同时发生。您可以在此处阅读有关 async IO 的更多信息async io wiki

另一方面,在 Executor 服务中,即使您使用异步库,如果您的池中有1个线程,则您的多个 IO 任务将按顺序执行和阻塞该线程。

结构化并发

使用协程和协程范围,您可以获得称为结构化并发的东西。这意味着您需要做更少的后台任务记录,以便在进入某些错误路径时可以正确清理这些任务。对于执行程序,您需要跟踪您的 future 并自己进行清理。在此处, Kotlin 团队领导之一撰写了一篇非常好的文章,以充分解释这个细微差别。Structured Concurrency

与 Actors 的交互

另一个可能更具特色的优点是,使用协程、生产者和消费者,您可以与 Actors 进行交互。Actor 封装状态,并通过通信而不是传统的同步工具实现线程安全的并发性。使用所有这些,您可以使用非常少的线程开销实现非常轻量级和高并发状态。 Executor 无法提供与像具有10,000个甚至1,000个线程的 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)
}

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


3
我认为你忽略了“革命性”的观点。对于大多数情况来说,4-5个线程可能已经足够,即使有1万个协程在这4-5个线程背后运行。但是,如果你想要同时处理1000个任务,如果你使用一个包含4-5个线程的线程池,并且使用执行器进行操作,那么你每次只能同时处理4-5个任务。如果每个任务需要1秒钟,则完成这1000个任务堆栈需要比小于2秒的时间长得多,可能需要10几秒钟。如果你不打算利用优势,最好还是继续使用执行器。 - Laurence
3
很抱歉,这并不正确。Android开发并不意味着少于10个并发任务。如果我编写一个Android游戏引擎,将游戏中的每个实体都视为一个Actor,并使用自己的协程进行更新并通过通道进行通信,那么怎么办?您正在将这个问题变成非常主观和单一用例的问题,这在我看来并不真正合适。 - Laurence
1
@CharlyLafon所以即使在您的特定用例中-您的磁盘I/O执行程序有多少线程?您提到了5个,因此让我们使用它。如果您想并发地将10个文件的内容读入内存,使用10个协程来完成而不是提交10个任务到该执行程序,它将用一半的时间完成。除此之外,您认为什么是对您问题的完美答案,或者您已经有一个答案了吗? - Laurence
2
@CharlyLafon,当询问使用线程池和使用协程的执行器之间的区别时,“已售出”这样的说法是不存在的。存在明显的差异,提供了极大的优势。答案不是“没有区别”。 - Laurence
2
@CharlyLafon 提供一些进一步的 SO 问题指导,你以非主观的方式提出了问题,但是你对答案应用了主观标准。这种类型的对话不适合 SO,因为它关于明确定义的问题和明确定义的答案。 - Laurence
显示剩余3条评论

-5

好的,我在使用协程时自己找到了答案。提醒一下,我当时在寻找用法上的区别。我能够使用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)
    }
}

我们可以看到,是suspend函数选择了哪个Dispatcher来管理任务,而不是ViewModel。我认为这样更好,因为它将这个逻辑封装到了Repository中。
此外,协程的取消比ExecutorService的取消要容易得多。ExecutorService并不真正适用于取消操作。它有一个shutdown()方法,但它会取消ExecutorService的所有任务,而不仅仅是我们需要取消的那个任务。如果我们的ExecutorService的范围比我们的ViewModel还大,那么我们就完了。 使用协程,这非常容易,你甚至不需要关心它。如果你使用viewModelScope(你应该这样做),它会在viewmodel的onCleared()方法中自动取消此作用域内的所有协程。
总之,协程与Android组件的集成比ExecutorService更好、更清晰,管理功能更好,而且它们确实很轻量级。即使我不认为这是Android上的杀手锏,拥有更轻量级的组件也是好事。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接