如何让协程仅在第一个完成时启动下一个作业

How to make coroutines launch next job only when first is finished

假设我有一个按钮,它在 Android 中有一个侦听器:

someView.setOnClickListener {
    viewModel.doSomething()
}

这将在每次按下按钮时创建一个新作业。在我的 ViewModel 中,doSomething 函数正在从 viewModelScope 启动挂起函数:

fun doSomething() {
  viewModelScope.launch {
       doSomething1()
       doSomething2()
       doSomething3() 
   }
}

suspend fun doSomething1() {
    delay(100)
    Log.d("TEST", "doSomething1: 1 ")
}

suspend fun doSomething2() {
    delay(300)
    Log.d("TEST", "doSomething2: 2 ")
}

fun doSomething3() {
    Log.d("TEST", "doSomething3: 3 ")
}

现在,如果这个按钮被连续快速按下(理论上,可以说我可以从侦听器调用函数两次,这样第一次调用的执行还没有完成),我会得到以下结果在我的 logcat:

D/TEST: doSomething1: 1
D/TEST: doSomething1: 1
D/TEST: doSomething2: 2
D/TEST: doSomething3: 3
D/TEST: doSomething2: 2
D/TEST: doSomething3: 3

我真正想要实现的是,如果我可以从同一范围启动 doSomething() 两次,那么它会同步运行。

D/TEST: doSomething1: 1
D/TEST: doSomething1: 2
D/TEST: doSomething2: 3
D/TEST: doSomething3: 1
D/TEST: doSomething2: 2
D/TEST: doSomething3: 3

我怎样才能实现这种行为,以便在启动同一个协程之前,第一个协程必须完成?

可以通过保存最后一个作业并等待它完成再执行新协程来解决:

private var lastJob: Job? = null

fun doSomething() {
    val prevJob = lastJob
    lastJob = lifecycleScope.launch {
        prevJob?.join()
        doSomething1()
        doSomething2()
        doSomething3()
    }
}

您可以使用互斥锁来执行此操作 - 无论您按下按钮多少次,该锁都会阻止后续的 1-2-3 序列启动,直到前面的序列完成为止。

private val lock = Mutex()

fun doSomething() {
  viewModelScope.launch {
       // lock means only one "1-2-3" sequence can execute 
       // at a time, subsequent calls will suspend here and wait
       // for the lock to be released before starting
       lock.withLock {
           doSomething1()
           doSomething2()
           doSomething3() 
       }
   }
}

您也可以使用频道一次 运行 他们

private val channel = Channel<Job>(capacity = Channel.UNLIMITED).apply {
    viewModelScope.launch {
        consumeEach { it.join() }
    }
}

fun doSomething() {
    channel.trySend(
        // send a lazily executed "1-2-3" job to the channel for it
        // to run (will run jobs one at a time and wait for each
        // job to complete before starting the next)
        viewModelScope.launch(start = CoroutineStart.LAZY) {
            doSomething1()
            doSomething2()
            doSomething3() 
        } 
    )
}