在Kotlin中以同步方式运行异步任务

时间:2019-10-20 03:26:40

标签: firebase kotlin google-cloud-firestore kotlin-coroutines

我正在尝试运行Firebase Firestore的“批处理”作业。由于批处理作业是异步的,并且每个批处理仅处理500个文档,因此我创建了一系列要以同步方式运行的批处理作业,以便我确切地知道上一个批处理何时完成,然后继续进行下一个操作。

但是,当我在Kotlin进行阅读时,遇到了诸如runBlocking,Coroutine,Dispatcher,async,await,Context,Suspend,launch,join,Scope,Deferred,Continuation,CommonPool之类的术语

此外,许多帖子都说,在最新版的Kotlin中,情况已经改变。 Kotlin documentation谈到runBlocking,但是this post说runBlocking是一件坏事。

经过反复试验,我得到了这个要编译的

suspend fun doTheThing() {

   for ( b in batchArray) {
      coroutineScope {
      val job = async { b.commit() }
      job.await()
  }}
}

但是,现在我收到错误消息,说“仅应从协程或其他暂停函数调用暂停函数'doTheThing'” 我现在很困惑。我只想按顺序执行这些调用,或者等到所有这些完成为止。不知道完成此操作的正确语法是什么,我弄错了什么概念。


更新:以下代码段似乎有效:

for ( b in batchArray) {
    runBlocking {b.commit()}
}

这样做是一种好习惯吗?

3 个答案:

答案 0 :(得分:0)

  

这样做是一种好习惯吗?

不,runBlocking绝对是错误的选择。它将阻塞您的应用程序的主线程,并可能通过ANR使其崩溃。但是,编写代码的特殊方式意味着您可以删除runBlocking并获得完全相同的行为。 b.commit()是一个普通的异步调用,它立即返回一个Task对象,这意味着您尚未达到在提交下一个批处理之前等待批处理完成的预期目标。

现在,继续使用利用协程的正确解决方案。

org.jetbrains.kotlinx:kotlinx-coroutines-play-services依赖项放在类路径上。这将为您提供suspend fun Task.await()扩展功能,并允许您构造一个挂起调用b.commit().await(),直到提交该批处理后,该调用才能完成。

有了这个,您可以这样编写函数:

fun CoroutineScope.doTheThing(batchArray: List<Batch>) {
    launch {
        for (b in batchArray) {
            b.commit().await()
        }
        // add logic here for what to do when all batches are done
    }
}

要调用它,您需要一个CoroutineScope。如果您还不了解结构化并发及其使用方法,请快速阅读CoroutineScope的文档。

请注意,submitAll的调用方直到所有批次完成后才阻塞,而是在后台启动协程并继续。但是,启动的协程将在批处理过程中暂停,在完成后继续执行,开始下一个作业,暂停等等,直到完成所有操作。挂起时,它不会占用线程。

答案 1 :(得分:0)

协程通常由不同的构建者在某个协程范围的上下文中创建。与构建器一样,suspend函数在协程范围内运行,因此应在协程范围内调用,可以通过在协程内部调用,suspend函数或从定义的范围内显式提供。

CoroutineScope是仅包含单个属性coroutineContext的接口。您可以通过实现CoroutineScope接口来简单地创建自己的范围,并覆盖自己的协程上下文。

val myCoroutineScope = object : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = Job() + Dispatchers.Main
}

在您的范围内,您可以使用构建器,例如启动,异步,生产等。

您可以将函数重构为

suspend fun doTheThing() = coroutineScope{
for ( b in batchArray) {
    b.commit()
   }
}

fun main(args: Array<String>) {
    myCoroutineScope.launch {
        doTheThing()
        println("Completed")   
      }
}

我在这里使用启动,是因为我们实际上并不关心结果。暂停功能将暂停父协程仪,直到完成执行。

您还可以选择在其他调度程序中运行范围

fun main(args: Array<String>) {

myCoroutineScope.launch(Dispatchers.IO) {
    doTheThing()
    println("Completed")
   }
}

为了获得更好的结果,我们不想取消任何子协程以取消范围,我们使用SuperVisor作业代替常规作业。

val myCoroutineScope = object : CoroutineScope {
override val coroutineContext: CoroutineContext
    get() = SupervisorJob() + Dispatchers.Default
}

答案 2 :(得分:0)

请参阅下面的解决方案,从该处开始执行批处理作业操作的主要功能,您需要定义将在所有线程上处理的500个文档。 因此,您可以使用IO分配器初始化Coroutine范围,并在其中调用主要处理方法。

科特林有三名调度员

  • IO 用于网络和磁盘IO的相关工作。
  • 默认用于复杂的操作,例如遍历列表或 数学运算。
  • 主要,用于将结果保存到UI或与UI相关的操作中。

现在,由于您希望所有500个文档并行处理,因此可以在该后台线程内创建一个同步块。 除非所有异步块(.commit)操作没有完成,否则该同步块将不会完成。

我想您可以通过这种方式实现所需的行为。请参见下面的相同代码:

fun initialFunction() {

   //inside this function start the Coroutine using launch
   //using Dispatcher.IO will perform execution of coroutine in background/IO
   CoroutineScope(Dispatchers.IO).launch {

      //call your method which will process batch job asynchronously
      doTheThing()

   }
}

suspend fun doTheThing() {
  //now start your blocking call, this execute following block 
  //synchronously 
  runBlocking {
     for ( b in batchArray) {

     //commit will run synchronously and following nested coroutine
     //will wait for job to get completed 
     launch {
        val job = async { b.commit() }
        job.await()
     }

    }
  }
}