blob: b085b938da54735e30ef529d6743c5b46b548f5e [file] [log] [blame] [view]
hadihariri7db55532018-09-15 10:35:08 +02001<!--- INCLUDE .*/example-([a-z]+)-([0-9a-z]+)\.kt
2/*
3 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
4 */
5
6// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
Roman Elizarov0950dfa2018-07-13 10:33:25 +03007package kotlinx.coroutines.guide.$$1$$2
hadihariri7db55532018-09-15 10:35:08 +02008-->
Vsevolod Tolstopyatove50a0fa2019-01-28 11:34:24 +03009<!--- KNIT ../kotlinx-coroutines-core/jvm/test/guide/.*\.kt -->
10<!--- TEST_OUT ../kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt
hadihariri7db55532018-09-15 10:35:08 +020011// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
Roman Elizarov0950dfa2018-07-13 10:33:25 +030012package kotlinx.coroutines.guide.test
hadihariri7db55532018-09-15 10:35:08 +020013
14import org.junit.Test
15
16class SelectGuideTest {
17-->
18
19
Prendotab8a559d2018-11-30 16:24:23 +030020**Table of contents**
hadihariri7db55532018-09-15 10:35:08 +020021
22<!--- TOC -->
23
Vsevolod Tolstopyatovb590aa32018-09-27 18:34:05 +030024* [Select expression (experimental)](#select-expression-experimental)
hadihariri7db55532018-09-15 10:35:08 +020025 * [Selecting from channels](#selecting-from-channels)
26 * [Selecting on close](#selecting-on-close)
27 * [Selecting to send](#selecting-to-send)
28 * [Selecting deferred values](#selecting-deferred-values)
29 * [Switch over a channel of deferred values](#switch-over-a-channel-of-deferred-values)
30
31<!--- END_TOC -->
32
33
34
35## Select expression (experimental)
36
37Select expression makes it possible to await multiple suspending functions simultaneously and _select_
38the first one that becomes available.
39
40> Select expressions are an experimental feature of `kotlinx.coroutines`. Their API is expected to
41evolve in the upcoming updates of the `kotlinx.coroutines` library with potentially
42breaking changes.
43
44### Selecting from channels
45
46Let us have two producers of strings: `fizz` and `buzz`. The `fizz` produces "Fizz" string every 300 ms:
47
Alexander Prendotacbeef102018-09-27 18:42:04 +030048<div class="sample" markdown="1" theme="idea" data-highlight-only>
49
hadihariri7db55532018-09-15 10:35:08 +020050```kotlin
51fun CoroutineScope.fizz() = produce<String> {
52 while (true) { // sends "Fizz" every 300 ms
53 delay(300)
54 send("Fizz")
55 }
56}
57```
58
Alexander Prendotacbeef102018-09-27 18:42:04 +030059</div>
60
hadihariri7db55532018-09-15 10:35:08 +020061And the `buzz` produces "Buzz!" string every 500 ms:
62
Alexander Prendotacbeef102018-09-27 18:42:04 +030063<div class="sample" markdown="1" theme="idea" data-highlight-only>
64
hadihariri7db55532018-09-15 10:35:08 +020065```kotlin
66fun CoroutineScope.buzz() = produce<String> {
67 while (true) { // sends "Buzz!" every 500 ms
68 delay(500)
69 send("Buzz!")
70 }
71}
72```
73
Alexander Prendotacbeef102018-09-27 18:42:04 +030074</div>
75
hadihariri7db55532018-09-15 10:35:08 +020076Using [receive][ReceiveChannel.receive] suspending function we can receive _either_ from one channel or the
77other. But [select] expression allows us to receive from _both_ simultaneously using its
78[onReceive][ReceiveChannel.onReceive] clauses:
79
Alexander Prendotacbeef102018-09-27 18:42:04 +030080<div class="sample" markdown="1" theme="idea" data-highlight-only>
81
hadihariri7db55532018-09-15 10:35:08 +020082```kotlin
83suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
84 select<Unit> { // <Unit> means that this select expression does not produce any result
85 fizz.onReceive { value -> // this is the first select clause
86 println("fizz -> '$value'")
87 }
88 buzz.onReceive { value -> // this is the second select clause
89 println("buzz -> '$value'")
90 }
91 }
92}
93```
94
Alexander Prendotacbeef102018-09-27 18:42:04 +030095</div>
96
hadihariri7db55532018-09-15 10:35:08 +020097Let us run it all seven times:
98
Prendota0eee3c32018-10-22 12:52:56 +030099<!--- CLEAR -->
100
101<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
Alexander Prendotacbeef102018-09-27 18:42:04 +0300102
hadihariri7db55532018-09-15 10:35:08 +0200103```kotlin
Prendota0eee3c32018-10-22 12:52:56 +0300104import kotlinx.coroutines.*
105import kotlinx.coroutines.channels.*
106import kotlinx.coroutines.selects.*
107
108fun CoroutineScope.fizz() = produce<String> {
109 while (true) { // sends "Fizz" every 300 ms
110 delay(300)
111 send("Fizz")
112 }
113}
114
115fun CoroutineScope.buzz() = produce<String> {
116 while (true) { // sends "Buzz!" every 500 ms
117 delay(500)
118 send("Buzz!")
119 }
120}
121
122suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
123 select<Unit> { // <Unit> means that this select expression does not produce any result
124 fizz.onReceive { value -> // this is the first select clause
125 println("fizz -> '$value'")
126 }
127 buzz.onReceive { value -> // this is the second select clause
128 println("buzz -> '$value'")
129 }
130 }
131}
132
Prendota65e6c8c2018-10-17 11:51:08 +0300133fun main() = runBlocking<Unit> {
Prendota0eee3c32018-10-22 12:52:56 +0300134//sampleStart
hadihariri7db55532018-09-15 10:35:08 +0200135 val fizz = fizz()
136 val buzz = buzz()
137 repeat(7) {
138 selectFizzBuzz(fizz, buzz)
139 }
Prendota0eee3c32018-10-22 12:52:56 +0300140 coroutineContext.cancelChildren() // cancel fizz & buzz coroutines
141//sampleEnd
hadihariri7db55532018-09-15 10:35:08 +0200142}
143```
144
Alexander Prendotacbeef102018-09-27 18:42:04 +0300145</div>
146
Inego69c26df2019-04-21 14:51:25 +0700147> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-01.kt).
hadihariri7db55532018-09-15 10:35:08 +0200148
149The result of this code is:
150
151```text
152fizz -> 'Fizz'
153buzz -> 'Buzz!'
154fizz -> 'Fizz'
155fizz -> 'Fizz'
156buzz -> 'Buzz!'
157fizz -> 'Fizz'
158buzz -> 'Buzz!'
159```
160
161<!--- TEST -->
162
163### Selecting on close
164
165The [onReceive][ReceiveChannel.onReceive] clause in `select` fails when the channel is closed causing the corresponding
166`select` to throw an exception. We can use [onReceiveOrNull][ReceiveChannel.onReceiveOrNull] clause to perform a
167specific action when the channel is closed. The following example also shows that `select` is an expression that returns
168the result of its selected clause:
169
Alexander Prendotacbeef102018-09-27 18:42:04 +0300170<div class="sample" markdown="1" theme="idea" data-highlight-only>
171
hadihariri7db55532018-09-15 10:35:08 +0200172```kotlin
173suspend fun selectAorB(a: ReceiveChannel<String>, b: ReceiveChannel<String>): String =
174 select<String> {
175 a.onReceiveOrNull { value ->
176 if (value == null)
177 "Channel 'a' is closed"
178 else
179 "a -> '$value'"
180 }
181 b.onReceiveOrNull { value ->
182 if (value == null)
183 "Channel 'b' is closed"
184 else
185 "b -> '$value'"
186 }
187 }
188```
189
Alexander Prendotacbeef102018-09-27 18:42:04 +0300190</div>
191
hadihariri7db55532018-09-15 10:35:08 +0200192Let's use it with channel `a` that produces "Hello" string four times and
193channel `b` that produces "World" four times:
194
Prendota0eee3c32018-10-22 12:52:56 +0300195<!--- CLEAR -->
196
197<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
Alexander Prendotacbeef102018-09-27 18:42:04 +0300198
hadihariri7db55532018-09-15 10:35:08 +0200199```kotlin
Prendota0eee3c32018-10-22 12:52:56 +0300200import kotlinx.coroutines.*
201import kotlinx.coroutines.channels.*
202import kotlinx.coroutines.selects.*
203
204suspend fun selectAorB(a: ReceiveChannel<String>, b: ReceiveChannel<String>): String =
205 select<String> {
206 a.onReceiveOrNull { value ->
207 if (value == null)
208 "Channel 'a' is closed"
209 else
210 "a -> '$value'"
211 }
212 b.onReceiveOrNull { value ->
213 if (value == null)
214 "Channel 'b' is closed"
215 else
216 "b -> '$value'"
217 }
218 }
219
Prendota65e6c8c2018-10-17 11:51:08 +0300220fun main() = runBlocking<Unit> {
Prendota0eee3c32018-10-22 12:52:56 +0300221//sampleStart
hadihariri7db55532018-09-15 10:35:08 +0200222 val a = produce<String> {
223 repeat(4) { send("Hello $it") }
224 }
225 val b = produce<String> {
226 repeat(4) { send("World $it") }
227 }
228 repeat(8) { // print first eight results
229 println(selectAorB(a, b))
230 }
Prendota0eee3c32018-10-22 12:52:56 +0300231 coroutineContext.cancelChildren()
232//sampleEnd
233}
hadihariri7db55532018-09-15 10:35:08 +0200234```
235
Alexander Prendotacbeef102018-09-27 18:42:04 +0300236</div>
237
Inego69c26df2019-04-21 14:51:25 +0700238> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-02.kt).
hadihariri7db55532018-09-15 10:35:08 +0200239
240The result of this code is quite interesting, so we'll analyze it in mode detail:
241
242```text
243a -> 'Hello 0'
244a -> 'Hello 1'
245b -> 'World 0'
246a -> 'Hello 2'
247a -> 'Hello 3'
248b -> 'World 1'
249Channel 'a' is closed
250Channel 'a' is closed
251```
252
253<!--- TEST -->
254
255There are couple of observations to make out of it.
256
257First of all, `select` is _biased_ to the first clause. When several clauses are selectable at the same time,
258the first one among them gets selected. Here, both channels are constantly producing strings, so `a` channel,
259being the first clause in select, wins. However, because we are using unbuffered channel, the `a` gets suspended from
260time to time on its [send][SendChannel.send] invocation and gives a chance for `b` to send, too.
261
262The second observation, is that [onReceiveOrNull][ReceiveChannel.onReceiveOrNull] gets immediately selected when the
263channel is already closed.
264
265### Selecting to send
266
267Select expression has [onSend][SendChannel.onSend] clause that can be used for a great good in combination
268with a biased nature of selection.
269
270Let us write an example of producer of integers that sends its values to a `side` channel when
271the consumers on its primary channel cannot keep up with it:
272
Alexander Prendotacbeef102018-09-27 18:42:04 +0300273<div class="sample" markdown="1" theme="idea" data-highlight-only>
274
hadihariri7db55532018-09-15 10:35:08 +0200275```kotlin
276fun CoroutineScope.produceNumbers(side: SendChannel<Int>) = produce<Int> {
277 for (num in 1..10) { // produce 10 numbers from 1 to 10
278 delay(100) // every 100 ms
279 select<Unit> {
280 onSend(num) {} // Send to the primary channel
281 side.onSend(num) {} // or to the side channel
282 }
283 }
284}
285```
286
Alexander Prendotacbeef102018-09-27 18:42:04 +0300287</div>
288
hadihariri7db55532018-09-15 10:35:08 +0200289Consumer is going to be quite slow, taking 250 ms to process each number:
290
Prendota0eee3c32018-10-22 12:52:56 +0300291<!--- CLEAR -->
292
293<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
Alexander Prendotacbeef102018-09-27 18:42:04 +0300294
hadihariri7db55532018-09-15 10:35:08 +0200295```kotlin
Prendota0eee3c32018-10-22 12:52:56 +0300296import kotlinx.coroutines.*
297import kotlinx.coroutines.channels.*
298import kotlinx.coroutines.selects.*
299
300fun CoroutineScope.produceNumbers(side: SendChannel<Int>) = produce<Int> {
301 for (num in 1..10) { // produce 10 numbers from 1 to 10
302 delay(100) // every 100 ms
303 select<Unit> {
304 onSend(num) {} // Send to the primary channel
305 side.onSend(num) {} // or to the side channel
306 }
307 }
308}
309
Prendota65e6c8c2018-10-17 11:51:08 +0300310fun main() = runBlocking<Unit> {
Prendota0eee3c32018-10-22 12:52:56 +0300311//sampleStart
hadihariri7db55532018-09-15 10:35:08 +0200312 val side = Channel<Int>() // allocate side channel
313 launch { // this is a very fast consumer for the side channel
314 side.consumeEach { println("Side channel has $it") }
315 }
316 produceNumbers(side).consumeEach {
317 println("Consuming $it")
318 delay(250) // let us digest the consumed number properly, do not hurry
319 }
320 println("Done consuming")
Prendota0eee3c32018-10-22 12:52:56 +0300321 coroutineContext.cancelChildren()
322//sampleEnd
hadihariri7db55532018-09-15 10:35:08 +0200323}
Alexander Prendotacbeef102018-09-27 18:42:04 +0300324```
325
326</div>
hadihariri7db55532018-09-15 10:35:08 +0200327
Inego69c26df2019-04-21 14:51:25 +0700328> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-03.kt).
hadihariri7db55532018-09-15 10:35:08 +0200329
330So let us see what happens:
331
332```text
333Consuming 1
334Side channel has 2
335Side channel has 3
336Consuming 4
337Side channel has 5
338Side channel has 6
339Consuming 7
340Side channel has 8
341Side channel has 9
342Consuming 10
343Done consuming
344```
345
346<!--- TEST -->
347
348### Selecting deferred values
349
350Deferred values can be selected using [onAwait][Deferred.onAwait] clause.
351Let us start with an async function that returns a deferred string value after
352a random delay:
353
Alexander Prendotacbeef102018-09-27 18:42:04 +0300354<div class="sample" markdown="1" theme="idea" data-highlight-only>
355
hadihariri7db55532018-09-15 10:35:08 +0200356```kotlin
357fun CoroutineScope.asyncString(time: Int) = async {
358 delay(time.toLong())
359 "Waited for $time ms"
360}
361```
362
Alexander Prendotacbeef102018-09-27 18:42:04 +0300363</div>
364
hadihariri7db55532018-09-15 10:35:08 +0200365Let us start a dozen of them with a random delay.
366
Alexander Prendotacbeef102018-09-27 18:42:04 +0300367<div class="sample" markdown="1" theme="idea" data-highlight-only>
368
hadihariri7db55532018-09-15 10:35:08 +0200369```kotlin
370fun CoroutineScope.asyncStringsList(): List<Deferred<String>> {
371 val random = Random(3)
372 return List(12) { asyncString(random.nextInt(1000)) }
373}
374```
375
Alexander Prendotacbeef102018-09-27 18:42:04 +0300376</div>
377
hadihariri7db55532018-09-15 10:35:08 +0200378Now the main function awaits for the first of them to complete and counts the number of deferred values
Inegoebe519a2019-04-21 13:22:27 +0700379that are still active. Note that we've used here the fact that `select` expression is a Kotlin DSL,
hadihariri7db55532018-09-15 10:35:08 +0200380so we can provide clauses for it using an arbitrary code. In this case we iterate over a list
381of deferred values to provide `onAwait` clause for each deferred value.
382
Prendota0eee3c32018-10-22 12:52:56 +0300383<!--- CLEAR -->
384
385<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
Alexander Prendotacbeef102018-09-27 18:42:04 +0300386
hadihariri7db55532018-09-15 10:35:08 +0200387```kotlin
Prendota0eee3c32018-10-22 12:52:56 +0300388import kotlinx.coroutines.*
389import kotlinx.coroutines.selects.*
390import java.util.*
391
392fun CoroutineScope.asyncString(time: Int) = async {
393 delay(time.toLong())
394 "Waited for $time ms"
395}
396
397fun CoroutineScope.asyncStringsList(): List<Deferred<String>> {
398 val random = Random(3)
399 return List(12) { asyncString(random.nextInt(1000)) }
400}
401
Prendota65e6c8c2018-10-17 11:51:08 +0300402fun main() = runBlocking<Unit> {
Prendota0eee3c32018-10-22 12:52:56 +0300403//sampleStart
hadihariri7db55532018-09-15 10:35:08 +0200404 val list = asyncStringsList()
405 val result = select<String> {
406 list.withIndex().forEach { (index, deferred) ->
407 deferred.onAwait { answer ->
408 "Deferred $index produced answer '$answer'"
409 }
410 }
411 }
412 println(result)
413 val countActive = list.count { it.isActive }
414 println("$countActive coroutines are still active")
Prendota0eee3c32018-10-22 12:52:56 +0300415//sampleEnd
hadihariri7db55532018-09-15 10:35:08 +0200416}
417```
418
Alexander Prendotacbeef102018-09-27 18:42:04 +0300419</div>
420
Inego69c26df2019-04-21 14:51:25 +0700421> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-04.kt).
hadihariri7db55532018-09-15 10:35:08 +0200422
423The output is:
424
425```text
426Deferred 4 produced answer 'Waited for 128 ms'
42711 coroutines are still active
428```
429
430<!--- TEST -->
431
432### Switch over a channel of deferred values
433
434Let us write a channel producer function that consumes a channel of deferred string values, waits for each received
435deferred value, but only until the next deferred value comes over or the channel is closed. This example puts together
436[onReceiveOrNull][ReceiveChannel.onReceiveOrNull] and [onAwait][Deferred.onAwait] clauses in the same `select`:
437
Alexander Prendotacbeef102018-09-27 18:42:04 +0300438<div class="sample" markdown="1" theme="idea" data-highlight-only>
439
hadihariri7db55532018-09-15 10:35:08 +0200440```kotlin
441fun CoroutineScope.switchMapDeferreds(input: ReceiveChannel<Deferred<String>>) = produce<String> {
442 var current = input.receive() // start with first received deferred value
443 while (isActive) { // loop while not cancelled/closed
444 val next = select<Deferred<String>?> { // return next deferred value from this select or null
445 input.onReceiveOrNull { update ->
446 update // replaces next value to wait
447 }
448 current.onAwait { value ->
449 send(value) // send value that current deferred has produced
450 input.receiveOrNull() // and use the next deferred from the input channel
451 }
452 }
453 if (next == null) {
454 println("Channel was closed")
455 break // out of loop
456 } else {
457 current = next
458 }
459 }
460}
461```
462
Alexander Prendotacbeef102018-09-27 18:42:04 +0300463</div>
464
hadihariri7db55532018-09-15 10:35:08 +0200465To test it, we'll use a simple async function that resolves to a specified string after a specified time:
466
Alexander Prendotacbeef102018-09-27 18:42:04 +0300467
468<div class="sample" markdown="1" theme="idea" data-highlight-only>
469
hadihariri7db55532018-09-15 10:35:08 +0200470```kotlin
471fun CoroutineScope.asyncString(str: String, time: Long) = async {
472 delay(time)
473 str
474}
475```
476
Alexander Prendotacbeef102018-09-27 18:42:04 +0300477</div>
478
hadihariri7db55532018-09-15 10:35:08 +0200479The main function just launches a coroutine to print results of `switchMapDeferreds` and sends some test
480data to it:
481
Prendota0eee3c32018-10-22 12:52:56 +0300482<!--- CLEAR -->
483
484<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
Alexander Prendotacbeef102018-09-27 18:42:04 +0300485
hadihariri7db55532018-09-15 10:35:08 +0200486```kotlin
Prendota0eee3c32018-10-22 12:52:56 +0300487import kotlinx.coroutines.*
488import kotlinx.coroutines.channels.*
489import kotlinx.coroutines.selects.*
490
491fun CoroutineScope.switchMapDeferreds(input: ReceiveChannel<Deferred<String>>) = produce<String> {
492 var current = input.receive() // start with first received deferred value
493 while (isActive) { // loop while not cancelled/closed
494 val next = select<Deferred<String>?> { // return next deferred value from this select or null
495 input.onReceiveOrNull { update ->
496 update // replaces next value to wait
497 }
498 current.onAwait { value ->
499 send(value) // send value that current deferred has produced
500 input.receiveOrNull() // and use the next deferred from the input channel
501 }
502 }
503 if (next == null) {
504 println("Channel was closed")
505 break // out of loop
506 } else {
507 current = next
508 }
509 }
510}
511
512fun CoroutineScope.asyncString(str: String, time: Long) = async {
513 delay(time)
514 str
515}
516
Prendota65e6c8c2018-10-17 11:51:08 +0300517fun main() = runBlocking<Unit> {
Prendota0eee3c32018-10-22 12:52:56 +0300518//sampleStart
hadihariri7db55532018-09-15 10:35:08 +0200519 val chan = Channel<Deferred<String>>() // the channel for test
520 launch { // launch printing coroutine
521 for (s in switchMapDeferreds(chan))
522 println(s) // print each received string
523 }
524 chan.send(asyncString("BEGIN", 100))
525 delay(200) // enough time for "BEGIN" to be produced
526 chan.send(asyncString("Slow", 500))
527 delay(100) // not enough time to produce slow
528 chan.send(asyncString("Replace", 100))
529 delay(500) // give it time before the last one
530 chan.send(asyncString("END", 500))
531 delay(1000) // give it time to process
532 chan.close() // close the channel ...
533 delay(500) // and wait some time to let it finish
Prendota0eee3c32018-10-22 12:52:56 +0300534//sampleEnd
hadihariri7db55532018-09-15 10:35:08 +0200535}
536```
537
Alexander Prendotacbeef102018-09-27 18:42:04 +0300538</div>
539
Inego69c26df2019-04-21 14:51:25 +0700540> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-05.kt).
hadihariri7db55532018-09-15 10:35:08 +0200541
542The result of this code:
543
544```text
545BEGIN
546Replace
547END
548Channel was closed
549```
550
551<!--- TEST -->
552
553<!--- MODULE kotlinx-coroutines-core -->
Roman Elizarov0950dfa2018-07-13 10:33:25 +0300554<!--- INDEX kotlinx.coroutines -->
555[Deferred.onAwait]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html
556<!--- INDEX kotlinx.coroutines.channels -->
557[ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
558[ReceiveChannel.onReceive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive.html
559[ReceiveChannel.onReceiveOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive-or-null.html
560[SendChannel.send]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html
561[SendChannel.onSend]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/on-send.html
562<!--- INDEX kotlinx.coroutines.selects -->
563[select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
hadihariri7db55532018-09-15 10:35:08 +0200564<!--- END -->