Kotlin 协程基础知识汇总(一)
1、协程基础
Kotlin 是一门仅在标准库中提供最基本底层 API 以便其他库能够利用协程的语言。与许多其他具有类似功能的语言不同,async 与 await 在 Kotlin 中并不是关键字,甚至都不是标准库的一部分。此外,Kotlin 的挂起函数概念为异步操作提供了比 future 与 promise 更安全、更不易出错的抽象。
kotlinx.coroutines 是由 JetBrains 开发的功能丰富的协程库。使用协程需要添加对 kotlinx-coroutines-core 模块的依赖,如项目的 readme 文件所述。
1.1 第一个协程
协程是一个可以挂起(suspend)的计算实例。在概念上它类似于线程,因为它需要运行一段与其他代码并发的代码块。但是,协程不绑定到任何特定的线程。它可以在一个线程中暂停执行,然后在另一个线程中恢复执行。
协程可以被视为轻量级线程,但有很多重要的差异使得它们在实际使用中与线程很不同。
运行以下代码获取你的第一个工作协程:
fun main() = runBlocking { // this: CoroutineScopelaunch { // launch a new coroutine and continuedelay(1000L) // non-blocking delay for 1 second (default time unit is ms)println("World!") // print after delay}println("Hello") // main coroutine continues while a previous one is delayed
}// 输出结果:
Hello
World!
launch 是一个协程构建器。它启动一个与其他代码并发的新协程,该协程继续独立工作。
delay 是一个特殊的挂起函数,它会协程挂起一定的时间。挂起协程不会阻塞底层线程,而是允许其他协程运行并使用底层线程执行它们的代码。
runBlocking 也是一个协程构建器,它将一个常规的 main() 的非协程世界与 runBlocking {…} 大括号内的协程代码连接起来。在 IDE 中,在 runBlocking {…} 的起始括号后面会有 this:CoroutineScope 的提示,如果你删除或者忘记在上述代码中添加 runBlocking {…},那么 launch 的调用会收到错误,因为它只能在 CoroutineScope 内使用。
runBlocking 这个名字表示运行它的线程(在这个例子中是主线程)在被调用期间会被阻塞,直到 runBlocking 内的所有协程执行完毕。你经常会在应用程序的最顶层看到这样使用 runBlocking,而在真正的代码之内则很少如此,因为线程是代价高昂的资源,阻塞线程是比较低效的,我们通常并不希望阻塞线程。
结构化并发
协程遵循结构化并发,意思是说新的协程只能在一个指定的 CoroutineScope 内启动,CoroutineScope 界定了协程的生命周期。上面的例子展示了 runBlocking 建立了相应的作用域(Scope)。
在真实的应用程序中,你会启动很多协程。结构化的并发保证协程不会丢失或泄露。直到所有子协程结束之前,外层的作用范围不会结束。结构化的并发还保证代码中的任何错误都会正确的向外报告,不会丢失。
1.2 提取函数重构
假如你想要将 launch 代码块内的代码抽取到一个单独的函数中,当你在 IDE 中点击 “Extract function” 选项时,你会得到一个 suspend 修饰的函数,即挂起函数。挂起函数在协程内部可以被当作普通函数使用,它额外的功能是,调用其他挂起函数(就像上例中的 delay 那样)以挂起协程的执行。
1.3 作用域构建器
除了由不同的构建器提供协程作用域之外,还可以使用 coroutineScope 构建器声明自己的作用域。它创建一个协程作用域,并且不会在所有已启动的子协程执行完毕之前结束。
runBlocking 和 coroutineScope 看起来很相似,都会等待其协程体以及所有子协程结束。主要区别在于,runBlocking 会阻塞当前线程来等待,而 coroutineScope 只是挂起,会释放底层线程用于其他用途。由于存在这点差异,runBlocking 是常规函数,而 coroutineScope 是挂起函数。
你可以在挂起函数中使用 coroutineScope。例如,将并发打印 Hello 和 World 的操作移入 suspend fun doWorld() 函数中:
fun main() = runBlocking {doWorld()
}suspend fun doWorld() = coroutineScope { // this: CoroutineScopelaunch {delay(1000L)println("World!")}println("Hello")
}// 输出结果:
Hello
World!
1.4 作用域构建器与并发
一个 coroutineScope 构建器可以在任意挂起函数内使用以进行多个并发操作。在挂起函数 doWorld 内启动两个并发的协程:
// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {doWorld()println("Done")
}// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScopelaunch {delay(2000L)println("World 2")}launch {delay(1000L)println("World 1")}println("Hello")
}// 输出结果:
Hello
World 1
World 2
Done
1.5 显式任务
launch 这个协程构建器会返回一个 Job 对象,它是被启动的协程的处理器,可以用来显式地等待协程结束。比如,你可以等待子协程结束后再输出 Done:
val job = launch { // launch a new coroutine and keep a reference to its Jobdelay(1000L)println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done") // 输出结果:
Hello
World!
Done
1.6 协程是轻量的
与 JVM 线程相比,协程消耗更少的资源。有些代码使用线程时会耗尽 JVM 的可用内存,如果用协程来表达,则不会达到资源上限。比如,以下代码启动 50000 个不同的协程,每个协程等待 5 秒,然后打印一个点号(‘.’),但只消耗非常少的内存:
fun main() = runBlocking {repeat(50_000) { // launch a lot of coroutineslaunch {delay(5000L)print(".")}}
}
如果使用线程来实现相同功能(删除 runBlocking,将 launch 替换为 thread,将 delay 替换为 Thread.sleep),程序可能会消耗过多内存,抛出内存不足(out-of-memory)的错误或者启动线程缓慢。
2、取消与超时
2.1 取消协程执行
在一个长时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。 比如说,用户关闭了一个启动了协程的界面,那么现在不再需要协程的执行结果,这时,它应该是可以被取消的。 launch 会返回一个 Job,可以用来取消运行中的协程:
val job = launch {repeat(1000) { i ->println("job: I'm sleeping $i ...")delay(500L)}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
job.join() // waits for job's completion
println("main: Now I can quit.")
输出结果如下:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
调用了 job.cancel() 后,该协程会被取消。这里也可以使用 Job 的挂起函数 cancelAndJoin,它合并了对 cancel() 和 join() 的调用。
2.2 取消是协作式的
协程取消是协作式的。协程代码必须进行协作才能被取消。在 kotlinx.coroutines 中的所有挂起函数都是可取消的。它们会检查协程是否已取消,并在取消时抛出 CancellationException 异常。然而,如果协程正在进行计算并且没有检查取消,则它无法被取消,就像以下示例所示:
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {var nextPrintTime = startTimevar i = 0// 循环中并没有检查协程是否取消的代码,因此它无法在进入循环后被取消while (i < 5) { // computation loop, just wastes CPU// print a message twice a secondif (System.currentTimeMillis() >= nextPrintTime) {println("job: I'm sleeping ${i++} ...")nextPrintTime += 500L}}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
输出结果:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
main: Now I can quit.
查看结果发现在取消协程之后仍在继续打印,直到迭代完成。
同样的问题可以通过捕获 CancellationException 并不重新抛出它来观察到:
val job = launch(Dispatchers.Default) {repeat(5) { i ->try {// print a message twice a secondprintln("job: I'm sleeping $i ...")delay(500)} catch (e: Exception) {// log the exceptionprintln(e)}}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")// 输出结果:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@3151f9b5
job: I'm sleeping 3 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@3151f9b5
job: I'm sleeping 4 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@3151f9b5
main: Now I can quit.
虽然捕获 Exception 是一种反模式,但这个问题可能以更微妙的方式浮现,例如在使用 runCatching 函数时,它不会重新抛出 CancellationException。
2.3 使计算代码可取消
确保计算代码可取消的方法有两种:一是定期调用一个挂起函数来检查取消状态,yield 函数是一个很好的选择;二是显式地检查取消状态。我们来试试后者:
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {var nextPrintTime = startTimevar i = 0while (isActive) { // cancellable computation loop// print a message twice a secondif (System.currentTimeMillis() >= nextPrintTime) {println("job: I'm sleeping ${i++} ...")nextPrintTime += 500L}}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
输出结果:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
如你所见,现在这个循环被取消了。isActive 是通过 CoroutineScope 对象在协程内部可用的扩展属性。
2.4 在 finally 中关闭资源
可取消的挂起函数在取消时会抛出 CancellationException 异常,我们可以像处理其他异常一样来处理它。例如,可以使用 try {...} finally {...} 表达式或 Kotlin 的 use 函数来执行终止操作:
val job = launch {try {repeat(1000) { i ->println("job: I'm sleeping $i ...")delay(500L)}} finally {println("job: I'm running finally")}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
join 和 cancelAndJoin 函数都会等待所有终止操作完成,因此上面的示例将产生以下输出:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
main: Now I can quit.
2.5 运行不能取消的代码块
在前面的示例中,在 finally 块中使用挂起函数会导致 CancellationException 异常,因为协程已被取消。通常情况下,这不是问题,因为所有良好行为的关闭操作(如关闭文件、取消作业或关闭任何类型的通信通道)通常都是非阻塞的,不涉及任何挂起函数。然而,在极少数情况下,如果你需要在已取消的协程中挂起,则可以使用 withContext(NonCancellable) 来包装相应的代码。下面的示例展示了如何使用 withContext 函数和 NonCancellable 上下文:
val job = launch {try {repeat(1000) { i ->println("job: I'm sleeping $i ...")delay(500L)}} finally {withContext(NonCancellable) {println("job: I'm running finally")delay(1000L)println("job: And I've just delayed for 1 sec because I'm non-cancellable")}}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
输出如下:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
job: And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.
2.6 超时
取消协程执行的最明显的实际原因是因为其执行时间已经超过了某个超时时间。虽然你可以手动跟踪对应的 Job 引用,并启动一个单独的协程来延迟取消跟踪的协程,但是有一个名为 withTimeout 的函数可以直接使用。以下是一个示例:
withTimeout(1300L) {repeat(1000) { i ->println("I'm sleeping $i ...")delay(500L)}
}
输出如下:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
TimeoutCancellationException 是 CancellationException 的子类。我们之前没有在控制台上看到它的堆栈跟踪,是因为在已取消的协程中,CancellationException 被认为是协程完成的正常原因。然而,在这个示例中,我们在 main 函数中直接使用了 withTimeout 函数,因此在主函数中抛出的 TimeoutCancellationException 将被打印到控制台上。
由于取消只是一个异常,所有的资源都可以按照通常的方式关闭。如果你需要在任何类型的超时上执行一些额外的操作,可以将包含超时代码的 try {...} catch (e: TimeoutCancellationException) {...} 块包装起来。另外,如果你希望在超时时返回 null 而不是抛出异常,可以使用 withTimeoutOrNull 函数,它与 withTimeout 函数类似,但在超时时返回 null:
val result = withTimeoutOrNull(1300L) {repeat(1000) { i ->println("I'm sleeping $i ...")delay(500L)}"Done" // will get cancelled before it produces this result
}
println("Result is $result")
输出结果:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null
2.7 异步超时与资源
withTimeout 函数中的超时事件是相对于其块中正在运行的代码异步发生的,可能会在超时块内部的返回之前发生。如果在块内部打开或获取某些需要在块外部关闭或释放的资源,请记住这一点。
例如,下面的示例中,我们使用 Resource 类模拟一个可关闭的资源,该类通过增加 acquired 计数器来跟踪资源被创建的次数,并在其 close 函数中减少计数器。现在,让我们创建许多协程,每个协程都在 withTimeout 块的末尾创建一个 Resource,并在块外部释放该资源。我们添加一个小延迟,以便超时事件更有可能在 withTimeout 块已经完成时发生,这将导致资源泄漏。
var acquired = 0class Resource {init { acquired++ } // Acquire the resourcefun close() { acquired-- } // Release the resource
}fun main() {runBlocking {repeat(10_000) { // Launch 10K coroutineslaunch { val resource = withTimeout(60) { // Timeout of 60 msdelay(50) // Delay for 50 msResource() // Acquire a resource and return it from withTimeout block }resource.close() // Release the resource}}}// Outside of runBlocking all coroutines have completedprintln(acquired) // Print the number of resources still acquired
}
如果你运行上面的代码,会发现它并不总是打印零,虽然这可能取决于你的机器的时间安排。你可能需要调整这个示例中的超时时间,以实际看到非零值。
请注意,在这里从 10000 个协程中对
acquired计数器进行递增和递减是完全线程安全的,因为它总是发生在同一个线程上,即runBlocking使用的线程上。更多关于此的解释将在协程上下文的章节中进行说明。
为了解决这个问题,你可以将资源的引用存储在变量中,而不是从 withTimeout 块中返回它。这样,即使超时事件发生,资源也不会在超时块之外被释放,因为它们的引用仍然存在于变量中。
runBlocking {repeat(10_000) { // Launch 10K coroutineslaunch { var resource: Resource? = null // Not acquired yettry {withTimeout(60) { // Timeout of 60 msdelay(50) // Delay for 50 msresource = Resource() // Store a resource to the variable if acquired }// We can do something else with the resource here} finally { resource?.close() // Release the resource if it was acquired}}}
}
// Outside of runBlocking all coroutines have completed
println(acquired) // Print the number of resources still acquired
这个示例总是打印零。资源不会泄漏。
3、组合挂起函数
3.1 默认顺序调用
假设我们在其他地方定义了两个挂起函数,它们可以做一些有用的事情,比如远程服务调用或计算。我们只是假装它们有用,但实际上每个函数只是为了这个例子而延迟一秒钟:
suspend fun doSomethingUsefulOne(): Int {delay(1000L) // pretend we are doing something useful herereturn 13
}suspend fun doSomethingUsefulTwo(): Int {delay(1000L) // pretend we are doing something useful here, tooreturn 29
}
如果我们需要按顺序调用它们 —— 首先执行 doSomethingUsefulOne,然后执行 doSomethingUsefulTwo,并计算它们结果的总和,该怎么办?在实践中,如果我们使用第一个函数的结果来决定是否需要调用第二个函数或决定如何调用第二个函数,我们会使用普通的顺序调用,因为协程中的代码与常规代码一样,默认情况下是顺序的。以下示例通过测量两个挂起函数执行所需的总时间来演示它:
val time = measureTimeMillis {val one = doSomethingUsefulOne()val two = doSomethingUsefulTwo()println("The answer is ${one + two}")
}
println("Completed in $time ms")
输出结果:
The answer is 42
Completed in 2017 ms
3.2 async 并发
如果在调用 doSomethingUsefulOne 和 doSomethingUsefulTwo 之间没有依赖关系,并且我们想通过同时执行两者来更快地获取答案,那么可以使用 async 函数来实现。
概念上,async 与 launch 非常相似。它们都会启动一个单独的协程,该协程是一个轻量级的线程,可以与其他协程并发工作。它们的区别在于,launch 函数返回一个 Job 对象,并不返回任何结果值,而 async 函数返回一个 Deferred 对象,它是一个轻量级的非阻塞 future,表示承诺稍后提供结果。你可以在 Deferred 值上使用 .await() 函数来获取其最终结果,但是 Deferred 对象也是一个 Job 对象,因此如果需要,可以取消它。
val time = measureTimeMillis {val one = async { doSomethingUsefulOne() }val two = async { doSomethingUsefulTwo() }println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
输出如下:
The answer is 42
Completed in 1017 ms
这样做的速度是原来的两倍,因为这两个协程是并发执行的。请注意,协程的并发始终是显式的。
3.3 延迟启动 async
可选地,可以通过将 async 的 start 参数设置为 CoroutineStart.LAZY,使 async 变为延迟启动。在这种模式下,它仅在 await 需要其结果时或者在调用其 Job 的 start 函数时启动协程。运行以下示例:
val time = measureTimeMillis {val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }// some computationone.start() // start the first onetwo.start() // start the second oneprintln("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
输出如下:
The answer is 42
Completed in 1017 ms
因此,在这里定义了两个协程,但不像前面的示例那样执行它们,而是将控制权交给程序员,由程序员决定何时调用 start() 开始执行。我们首先启动一个协程,然后启动另一个协程,最后等待各个协程完成。
需要注意的是,如果我们在 println 中只调用 await 而不先在各个协程上调用 start,那么这将导致顺序行为,因为 await 会启动协程执行并等待其完成,这不是延迟启动的预期用例。async(start = CoroutineStart.LAZY) 的用例是在计算值涉及挂起函数的情况下,替换标准的 lazy 函数。
3.4 Async 风格的函数(Async-style functions)
我们可以使用 async 协程构建器定义异步调用 doSomethingUsefulOne 和 doSomethingUsefulTwo 的 Async 风格函数,使用 GlobalScope 引用来退出结构化并发。我们使用“…Async”后缀命名这样的函数,以突出它们仅启动异步计算,并且需要使用生成的延迟值来获取结果的事实。
GlobalScope 是一个敏感的 API,可能会以非平凡的方式产生反作用,其中一种将在下面解释,因此你必须显式使用 @OptIn(DelicateCoroutinesApi::class) 来选择使用 GlobalScope。
// The result type of somethingUsefulOneAsync is Deferred<Int>
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {doSomethingUsefulOne()
}// The result type of somethingUsefulTwoAsync is Deferred<Int>
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulTwoAsync() = GlobalScope.async {doSomethingUsefulTwo()
}
需要注意的是,这些 xxxAsync 函数不是挂起函数。它们可以从任何地方使用。然而,它们的使用始终意味着将它们的操作与调用代码异步(这里是指并发)执行。
以下示例展示了它们在协程外部的使用:
// note that we don't have `runBlocking` to the right of `main` in this example
fun main() {val time = measureTimeMillis {// we can initiate async actions outside of a coroutineval one = somethingUsefulOneAsync()val two = somethingUsefulTwoAsync()// but waiting for a result must involve either suspending or blocking.// here we use `runBlocking { ... }` to block the main thread while waiting for the resultrunBlocking {println("The answer is ${one.await() + two.await()}")}}println("Completed in $time ms")
}
这种使用异步函数的编程风格在此仅作为说明提供,因为它是其他编程语言中的一种流行风格。使用这种风格与 Kotlin 协程强烈不建议,原因如下所述。
考虑以下情况:在 val one = somethingUsefulOneAsync() 行和 one.await() 表达式之间的代码中存在某些逻辑错误,程序抛出异常,并且程序正在执行的操作中止。通常,全局错误处理程序可以捕获此异常,记录和报告开发人员的错误,但程序仍然可以继续执行其他操作。但是,在这里,somethingUsefulOneAsync 仍然在后台运行,即使启动它的操作已中止。结构化并发不会出现这个问题,如下面的部分所示。
3.5 使用 async 的结构化并发
让我们拿并发使用 async 的例子,并提取一个函数,该函数同时执行 doSomethingUsefulOne 和 doSomethingUsefulTwo,并返回它们结果的总和。由于 async 协程构建器是在 CoroutineScope 上定义的扩展,因此我们需要在作用域中拥有它,这就是 coroutineScope 函数提供的:
suspend fun concurrentSum(): Int = coroutineScope {val one = async { doSomethingUsefulOne() }val two = async { doSomethingUsefulTwo() }one.await() + two.await()
}
这样,如果 concurrentSum 函数内部发生错误,并抛出异常,那么在其作用域内启动的所有协程都将被取消。
我们仍然可以看到两个操作的并发执行,正如上面的 main 函数输出所示:
The answer is 42
Completed in 1017 ms
取消请求始终会通过协程层次结构进行传播:
fun main() = runBlocking<Unit> {try {failedConcurrentSum()} catch(e: ArithmeticException) {println("Computation failed with ArithmeticException")}
}suspend fun failedConcurrentSum(): Int = coroutineScope {val one = async<Int> { try {delay(Long.MAX_VALUE) // Emulates very long computation42} finally {println("First child was cancelled")}}val two = async<Int> { println("Second child throws an exception")throw ArithmeticException()}one.await() + two.await()
}
请注意,当子协程(即 two)失败时,第一个 async 和等待其完成的父协程都将被取消:
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException
4、协程上下文与调度器
协程始终在某些上下文中执行,该上下文由 Kotlin 标准库中 CoroutineContext 类型的值表示。协程上下文是各种元素的集合,主要元素是协程的 Job,及其调度器。
4.1 调度器与线程
协程上下文包括一个协程调度器(参见 CoroutineDispatcher),该调度器确定相应协程用于执行的线程或线程集。协程调度器可以将协程执行限制在特定线程上,将其调度到线程池中,或允许其无限制地运行。
所有协程构建器(如 launch 和 async)都接受一个可选的 CoroutineContext 参数,该参数可用于显式指定新协程的调度器和其他上下文元素。
尝试以下示例:
launch { // context of the parent, main runBlocking coroutineprintln("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // not confined -- will work with main threadprintln("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) { // will get its own new threadprintln("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
输出如下(顺序可能不同):
Unconfined : I'm working in thread main
Default : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking : I'm working in thread main
当不带参数使用 launch 时,它会从启动它的 CoroutineScope 继承上下文(因此也继承调度器)。在这种情况下,它继承了运行在主线程中的 main runBlocking 协程的上下文。
Dispatchers.Unconfined 是一种特殊的调度器,看起来也在主线程中运行,但实际上是一种不同的机制,稍后会进行解释。
当作用域中没有显式指定其他调度器时,默认调度器将被使用。它由 Dispatchers.Default 表示,并使用共享的后台线程池。
newSingleThreadContext 为协程创建一个线程来运行。专用线程是非常昂贵的资源。在实际应用中,当不再需要时,它必须使用 close 函数释放,或者存储在顶级变量中并在整个应用程序中重复使用。
4.2 自由调度器与受限调度器(Unconfined vs confined dispatcher)
Dispatchers.Unconfined 协程调度器在调用者线程中启动一个协程,但仅在第一个挂起点之前。在挂起之后,它会在完全由调用的挂起函数决定的线程中恢复协程。Unconfined 调度器适用于既不消耗 CPU 时间,也不更新特定线程中的任何共享数据(如 UI)的协程。
另一方面,默认情况下从外部 CoroutineScope 继承的调度器,特别是对于 runBlocking 协程的默认调度器,它被限制在调用者线程中,因此继承它的效果是将执行限制在这个线程中,具有可预测的 FIFO 调度:
launch(Dispatchers.Unconfined) { // not confined -- will work with main threadprintln("Unconfined : I'm working in thread ${Thread.currentThread().name}")delay(500)println("Unconfined : After delay in thread ${Thread.currentThread().name}")
}
launch { // context of the parent, main runBlocking coroutineprintln("main runBlocking: I'm working in thread ${Thread.currentThread().name}")delay(1000)println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
}
输出:
Unconfined : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main
因此,使用从 runBlocking{...} 继承的上下文启动的协程将继续在主线程中执行,而使用 Dispatchers.Unconfined 启动的协程将最初在主线程中启动,但在遇到挂起点(例如 delay 函数)时将在默认执行器线程中恢复执行。它恢复执行的线程将由下一个挂起函数关联的调度器决定,例如在本例中与默认执行器线程相关联的 delay 函数。
Unconfined 调度器是一种高级机制,可以在某些特殊情况下提供帮助,比如不需要将协程分派到稍后执行,或者分派会产生不良副作用,因为某个协程中的某个操作必须立即执行。不过,一般情况下不应该在代码中使用 Unconfined 调度器。
4.3 调试协程与线程
协程可以在一个线程上挂起,在另一个线程上恢复执行。即使在单线程调度器中,如果没有特殊的工具来帮助,也可能很难确定协程正在做什么、在哪里以及何时执行。
使用 IDEA 调试
Kotlin 插件的 Coroutine Debugger 简化了在 IntelliJ IDEA 中调试协程的过程。
调试功能适用于 kotlinx-coroutines-core 版本 1.3.8 或更高版本。
调试工具窗口包含“协程”选项卡。在此选项卡中,你可以找到有关当前正在运行和挂起的协程的信息。协程按它们正在运行的调度器进行分组。
使用协程调试器,你可以:
- 检查每个协程的状态。
- 查看运行中和挂起的协程的本地变量和捕获的变量的值。
- 查看完整的协程创建堆栈,以及协程内部的调用堆栈。堆栈包括所有带有变量值的帧,即使在标准调试期间也会丢失这些帧。
- 获取包含每个协程及其堆栈状态的完整报告。要获取报告,请在“协程”选项卡中右键单击,然后单击“获取协程转储”。
要开始进行协程调试,只需要设置断点并以调试模式运行应用程序。
可以在教程中了解更多关于协程调试的内容。
使用日志调试
在没有 Coroutine Debugger 的情况下,使用线程打印日志文件中的线程名称是调试带有线程的应用程序的另一种方法。此功能由日志记录框架广泛支持。使用协程时,仅使用线程名称并不能提供太多上下文,因此 kotlinx.coroutines 包括调试工具以使其更容易。
使用 JVM 选项 -Dkotlinx.coroutines.debug 运行下列代码:
val a = async {log("I'm computing a piece of the answer")6
}
val b = async {log("I'm computing another piece of the answer")7
}
log("The answer is ${a.await() * b.await()}")
这段代码中有三个协程。主协程 (#1) 在 runBlocking 中,另外两个协程计算延迟值 a (#2) 和 b (#3)。它们都在 runBlocking 的上下文中执行,并且限制在主线程中。该代码的输出为:
[main @coroutine#2] I'm computing a piece of the answer
[main @coroutine#3] I'm computing another piece of the answer
[main @coroutine#1] The answer is 42
log 函数在方括号中打印线程名称,你可以看到它是主线程,后面附加了当前执行协程的标识符。当调试模式启用时,此标识符会按顺序分配给所有创建的协程。
4.4 在线程之间跳转
使用 JVM 选项 -Dkotlinx.coroutines.debug 运行下列代码:
newSingleThreadContext("Ctx1").use { ctx1 ->newSingleThreadContext("Ctx2").use { ctx2 ->runBlocking(ctx1) {log("Started in ctx1")withContext(ctx2) {log("Working in ctx2")}log("Back to ctx1")}}
}
它展示了几种新技术。其中一种是使用带有显式指定上下文的 runBlocking 函数,另一种是使用 withContext 函数来改变协程的上下文,同时仍然保持在同一个协程中,正如你可以在下面的输出中看到的那样:
[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1
注意,此示例还使用 Kotlin 标准库中的 use 函数,该函数会在不再需要 newSingleThreadContext 创建的线程时释放它们。
4.5 上下文中的 Job
协程的 Job 是其上下文的一部分,可以使用 coroutineContext[Job] 表达式从上下文中检索它:
println("My job is ${coroutineContext[Job]}")
在调试模式下会输出如下内容:
My job is "coroutine#1":BlockingCoroutine{Active}@6d311334
注意,CoroutineScope 中的 isActive 只是一个方便的快捷方式,用于检查 coroutineContext[Job]?.isActive == true。
4.6 子协程
当在另一个协程的 CoroutineScope 中启动协程时,它通过 CoroutineScope.coroutineContext 继承其上下文,并且新协程的 Job 成为父协程 Job 的子 Job。当取消父协程时,所有子协程也会被递归取消。
然而,父子关系可以通过以下两种方式之一显式地覆盖:
- 当在启动协程时显式指定不同的范围(例如 GlobalScope.launch)时,它不会从父范围继承 Job。
- 当将不同的 Job 对象作为新协程的上下文传递(如下面的示例所示)时,它会覆盖父范围的 Job。
在这两种情况下,启动的协程不会绑定到它所启动的范围上,并且独立运行。
// launch a coroutine to process some kind of incoming request
val request = launch {// it spawns two other jobslaunch(Job()) { println("job1: I run in my own Job and execute independently!")delay(1000)println("job1: I am not affected by cancellation of the request")}// and the other inherits the parent contextlaunch {delay(100)println("job2: I am a child of the request coroutine")delay(1000)println("job2: I will not execute this line if my parent request is cancelled")}
}
delay(500)
request.cancel() // cancel processing of the request
println("main: Who has survived request cancellation?")
delay(1000) // delay the main thread for a second to see what happens
输出如下:
job1: I run in my own Job and execute independently!
job2: I am a child of the request coroutine
main: Who has survived request cancellation?
job1: I am not affected by cancellation of the request
4.7 父协程的职责
父协程总是会等待所有子协程完成。父协程无须显式追踪所有子协程的启动,并且无须在最后使用 Job.join() 等待子协程:
// launch a coroutine to process some kind of incoming request
val request = launch {repeat(3) { i -> // launch a few children jobslaunch {delay((i + 1) * 200L) // variable delay 200ms, 400ms, 600msprintln("Coroutine $i is done")}}println("request: I'm done and I don't explicitly join my children that are still active")
}
request.join() // wait for completion of the request, including all its children
println("Now processing of the request is complete")
输出结果:
request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done
Now processing of the request is complete
4.8 命名协程以调试
当协程经常打印日志并且你只需要关联来自同一个协程的日志记录时, 则自动分配的 id 是好的。然而,当一个协程与特定请求的处理相关联或做一些特定的后台任务时,最好将其明确命名以用于调试。 CoroutineName 上下文元素与线程名具有相同的目的,当调试模式开启时,它被包含在正在执行此协程的线程名中。
下面的例子演示了这一概念:
log("Started main coroutine")
// run two background value computations
val v1 = async(CoroutineName("v1coroutine")) {delay(500)log("Computing v1")252
}
val v2 = async(CoroutineName("v2coroutine")) {delay(1000)log("Computing v2")6
}
log("The answer for v1 / v2 = ${v1.await() / v2.await()}")
使用 JVM 选项 -Dkotlinx.coroutines.debug 运行代码,输出类似于:
[main @main#1] Started main coroutine
[main @v1coroutine#2] Computing v1
[main @v2coroutine#3] Computing v2
[main @main#1] The answer for v1 / v2 = 42
4.9 组合上下文元素
有时我们需要在协程上下文中定义多个元素。我们可以使用 + 操作符来实现。 比如说,我们可以显式指定一个调度器来启动协程并且同时显式指定一个命名:
launch(Dispatchers.Default + CoroutineName("test")) {println("I'm working in thread ${Thread.currentThread().name}")
}
使用 -Dkotlinx.coroutines.debug JVM 参数,输出如下所示:
I'm working in thread DefaultDispatcher-worker-1 @test#2
4.10 协程作用域
让我们将关于上下文、子协程和 Job 的知识结合起来。假设我们的应用程序有一个具有生命周期的对象,但该对象不是协程。例如,我们正在编写一个 Android 应用程序,并在 Android Activity 的上下文中启动各种协程以执行异步操作来获取和更新数据、进行动画等。所有这些协程在活动被销毁时必须被取消,以避免内存泄漏。当然,我们可以手动操作上下文和 Job,以绑定 Activity 及其协程的生命周期,但 kotlinx.coroutines 提供了一种封装的抽象:CoroutineScope。你应该已经熟悉了 CoroutineScope,因为所有协程构建器都声明为其扩展。
我们通过创建与 Activity 生命周期相关联的 CoroutineScope 实例来管理协程的生命周期。CoroutineScope 实例可以通过 CoroutineScope() 或 MainScope() 工厂函数创建。前者创建一个通用作用域,而后者创建一个用于 UI 应用程序的作用域,并将 Dispatchers.Main 作为默认调度器:
class Activity {private val mainScope = MainScope()fun destroy() {mainScope.cancel()}// to be continued ...
现在,我们可以使用定义的作用域在此 Activity 的作用域内启动协程:
// class Activity continuesfun doSomething() {// launch ten coroutines for a demo, each working for a different timerepeat(10) { i ->mainScope.launch {delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etcprintln("Coroutine $i is done")}}}
} // class Activity ends
在我们的主函数中,我们创建该 Activity,调用我们的测试函数 doSomething,并在 500ms 后销毁该 Activity。这将取消从 doSomething 启动的所有协程。我们可以看到,在 Activity 销毁后,即使我们等待更长时间,也不会再打印出任何消息了。
val activity = Activity()
activity.doSomething() // run test function
println("Launched coroutines")
delay(500L) // delay for half a second
println("Destroying activity!")
activity.destroy() // cancels all coroutines
delay(1000) // visually confirm that they don't work
输出如下:
Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!
只有前两个协程打印了消息,而其他协程则被 Activity.destroy() 中的 job.cancel() 取消了。
注意,Android 在所有具有生命周期的实体中都提供了协程作用域的一级支持。请参阅相应的文档。
线程本地数据
有时候,将一些线程本地数据传递给协程,或在协程之间传递这些数据非常方便。但是,由于协程不绑定到任何特定的线程,如果手动完成这个过程,可能会导致样板代码的出现。
对于 ThreadLocal,扩展函数 asContextElement 可以解决这个问题。它创建了一个附加的上下文元素,保留了给定 ThreadLocal 的值,并在每次协程切换其上下文时恢复它的值。
很容易通过演示来展示它的工作原理:
threadLocal.set("main")
println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")yield()println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}
job.join()
println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
使用 Dispatchers.Default 在后台线程池中启动了一个新的协程,因此它在与线程池不同的线程上工作,但它仍然具有我们使用threadLocal.asContextElement(value = “launch”) 指定的线程本地变量的值,无论协程在哪个线程上执行。因此,输出(带有调试信息)如下所示:
Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Launch start, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch'
After yield, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
很容易忘记设置相应的上下文元素。如果运行协程的线程不同,从协程访问的线程本地变量可能具有意外的值。为避免这种情况,建议使用 ensurePresent 方法,在使用不正确时立即抛出异常。
ThreadLocal 具有一流的支持,并且可以与 kotlinx.coroutines 提供的任何原语一起使用。但它有一个关键限制:当 ThreadLocal 被修改时,新值不会传播到协程调用方(因为上下文元素无法跟踪所有 ThreadLocal 对象访问),并且在下一次挂起时更新的值会丢失。使用 withContext 在协程中更新线程本地变量的值,有关更多详细信息,请参见 asContextElement。
或者,可以将值存储在类似于 Counter(var i: Int) 的可变框内,这个框又存储在线程本地变量中。但是,在这种情况下,您完全负责同步对该可变框中变量的潜在并发修改。
对于高级用途,例如与日志 MDC、事务上下文或任何其他在内部使用线程本地变量传递数据的库集成,请参阅应该实现的 ThreadContextElement 接口的文档。
相关文章:
Kotlin 协程基础知识汇总(一)
1、协程基础 Kotlin 是一门仅在标准库中提供最基本底层 API 以便其他库能够利用协程的语言。与许多其他具有类似功能的语言不同,async 与 await 在 Kotlin 中并不是关键字,甚至都不是标准库的一部分。此外,Kotlin 的挂起函数概念为异步操作提…...
Deepseek训练成AI图片生成机器人
目录 内容安全层 语义理解层 提示词工程层 图像生成层 交付系统 训练好的指令(复制就可以) 内容安全层 理论支撑:基于深度语义理解的混合过滤系统 敏感词检测:采用BERT+CRF混合模型,建立三级敏感词库(显性/隐性/文化禁忌),通过注意力机制捕捉上下文关联风险 伦…...
关于MTU的使用(TCP/IP网络下载慢可能与此有关)
参考链接:告诉你mtu值怎么设置才能网速最好! -Win7系统之家 出现网络速度被限制,可能与MTU值相关,先查看下本机的MTU winR,然后输入:netsh interface ipv4 show subinterfaces ,查看自己网络中的MTU&…...
【信息系统项目管理师】【高分范文】【历年真题】论信息系统项目的风险管理
【手机端浏览】☞【信息系统项目管理师】【高分范文】【历年真题】论信息系统项目的风险管理 2023年上半年考题 【题目】 论信息系统项目的风险管理 项目风险管理旨在识别和管理未被项目计划及其他过程所管理的风险,如果不妥善管理,这些风险可能导致项…...
Debain-12.9使用vllm部署内嵌模型/embedding
Debain-12.9使用vllm部署内嵌模型/embedding 基础环境准备下载模型部署模型注册dify模型 基础环境准备 基础环境安装 下载模型 modelscope download --model BAAI/bge-m3 --local_dir BAAI/bge-m3部署模型 vllm serve ~/ollama/BAAI/bge-m3 --served-model-name bge-m3 --t…...
香橙派连接摄像头过程
在香橙派上下载NoMachine 在控制电脑上也下载NoMachine sudo nmcli dev wifi connect "你的WiFi名称" password "你的WiFi密码" 连接上wifi后就可以在NoMachine连上香橙派了 (不过前提是香橙派有安装桌面端系统(非仅窗口端&…...
Milvus学习整理
Milvus学习整理 一、度量类型(metric_type) 二、向量字段和适用场景介绍 三、索引字段介绍 (一)、概述总结 (二)、详细说明 四、简单代码示例 (一)、建立集合和索引示例 (二)…...
MySQL事务全解析:从概念到实战
在数据库操作中,事务是一个至关重要的概念,它确保了数据的完整性和一致性。今天,就让我们深入探讨MySQL事务的方方面面,从基础概念到实际应用,全面掌握这一技能。 一、为什么需要事务 假设张三要给李四转账100元&…...
重叠构造函数 、JavaBean模式、建造者模式、Spring的隐性大手
构造函数 重叠构造函数JavaBean模式建造者模式构造Spring看起来为什么简单番外篇为什么在JavaBean中 无参构造函数是必须的呢 小结 构造函数对我来讲是很平常的一个东西,今天来谈谈新的收获。 重叠构造函数 通常我们定义好实体类后,不会特意的去调整构造…...
题单:精挑细选
题目描述 小王是公司的仓库管理员,一天,他接到了这样一个任务:从仓库中找出一根钢管。这听起来不算什么,但是这根钢管的要求可真是让他犯难了,要求如下: 1.1. 这根钢管一定要是仓库中最长的; …...
GGUF 和 llama.cpp 是什么关系
这是个非常关键的问题,咱们来细说下:GGUF 和 llama.cpp 是什么关系,它们各自干什么,如何配合工作。 🔧 一、llama.cpp 是什么? llama.cpp 是 Meta 的开源大语言模型 LLaMA(Language Model from…...
手机怎么换网络IP有什么用?操作指南与场景应用
在数字化时代,手机已经成为我们日常生活中不可或缺的一部分,无论是工作、学习还是娱乐,手机都扮演着至关重要的角色。而在手机的使用过程中,网络IP地址作为设备在互联网上的唯一标识符,其重要性和作用不容忽视。本文将…...
强化学习中的深度卷积神经网络设计与应用实例
I. 引言 强化学习(Reinforcement Learning,RL)是机器学习的一个重要分支,通过与环境的交互来学习最优策略。深度学习,特别是深度卷积神经网络(Deep Convolutional Neural Networks,DCNNs&#…...
软考程序员-操作系统基本知识核心考点和知识重点总结
以下是软考程序员考试中操作系统基本知识章节的核心考点和知识重点总结,结合历年真题和考试大纲整理而成: 一、操作系统基本概念与功能 定义与作用 操作系统是管理计算机软硬件资源的核心系统软件,负责协调程序执行、优化资源利用,…...
思源配置阿里云 OSS 踩坑记
按照正常的配置IAM,赋予OSS权限,思源笔记还是无法使用,缺少ListBuckets权限。 正常配置权限,又无法覆盖,因此需要手动配置权限。 {"Version": "1","Statement": [{"Effect":…...
科技赋能安全:慧通测控的安全带全静态性能测试
汽车的广泛普及给人们的出行带来了极大便利,但交通事故频发也成为严重的社会问题。据世界卫生组织统计,全球每年约有 135 万人死于道路交通事故,而安全带在减少事故伤亡方面起着不可替代的作用。正确使用安全带可使前排驾乘人员的死亡风险降低…...
记录修复一个推拉门滑轮
推拉门有个滑轮的固定螺丝不知什么时候掉了,也找不到,这就导致推拉门卡在轨道上。 这种滑轮在夕夕上很便宜,比哈罗单车还划算,但是现在缺的只是螺丝,如果买就会多出来一个轮… 这种螺丝比较长,大概是m4的…...
压缩壳学习
壳是什么 壳就是软件的一个保护套,防止软件被进行反编译或被轻易地修改。 其作用就是为了保护软件。 常见的大类壳有压缩壳、加密壳、VM 壳的分类。 压缩壳顾名思义就是用来减小软件的文件大小的;加密壳,通过加密软件来保护软件ÿ…...
深入理解 Linux ALSA 音频架构:从入门到驱动开发
文章目录 一、什么是 ALSA?二、ALSA 系统架构全景图核心组件详解:三、用户空间开发实战1. PCM 音频流操作流程2. 高级配置(asound.conf)四、内核驱动开发指南1. 驱动初始化模板2. DMA 缓冲区管理五、高级主题1. 插件系统原理2. 调试技巧3. 实时音频优化六、现代 ALSA 发展七…...
#13【CVPR2024】“不确定性不是敌人”:深入剖析多模态融合中的不确定性
📜 Embracing Unimodal Aleatoric Uncertainty for Robust Multimodal Fusion 本文没有源码,适合基础好的读者 🍞 1:研究背景与问题定义 🍫 1.1 多模态融合的黄金承诺与现实落差 在人工智能的迅猛发展浪潮中,多模态学习(Multimodal Learning)扮演着越来越重要的角…...
使用 QR-Code-Styling 在 Vue 3 中生成二维码
使用 QR-Code-Styling 在 Vue 3 中生成二维码 1. 前言 二维码广泛应用于网站跳转、支付、身份认证等场景。普通的二维码较为单调,而 qr-code-styling 允许我们自定义二维码的颜色、Logo、样式,使其更具个性化。本文将介绍如何在 Vue 3 Element Plus 中…...
CCF-CSP认证 202206-2寻宝!大冒险!
题目描述 思路 有一张绿化图和藏宝图,其中绿化图很大(二维数组在限定的空间内无法存储),而藏宝图是绿化图中的一部分,对于绿化图和藏宝图,左下角的坐标为(0, 0),右上角的坐标是(L, L)、(S, S)&…...
Redis项目:秒杀业务(优化)
当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤 1、查询优惠卷 2、判断秒杀库存是否足够 3、查询订单 4、校验是否是一人一单 5、扣减库存 6、创建订单…...
《Gradio Python 客户端入门》
《Gradio Python 客户端入门》 Gradio Python 客户端使将任何 Gradio 应用程序用作 API 变得非常容易。例如,考虑这个 Hugging Face Space,它转录从麦克风录制的音频文件。 使用该库,我们可以轻松地将 Gradio 用作 API 以编程方式转录音频文…...
仿函数 VS 函数指针实现回调
前提: 本博客对比 函数指针实现回调 和 仿函数 ,突出仿函数的优势。 目的: 一个类要能够灵活的调用两个函数,essfc 和 greaterfc,分别用于比较两个整数的大小: ①:lessfc:判断 x …...
MQTT的安装和使用
MQTT的安装和使用 在物联网开发中,mqtt几乎已经成为了广大程序猿必须掌握的技术,这里小编和大家一起学习并记录一下~~ 一、安装 方式1、docker安装 官网地址 https://www.emqx.com/zh/downloads-and-install/broker获取 Docker 镜像 docker pull e…...
网络工程师考试详细介绍,讲解,备考方案
一、考试科目与形式 1. 科目1:基础知识(计算机与网络知识) - 考试形式:机考,75道选择题(含5道英文题),满分75分 - 核心内容: - 计算机系统:硬件组成&…...
ROS melodic 安装 python3 cv_bridge
有时候,我们需要处理这些兼容性问题。此处列举我的过程,以供参考 mkdir -p my_ws_py39/src cd my_ws_py39 catkin_make_isolated-DPYTHON_EXECUTABLE/usr/bin/python3 \-DPYTHON_INCLUDE_DIR/usr/include/python3.8 \-DPYTHON_LIBRARY/usr/lib/x86_64-l…...
SHELL练习01
判断一个数是奇数还是偶数 要求: 编写一个 Shell 脚本,用户输入一个整数,判断该数是奇数还是偶数,并输出结果。 [rootnode test01]# touch Determine parity.sh [rootnode test01]# vim Determine parity.sh 还有 2 个文件等待…...
PRODIGY: “不折腾人”的蛋白-蛋白/蛋白-小分子结合能计算工具
PRODIGY(全称为 PROtein binDIng enerGY prediction)是一种蛋白质结合能预测工具,可利用蛋白质-蛋白质复合物的三维结构来预测其结合亲和力。PRODIGY 利用一种高效的基于接触的方法,在估计结合自由能和解离常数的同时,…...
