并发金属版–并发不是并行性

在Kotlin Coroutines以及并发性与并行性不同

官方文档 将Kotlin Coroutines描述为“用于异步编程和更多”的工具,特别是Coroutines支持我们使用“异步或非阻塞编程”来支持我们。这到底是什么意思?如何与术语“并发”和“并行性”相关的“异步”,标签我们也在这个上下文中听到了很多。在本文中,我们将看到科纳丁主要关注并发性,而不是主要涉及并行性。 Coroutines提供复杂的方法,帮助我们构建代码使其高度同时可执行,也能够启用并行性,这不是默认行为。如果您尚不理解差异,请不要担心它,它将在整个文章中更清晰。许多人,我包括,努力正确使用这些术语。让我们了解更多有关考申的信息以及如何与讨论的主题相关。

(您可以找到Kotlin Coroutines的一般介绍 本文)

Asynchrony - 一个编程模型

异步编程是我们在过去几年中一直在阅读和听到很多的主题。它主要是指“独立于主程序流动的事件的发生”以及“处理这些事件的方法”(维基百科)。异步编程的一个关键方面是异步启动操作不会立即阻止程序并发生 同时。在异步编程时,我们经常发现自己触发某些子程序,立即返回调用者以让主程序流继续,而不等待子程序的结果。一旦需要结果,您可能会遇到两种情况:1)结果已完全处理,只能请求或2)您需要阻止您的程序,直到它可用。这就是期货或承诺的工作原理。另一个流行的异步的例子是反应流如何如此描述的 反应宣言:

反应系统依赖于 异步消息传递 建立元件之间的边界,确保耦合松动,隔离和位置透明度。 [...]非阻塞通信允许收件人仅在激活时消耗资源,导致更少的系统开销。

完全,我们可以描述在软件工程领域中定义的异步,作为一种编程模型,可实现非阻塞和并发编程。我们派遣任务以让我们的程序继续做其他事情,直到我们收到结果可用的信号。以下图像说明了这一点:

我们希望继续读一本书,因此让机器为我们洗涤。

免责声明:我拍了这两个图像 这个Quora帖子 这也描述了讨论的术语。

并发 - 它是关于结构

在我们学到了异步指的是什么之后,让我们看看什么 并发 是。并发不是,正如许多人错误地相信,关于在“平行”或“同时”运行的东西。 Rob Pike是谷歌工程师,最闻名于他的工作,描述了并发作为“独立执行任务的组成”,他强调并发真的是关于 构建程序。这意味着并发程序处理同一时间的多个任务,但不一定同时执行。所有任务的工作都可以以某种任意顺序进行交互,在这一点图像中很好地说明:

并发不是并行性。它试图分解我们不一定需要同时执行的任务。它的主要目标是 结构体, 不是 并行.

并行性 - 它是关于执行

并行性,通常错误地误认为并发,是关于同时执行多件事的。如果并发是关于结构的,则并行性是关于执行多个任务的执行。我们可以说,并发性使得平行性更容易,但自从我们可以拥有以来,它甚至不是先决条件 并行 without concurrency.

最后,正如Rob Pike所描述的那样:“并发是关于一次性地处理很多事情。并行性是关于一次性的事情”。你可以观看他的谈话“并发不是并行性” YouTube.

在并发和并行性方面的金属版

科素是关于 并发 首先。它们提供了优秀的工具,让我们将任务分解为默认情况下未同时执行的各种块。说明这是kotlin coroutines的一部分的一个简单示例 文件:


fun main() = runBlocking<Unit> { val time = measureTimeMillis { val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } println("这 answer is ${one.await() + two.await()}") } println("Completed in $time ms") } suspend fun doSomethingUsefulOne(): Int { delay(1000L) return 13 } suspend fun doSomethingUsefulTwo(): Int { delay(1000L) return 29 }

这 example terminates in roughly 1000 milliseconds since both "somethingUseful" tasks take about 1 second each and we execute them asynchronously with the help of the async coroutine builder. Both tasks just use a simple non-blocking delay to simulate some reasonably long-running action. Let's see if the framework executes these tasks truly simultaneously. Therefore we add some log statements that tell us the threads the actions run on:

[main] DEBUG logger - in runBlocking
[main] DEBUG logger - in doSomethingUsefulOne
[main] DEBUG logger - in doSomethingUsefulTwo

Since we use runBlocking from the main thread, it also runs on this one. The async builders do not specify a separate 科因表 或者 Coroutinecontext. and therefore also inherently run on main.
We have two tasks run on the same thread, and they finish after a 1-second delay. That is possible since delay only suspends the coroutine and does not block main. The example is, as correctly described, an example of concurrency, not utilizing parallelism. Let's change the functions to something that really takes its time and see what happens.

并行金属树脂

Instead of just delaying the coroutines, we let the functions doSomethingUseful calculate the 下一个可能的素数 based on a randomly generated BigInteger which happens to be a fairly expensive task (since this calculation is based on a random it will not run in deterministic time):

fun doSomethingUsefulOne(): BigInteger {
    log.debug("in doSomethingUsefulOne")
    return BigInteger(1500, Random()).nextProbablePrime()
}

Note that the suspend keyword is not necessary anymore and would actually be misleading. The function does not make use of other suspending functions and blocks the calling thread for the needed time. Running the code results in the following logs:

22:22:04.716 [main] DEBUG logger - in runBlocking
22:22:04.749 [main] DEBUG logger - in doSomethingUsefulOne
22:22:05.595 [main] DEBUG logger - Prime calculation took 844 ms
22:22:05.602 [main] DEBUG logger - in doSomethingUsefulOne
22:22:08.241 [main] DEBUG logger - Prime calculation took 2638 ms
Completed in 3520 ms

As we can easily see, the tasks still run concurrently as in with async coroutines but don't execute at the same time anymore. The overall runtime is the sum of both sub-calculations (roughly). After changing the suspending code to blocking code, the result changes and we don't win any time while execution anymore.


关于示例的说明

Let me note that I find the example provided in the documentation slightly misleading as it concludes with "This is twice as fast, because we have concurrent execution of two coroutines" after applying async coroutine builders to the previously sequentially executed code. It only is "twice as fast" since the concurrently executed coroutines just delay in a non-blocking way. The example gives the impression that we get "并行" for free although it's only meant to demonstrate asynchronous programming as I see it.


现在我们如何使Coroutines并行运行?要从上面修复我们的Prime示例,我们需要在某些工作线程上派遣这些任务以不再阻止主线程。我们有一些可能做这项工作的可能性。

使Coroutines并行运行

1.运行 GlobalScope

We can spawn a coroutine in the GlobalScope. That means that the coroutine is not bound to any Job and only limited by the lifetime of the whole application. That is the behavior we know from spawning new threads. It's hard to keep track of global coroutines, and the whole approach seems naive and error-prone. Nonetheless, running in this global scope dispatches a coroutine onto Dispatchers.Default,由Kotlinx.Coroutines库管理的共享线程池。默认情况下,此调度程序使用的最大线程数等于可用CPU内核的数量,但至少为2。

Applying this approach to our example is simple. Instead of running async in the scope of runBlocking, i.e., on the main thread, we spawn them in GlobalScope:

val time = measureTimeMillis {
    val one = GlobalScope.async { doSomethingUsefulOne() }
    val two = GlobalScope.async { doSomethingUsefulTwo() }
}

这 output verifies that we now run in roughly max(time(calc1), time(calc2)):

22:42:19.375 [main] DEBUG logger - in runBlocking
22:42:19.393 [DefaultDispatcher-worker-1] DEBUG logger - in doSomethingUsefulOne
22:42:19.408 [DefaultDispatcher-worker-4] DEBUG logger - in doSomethingUsefulOne
22:42:22.640 [DefaultDispatcher-worker-1] DEBUG logger - Prime calculation took 3245 ms
22:42:23.330 [DefaultDispatcher-worker-4] DEBUG logger - Prime calculation took 3922 ms
Completed in 3950 ms

我们成功应用了并发示例。虽然我所说,这个修复是天真的,可以进一步提高。

2.指定Coroutine Dispatcher

Instead of spawning async in the GlobalScope, we can still let them run in the scope of, i.e., as a child of, runBlocking. To get the same result, we explicitly set a coroutine dispatcher now:

val time = measureTimeMillis {
    val one = async(Dispatchers.Default) { doSomethingUsefulOne() }
    val two = async(Dispatchers.Default) { doSomethingUsefulTwo() }
    println("这 answer is ${one.await() + two.await()}")
}

此调整导致与我们想要的儿童父结构相同的结果相同。我们仍然可以做得更好。是否最理想再次暂停函数?在执行阻塞功能时,而不是照顾未阻止主线程,而是最好仅调用不会阻止呼叫者的暂停功能。

3.使暂停功能暂停

我们可以用 withContext 其中“立即将Dispatcher从新上下文中应用,将块的执行移入块内的不同线程,并在完成时返回”:

suspend fun doSomethingUsefulOne(): BigInteger = withContext(Dispatchers.Default) {
    executeAndMeasureTimeMillis {
        log.debug("in doSomethingUsefulOne")
        BigInteger(1500, Random()).nextProbablePrime()
    }
}.also {
    log.debug("Prime calculation took ${it.second} ms")
}.first

通过这种方法,我们将派遣任务的执行限制在暂停功能内的素数计算中。输出很好地展示了在不同的线程上只发生实际的素数,而其他一切都会保持在主题。什么时候有多线程很容易?我真的很喜欢这个解决方案。

(The function executeAndMeasureTimeMillis is a custom one that measures execution time and returns a pair of result and execution time)

23:00:20.591 [main] DEBUG logger - in runBlocking
23:00:20.648 [DefaultDispatcher-worker-1] DEBUG logger - in doSomethingUsefulOne
23:00:20.714 [DefaultDispatcher-worker-2] DEBUG logger - in doSomethingUsefulOne
23:00:21.132 [main] DEBUG logger - Prime calculation took 413 ms
23:00:23.971 [main] DEBUG logger - Prime calculation took 3322 ms
Completed in 3371 ms

警告:我们互换使用并发和并行性,尽管我们不应该

如本文的介绍部分中已提及,我们经常使用术语并行和并发作为彼此的同义词。我想告诉你,即使是Kotlin文档也没有在这两种术语之间清楚地区分。关于“共享变形状态和并发性”的部分(截至11/5/2018,可能会在将来更改)引入:

可以执行金属版 同时 用一个 多线程调度员 like the Dispatchers.Default. It presents all the usual concurrency problems. The main problem being synchronization of access to shared mutable state. Some solutions to this problem in the land of coroutines are similar to the solutions in the multi-threaded world, but others are unique.

这句话应该真正读取“可以在线执行 平行线 using 多线程调度员s like Dispatchers.Default..."

结论

了解并发性和并行性之间的差异很重要。我们了解到,并发性主要是关于一次处理许多事情,而平行症是关于一次执行许多事情。 Coroutines提供复杂的工具来实现并发性,但不要免费提供我们并行性。在某些情况下,有必要将阻塞代码分发给某些工人线程以让主程序流继续。请记住,我们主要需要CPU密集型和性能关键任务的并行性。在大多数情况下,不担心并行性并对我们从科诉讼中获得的梦幻般的并发感到高兴,这可能只是很好。

最后,让我说谢谢你在我写文章之前与我讨论了这些主题的罗马伊利序罗马。🙏🏼

14 thoughts on “并发金属版–并发不是并行性

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *