Kotlin 的协程与 Java 中 Android 的执行器有何不同?

How are Kotlin's Coroutines different from Java's Executor in Android?

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

回到 Java,为了处理异步代码,我使用 Executor class 在另一个线程中执行一段耗时的代码,远离 [=47] =] 线程。我在我的 xxxRepository class 中注入了一个 AppExecutors class 来管理一组 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));
        });

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

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

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

异步 IO

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

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

结构化并发

有了协程和协程作用域,你就会得到一种叫做结构化并发的东西。这意味着您需要做的关于您正在执行的各种后台任务的记录要少得多,这样您就可以在进入某些错误路径时正确地清理这些任务。与您的遗嘱执行人一起,您需要跟踪您的未来并自己进行清理。这是一篇由 kotlin 团队领导撰写的非常好的文章,充分解释了这种微妙之处。 Structured Concurrency

与演员的互动

另一个可能更利基的优势是,通过协程、生产者和消费者,您可以与 Actors 进行交互。 Actors封装状态,通过通信而不是通过传统的同步工具实现线程安全的并发。使用所有这些,您可以以非常少的线程开销实现非常轻量级和高度并发的状态。 Executors 只是不提供与 Actor 之类的同步状态交互的能力,例如 10000 个线程甚至 1000 个线程。你可以愉快地启动 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)
}

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

好吧,我在我的应用程序中使用协程时自己找到了答案。提醒一下,我一直在寻找 usage 的差异。我能够使用 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)
    }
}

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

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

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