javascript 如何在Kotlin中创建一个可调度的函数,可以在延迟间隔内调度多次,但在间隔结束时运行一次?

nlejzf6q  于 2023-05-16  发布在  Java
关注(0)|答案(1)|浏览(107)

如何创建一个函数,可以从非挂起上下文**多次调用,并且在第一次调用后的特定延迟后,可挂起的函数运行?
我试图在Kotlin中重新创建的JavaScript代码:

const DELAY_MS = 500

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const schedulable = () => console.log('schedulable function is running')
let timeout = null

const schedule = () => {
  if (!timeout) {
    timeout = setTimeout(() => {
      timeout = null
      schedulable()
    }, DELAY_MS)
  }
}

;(async () => {
  schedule()
  await sleep(100)
  schedule()
  schedule()
  await sleep(200)
  schedule()
})()

// result: 'schedulable function is running' is printed once

Kotlin尝试(似乎有效,但使用GlobalScope):

object SchedulableAttempt {
  private fun schedulable() {
    println("schedulable function is running")
  }

  private const val DELAY_MS = 500L
  private var deferred: Deferred<Unit>? = null

  fun schedule() {
    synchronized(this){
      if(deferred === null){
        deferred = GlobalScope.async {
          delay(DELAY_MS)
          deferred = null
          schedulable()
        }
      }
    }
  }
}

fun main() {
  thread {
    SchedulableAttempt.schedule()
    Thread.sleep(100)
    SchedulableAttempt.schedule()
    SchedulableAttempt.schedule()
    Thread.sleep(200)
    SchedulableAttempt.schedule()
  }

  runBlocking { // just to keep the app running
    delay(1000)
  }
}

如何在Kotlin中正确地做到这一点?这样的解决方案有没有陷阱?Kotlin文档明确指出避免在非顶级协程的情况下使用GlobalScope,因为它可能会导致内存泄漏

mfuanj7w

mfuanj7w1#

这里有一种方法,即使在schedulable中存在异常时,也应该是健壮的,我将在下面描述另一种边缘情况。你不需要从schedule返回任何东西,我只是像这样做实验。

import kotlinx.coroutines.*
import kotlinx.coroutines.future.asCompletableFuture
import kotlin.concurrent.thread

object SchedulableAttempt {
    private const val DELAY_MS = 500L
    private val coroutineScope = CoroutineScope(Dispatchers.IO)
    private var coroutine: Job? = null

    private fun schedulable() {
        println("schedulable function is running")
    }

    @Synchronized
    fun schedule() = coroutine.let {
        when (it?.isCompleted) {
            null -> launch()
            false -> it
            true -> launch()
        }
    }

    private fun launch(): Job {
        return coroutineScope.launch {
            println("delaying")
            delay(DELAY_MS)
            reset()
            schedulable()
        }.also {
            coroutine = it
        }
    }

    @Synchronized
    private fun reset() {
        println("resetting")
        coroutine = null
    }
}

fun main() {
    thread {
        SchedulableAttempt.schedule()
        Thread.sleep(100L)
        SchedulableAttempt.schedule()
        SchedulableAttempt.schedule()
        Thread.sleep(200L)
        SchedulableAttempt.schedule()
    }

    // just to show what happens if there are cancellations/exceptions
    runBlocking {
        SchedulableAttempt.schedule().cancelAndJoin()
    }

    // just to keep the app running
    SchedulableAttempt.schedule().asCompletableFuture().get()
}

如果你运行它,你可能会看到这样的输出:

delaying
resetting
schedulable function is running

也就是说,delaying只出现一次。这意味着协程甚至在有机会启动 * 之前就被cancelAndJoin * 取消了。在这些情况下,协程不会调用reset,这就是为什么schedule也处理coroutine?.isCompleted == true的情况。我最初在launch ed逻辑中使用了try-finally,但这没有帮助,如果协程从未启动,那么finally甚至没有“排队”。
顺便说一句,如果你想在JVM中使用future扩展,并且你使用的kotlinx-coroutines-core版本比1.7.0早,你需要kotlinx-coroutines-jdk8

相关问题