Day13:内建的 suspend 函式,好函式不用吗? (2)

withContext

suspend fun<T> withContext(context: CoroutineContext,
						   block: suspend CoroutineScope.()->T):T

withContext 是用来在现有的 coroutine 中,使用新的 CoroutineContext 来建立一个执行区块。我们最常看到的是拿来做更新画面使用,也就是说当 coroutine 执行的任务完成之後,我们可以使用 withContext(Dispatchers.Main) 将执行绪切回主执行绪并更新。

这边我们看一个范例:

fun main() = runBlocking {
    val job = launch {
        println("inside: ${Thread.currentThread().name}")
        withContext(Dispatchers.Default) {
            println("Default dispatcher: ${Thread.currentThread().name}")
        }
    }
    println("outer: ${Thread.currentThread().name}")
}
outer: main
inside: main
Default dispatcher: DefaultDispatcher-worker-1

→ 根据我们前面所说的巢状架构,这段程序码会先列印最外侧的 outer: main ,接着才会是执行 launch 所建立出来的 coroutine,并依序执行 coroutine 里面的程序。

→ 在 launch 中,我们使用 withContext(Dispatchers.Default) ,也就是说在这个区块中我们将使用 Dispatchers.Default,而从 log 看来的确也已经切换到 DefaultDispatcher。

withContext 是可取消的

withContext 所建立出来的区间是可以被取消的,当调用 withContext 的 coroutine 被取消之後,withContext 也就会被取消。如下例:

fun main() = runBlocking {
    val job = launch {
        println("inside: ${Thread.currentThread().name}")
        withContext(Dispatchers.Default) {
            delay(200)
            println("Default: ${Thread.currentThread().name}")
        }
    }
    delay(100)
    job.cancel()
    job.join()
    println("outer: ${Thread.currentThread().name}")
}
inside: main
outer: main

→ 外侧的 coroutine 因为呼叫了 delay() 之後,所以把外侧的 coroutine 切换至等待状态,并寻找下一个可执行的 coroutine。那麽这边我们看到列印出 inside: main ,紧接者进入 withContext(Dispatchers.Default) ,在 withContext 中立刻就呼叫一个 delay() ,所以此 coroutine 又被切换成等待状态,并且找寻下一个可执行的 coroutine。此时,外侧的 coroutine 已经结束它的延时,所以呼叫到了 job.cancel() ,这个时候因为 launch 里面尚有任务还在等待,所以就直接被取消。最後则是列印出 outer: main

让 withContext 不可取消

在前面的范例中,我们知道 withContext 的区块会随着启动它的 coroutine 被取消也跟着被取消,what if 我们不希望让 withContext 被取消呢?

我们可以在 withContext 的 CoroutineContext 中加上 NonCancellable ,加上 NonCancellable 之後,被 withContext 包住的区块就不会因为外侧的 coroutine 取消而跟着被取消。

如下面的范例:

fun main() = runBlocking {
    val job = launch {
        println("inside: ${Thread.currentThread().name}")
        withContext(Dispatchers.Default + NonCancellable) {
            delay(200)
            println("Default: ${Thread.currentThread().name}")
        }
    }
    delay(100)
    job.cancel()
    job.join()
    println("outer: ${Thread.currentThread().name}")

}

→ 这个范例跟前面一个几乎相同,唯一不同的地方在於我加上 NonCancellable 在 withContext 的 CoroutineContext 中,结果会是如何呢?我们看一下:

inside: main
Default: DefaultDispatcher-worker-1
outer: main

虽然 job 的 cancel() 被呼叫,但是 withContext 里面的区块仍然会执行。

在最前面有说,我们经常使用 withContext 在更新画面上,也就是说,我们可以让更新画面这段程序码无论如何都会执行,而不会因为外侧的 coroutine 被取消而跟着被取消。

withTimeout

withTimeout 顾名思义就是跟 timeout 有关系,我们先看他是怎麽使用的:

suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T

→ 它有包含两个参数,一个是时间,另一个是 CoroutineScope 也就是执行的区块。

要如何使用呢?我们看一下下面的范例:

fun main() = runBlocking {
    val job = launch() {
        println("inside: ${Thread.currentThread().name}")
	       withTimeout(200) {
	            repeat(10) {
	                println("delay $it times")
	                delay(50)
	                yield()
	            }
	            println("withTimeout")
	        }
    }
    delay(100)
    println("outer: ${Thread.currentThread().name}")
}

→ 在上面这个范例中,同样的我们在 runBlocking 里面使用了 launch 建立了一个 coroutine,而在 launch 的下方则是有几行程序码。

→ 首先,会先执行最外侧的 delay(100) ,这时最外侧的 Coroutine 就被设定为暂停,此时会寻找下一个适当的 coroutine 来执行。在这个范例中,适当的 coroutine 是 launch 的区块。launch 内部有一重复 10 次,在每一次执行的时候,都会将 coroutine 暂停 50 毫秒,在暂停完成之後,会把执行绪使用权切换至外侧,但是外侧的 coroutine 还在等待,所以使用权又切回来执行下一次。如果要完成重复十次的任务,需要花费 10*50 = 500 毫秒。

→ 假设我们希望执行的时间能够在 200 毫秒,在这个 repeat(10){ ... } 的外侧,我们加上了 withTimeout(200) ,当 200 毫秒结束之後,我们就会取消这个区块里面的所有任务。所以这段程序码的输出会是:

inside: main
delay 0 times
delay 1 times
outer: main
delay 2 times
delay 3 times

→ 可以发现, repeat 区块的程序只有跑了四次,符合我们的需求。

TimeoutCancellationException

其实当 withTimeout 的时间到了之後,是会抛出 TimeoutCancellationException ,只不过由於 TimeoutCancellationException 是 CancellationException 的子类别,所以这个例外会被 coroutine 给吃掉。

但是,如果我们真的需要处理这个例外,我们可以使用 try-catch 来拦截 TimeoutCancellationException。如下:

fun main() = runBlocking {
    val job = launch() {
        println("inside: ${Thread.currentThread().name}")
        try {
           withTimeout(200) {
                repeat(10) {
                    println("delay $it times")
                    delay(50)
                    yield()
                }
                println("withTimeout")
            }
        } catch (e: TimeoutCancellationException) {
            println(e.message)
        }

    }
    delay(100)
    println("outer: ${Thread.currentThread().name}")
}
inside: main
delay 0 times
delay 1 times
outer: main
delay 2 times
delay 3 times
Timed out waiting for 200 ms

将上例的 withTimeout 用 try-catch 包起来之後,我们就可以接收到这个例外了。

可取消

如同 withContext,withTimeout 也同样是可以取消的。如下例:

fun main() = runBlocking {
    val job = launch() {
        println("inside: ${Thread.currentThread().name}")
        try {
           withTimeout(200) {
                repeat(10) {
                    println("delay $it times")
                    delay(50)
                    yield()
                }
                println("withTimeout")
            }
        } catch (e: TimeoutCancellationException) {
            println(e.message)
        }

    }
    delay(100)
    job.cancel()
    job.join()
    println("outer: ${Thread.currentThread().name}")
}

→ 我们在外侧 coroutine delay(100) 之後加上了 job.cancel() ,所以当外侧 coroutine 的暂停时间结束後,就会把内侧的 coroutine 给取消掉。哪麽这个结果会是如何呢?

inside: main
delay 0 times
delay 1 times
outer: main

→ 你应该有猜到,因为 withTimeout 是可以取消的,所以当外侧取消了内侧的 coroutine ,那麽连带 withTimeout 也一并被取消了,所以 repeat 第三次、第四次就被取消没做了。

withTimeoutOrNull

withTimeout 非常的像,只不过一个是会抛出 TimeoutCancellationException,而另一个是回传 null,我们看下面的范例:

fun main() = runBlocking {
    val job = launch() {
        println("inside: ${Thread.currentThread().name}")
        val timeout = withTimeoutOrNull(600) {
            repeat(10) {
                println("delay $it times")
                delay(50)
                yield()
            }
            return@withTimeoutOrNull 10
        }
        println("timeout= $timeout")
    }
    delay(100)
    println("outer: ${Thread.currentThread().name}")
}

→ 跟上面的范例很相像,差异只在於我们在 withTimeoutOrNull(){...} 的最後加上了一个回传值,所以当程序码顺利在时限内完成,就会回传这个值,否则就会回传 null

→ 如果 timeout 是 600 毫秒,那这段程序码可以正常跑完,所以结果会是

inside: main
delay 0 times
delay 1 times
outer: main
delay 2 times
delay 3 times
delay 4 times
delay 5 times
delay 6 times
delay 7 times
delay 8 times
delay 9 times
timeout= 10

将 timeout 改成 200

将 timeout 改成 200 之後,这段程序就没有办法执行 10 次,最多就只能跑 4 次,我们看看结果会是如何?

fun main() = runBlocking {
    val job = launch() {
        println("inside: ${Thread.currentThread().name}")
        val timeout = withTimeoutOrNull(200) {
            repeat(10) {
                println("delay $it times")
                delay(50)
                yield()
            }
            return@withTimeoutOrNull 10
        }
        println("timeout= $timeout")
    }
    delay(100)
    println("outer: ${Thread.currentThread().name}")
}
inside: main
delay 0 times
delay 1 times
outer: main
delay 2 times
delay 3 times
timeout= null

→ 原本输出应该是 10,但是因为 timeout 的关系,所以只能完成四次,所以最後 withTimeoutOrNull 会输出 null

小记

本篇文章介绍的三个 suspend 函式, withContext、withTimeout、withTimeoutOrNull。

withContext 适合使用在执行完非同步的呼叫後,需要切换成主执行绪并更新画面的情境,而另外两个与 timeout 有关的函式,主要就是要看使用者的需求,如果超时之後就不管他,可以使用 withTimeout,如果执行的区块是有一个回传值,withTimeoutOrNull 或许就比较适合了。

参考资料

withContext

withTimeout.html

withTimeoutOrNull

特别感谢

Kotlin Taiwan User Group

Kotlin 读书会


<<:  004-元件名称

>>:  Leetcode: 26. Remove Duplicates from Sorted Array

Day12 Vue Event Handing(v-on)

Event Handling是甚麽呢? Event Handing是可以用v-on指令监听DOM事件...

Day27. 范例:Line群组通知(观察者模式)

本文同步更新於blog 情境:让我们用Line群组,来实作观察者模式 首先实作抽象的观察者类别 (...

[Day8]-元组(tuple)

基本元组 元组的结构跟串列是一样的,但元组可以更安全的保护资料,因为它的资料不会被改变,而且元组的...

Day6 Vue实体的生命周期

这是在Vue官网提供的式意图: 红框白底的是各个钩子函式的名称,这些钩子代表 Vue 实体的每个阶段...

[FGL] 吸星大法 - IMPORT之 2: 带入JAVA或其他FGL套件

前一篇IMPORT中,提到Genero Package中有提供一些预先制作的功能套件可用。 可是面对...