Roman Elizarov | 660c2d7 | 2020-02-14 13:18:37 +0300 | [diff] [blame] | 1 | <!--- TEST_NAME ChannelsGuideTest --> |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 2 | |
Prendota | b8a559d | 2018-11-30 16:24:23 +0300 | [diff] [blame] | 3 | **Table of contents** |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 4 | |
| 5 | <!--- TOC --> |
| 6 | |
Denys M | a97fd43 | 2019-06-05 03:10:34 +0300 | [diff] [blame] | 7 | * [Channels](#channels) |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 8 | * [Channel basics](#channel-basics) |
| 9 | * [Closing and iteration over channels](#closing-and-iteration-over-channels) |
| 10 | * [Building channel producers](#building-channel-producers) |
| 11 | * [Pipelines](#pipelines) |
| 12 | * [Prime numbers with pipeline](#prime-numbers-with-pipeline) |
| 13 | * [Fan-out](#fan-out) |
| 14 | * [Fan-in](#fan-in) |
| 15 | * [Buffered channels](#buffered-channels) |
| 16 | * [Channels are fair](#channels-are-fair) |
| 17 | * [Ticker channels](#ticker-channels) |
| 18 | |
Roman Elizarov | 660c2d7 | 2020-02-14 13:18:37 +0300 | [diff] [blame] | 19 | <!--- END --> |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 20 | |
Denys M | a97fd43 | 2019-06-05 03:10:34 +0300 | [diff] [blame] | 21 | ## Channels |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 22 | |
| 23 | Deferred values provide a convenient way to transfer a single value between coroutines. |
| 24 | Channels provide a way to transfer a stream of values. |
| 25 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 26 | ### Channel basics |
| 27 | |
| 28 | A [Channel] is conceptually very similar to `BlockingQueue`. One key difference is that |
| 29 | instead of a blocking `put` operation it has a suspending [send][SendChannel.send], and instead of |
| 30 | a blocking `take` operation it has a suspending [receive][ReceiveChannel.receive]. |
| 31 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 32 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 33 | <div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3"> |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 34 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 35 | ```kotlin |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 36 | import kotlinx.coroutines.* |
| 37 | import kotlinx.coroutines.channels.* |
| 38 | |
| 39 | fun main() = runBlocking { |
| 40 | //sampleStart |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 41 | val channel = Channel<Int>() |
| 42 | launch { |
| 43 | // this might be heavy CPU-consuming computation or async logic, we'll just send five squares |
| 44 | for (x in 1..5) channel.send(x * x) |
| 45 | } |
| 46 | // here we print five received integers: |
| 47 | repeat(5) { println(channel.receive()) } |
| 48 | println("Done!") |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 49 | //sampleEnd |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 50 | } |
| 51 | ``` |
| 52 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 53 | </div> |
| 54 | |
Inego | 69c26df | 2019-04-21 14:51:25 +0700 | [diff] [blame] | 55 | > You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-channel-01.kt). |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 56 | |
| 57 | The output of this code is: |
| 58 | |
| 59 | ```text |
| 60 | 1 |
| 61 | 4 |
| 62 | 9 |
| 63 | 16 |
| 64 | 25 |
| 65 | Done! |
| 66 | ``` |
| 67 | |
| 68 | <!--- TEST --> |
| 69 | |
| 70 | ### Closing and iteration over channels |
| 71 | |
| 72 | Unlike a queue, a channel can be closed to indicate that no more elements are coming. |
| 73 | On the receiver side it is convenient to use a regular `for` loop to receive elements |
| 74 | from the channel. |
| 75 | |
| 76 | Conceptually, a [close][SendChannel.close] is like sending a special close token to the channel. |
| 77 | The iteration stops as soon as this close token is received, so there is a guarantee |
| 78 | that all previously sent elements before the close are received: |
| 79 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 80 | <div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3"> |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 81 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 82 | ```kotlin |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 83 | import kotlinx.coroutines.* |
| 84 | import kotlinx.coroutines.channels.* |
| 85 | |
| 86 | fun main() = runBlocking { |
| 87 | //sampleStart |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 88 | val channel = Channel<Int>() |
| 89 | launch { |
| 90 | for (x in 1..5) channel.send(x * x) |
| 91 | channel.close() // we're done sending |
| 92 | } |
| 93 | // here we print received values using `for` loop (until the channel is closed) |
| 94 | for (y in channel) println(y) |
| 95 | println("Done!") |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 96 | //sampleEnd |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 97 | } |
| 98 | ``` |
| 99 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 100 | </div> |
| 101 | |
Inego | 69c26df | 2019-04-21 14:51:25 +0700 | [diff] [blame] | 102 | > You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-channel-02.kt). |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 103 | |
| 104 | <!--- TEST |
| 105 | 1 |
| 106 | 4 |
| 107 | 9 |
| 108 | 16 |
| 109 | 25 |
| 110 | Done! |
| 111 | --> |
| 112 | |
| 113 | ### Building channel producers |
| 114 | |
| 115 | The pattern where a coroutine is producing a sequence of elements is quite common. |
| 116 | This is a part of _producer-consumer_ pattern that is often found in concurrent code. |
| 117 | You could abstract such a producer into a function that takes channel as its parameter, but this goes contrary |
| 118 | to common sense that results must be returned from functions. |
| 119 | |
arman simonyan | 02b3302 | 2018-10-10 22:06:04 +0400 | [diff] [blame] | 120 | There is a convenient coroutine builder named [produce] that makes it easy to do it right on producer side, |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 121 | and an extension function [consumeEach], that replaces a `for` loop on the consumer side: |
| 122 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 123 | <div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3"> |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 124 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 125 | ```kotlin |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 126 | import kotlinx.coroutines.* |
| 127 | import kotlinx.coroutines.channels.* |
| 128 | |
Vsevolod Tolstopyatov | a2d8088 | 2018-09-24 19:51:49 +0300 | [diff] [blame] | 129 | fun CoroutineScope.produceSquares(): ReceiveChannel<Int> = produce { |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 130 | for (x in 1..5) send(x * x) |
| 131 | } |
| 132 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 133 | fun main() = runBlocking { |
| 134 | //sampleStart |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 135 | val squares = produceSquares() |
| 136 | squares.consumeEach { println(it) } |
| 137 | println("Done!") |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 138 | //sampleEnd |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 139 | } |
| 140 | ``` |
| 141 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 142 | </div> |
| 143 | |
Inego | 69c26df | 2019-04-21 14:51:25 +0700 | [diff] [blame] | 144 | > You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-channel-03.kt). |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 145 | |
| 146 | <!--- TEST |
| 147 | 1 |
| 148 | 4 |
| 149 | 9 |
| 150 | 16 |
| 151 | 25 |
| 152 | Done! |
| 153 | --> |
| 154 | |
| 155 | ### Pipelines |
| 156 | |
| 157 | A pipeline is a pattern where one coroutine is producing, possibly infinite, stream of values: |
| 158 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 159 | <div class="sample" markdown="1" theme="idea" data-highlight-only> |
| 160 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 161 | ```kotlin |
| 162 | fun CoroutineScope.produceNumbers() = produce<Int> { |
| 163 | var x = 1 |
| 164 | while (true) send(x++) // infinite stream of integers starting from 1 |
| 165 | } |
| 166 | ``` |
| 167 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 168 | </div> |
| 169 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 170 | And another coroutine or coroutines are consuming that stream, doing some processing, and producing some other results. |
armansimonyan13 | 52f10c7 | 2018-10-11 02:52:58 +0400 | [diff] [blame] | 171 | In the example below, the numbers are just squared: |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 172 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 173 | <div class="sample" markdown="1" theme="idea" data-highlight-only> |
| 174 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 175 | ```kotlin |
Vsevolod Tolstopyatov | a2d8088 | 2018-09-24 19:51:49 +0300 | [diff] [blame] | 176 | fun CoroutineScope.square(numbers: ReceiveChannel<Int>): ReceiveChannel<Int> = produce { |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 177 | for (x in numbers) send(x * x) |
| 178 | } |
| 179 | ``` |
| 180 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 181 | </div> |
| 182 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 183 | The main code starts and connects the whole pipeline: |
| 184 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 185 | <!--- CLEAR --> |
| 186 | |
| 187 | <div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3"> |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 188 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 189 | ```kotlin |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 190 | import kotlinx.coroutines.* |
| 191 | import kotlinx.coroutines.channels.* |
| 192 | |
| 193 | fun main() = runBlocking { |
| 194 | //sampleStart |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 195 | val numbers = produceNumbers() // produces integers from 1 and on |
| 196 | val squares = square(numbers) // squares integers |
Tatsuya Fujisaki | 9be85a4 | 2020-01-12 10:39:33 +0900 | [diff] [blame] | 197 | repeat(5) { |
| 198 | println(squares.receive()) // print first five |
| 199 | } |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 200 | println("Done!") // we are done |
| 201 | coroutineContext.cancelChildren() // cancel children coroutines |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 202 | //sampleEnd |
| 203 | } |
| 204 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 205 | fun CoroutineScope.produceNumbers() = produce<Int> { |
| 206 | var x = 1 |
| 207 | while (true) send(x++) // infinite stream of integers starting from 1 |
| 208 | } |
Joffrey Bion | 3feef05 | 2018-12-12 23:57:53 +0100 | [diff] [blame] | 209 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 210 | fun CoroutineScope.square(numbers: ReceiveChannel<Int>): ReceiveChannel<Int> = produce { |
| 211 | for (x in numbers) send(x * x) |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 212 | } |
| 213 | ``` |
| 214 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 215 | </div> |
| 216 | |
Inego | 69c26df | 2019-04-21 14:51:25 +0700 | [diff] [blame] | 217 | > You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-channel-04.kt). |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 218 | |
| 219 | <!--- TEST |
| 220 | 1 |
| 221 | 4 |
| 222 | 9 |
| 223 | 16 |
| 224 | 25 |
| 225 | Done! |
| 226 | --> |
| 227 | |
| 228 | > All functions that create coroutines are defined as extensions on [CoroutineScope], |
armansimonyan13 | 52f10c7 | 2018-10-11 02:52:58 +0400 | [diff] [blame] | 229 | so that we can rely on [structured concurrency](https://kotlinlang.org/docs/reference/coroutines/composing-suspending-functions.html#structured-concurrency-with-async) to make |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 230 | sure that we don't have lingering global coroutines in our application. |
| 231 | |
| 232 | ### Prime numbers with pipeline |
| 233 | |
| 234 | Let's take pipelines to the extreme with an example that generates prime numbers using a pipeline |
| 235 | of coroutines. We start with an infinite sequence of numbers. |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 236 | |
| 237 | <div class="sample" markdown="1" theme="idea" data-highlight-only> |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 238 | |
| 239 | ```kotlin |
| 240 | fun CoroutineScope.numbersFrom(start: Int) = produce<Int> { |
| 241 | var x = start |
| 242 | while (true) send(x++) // infinite stream of integers from start |
| 243 | } |
| 244 | ``` |
| 245 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 246 | </div> |
| 247 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 248 | The following pipeline stage filters an incoming stream of numbers, removing all the numbers |
| 249 | that are divisible by the given prime number: |
| 250 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 251 | <div class="sample" markdown="1" theme="idea" data-highlight-only> |
| 252 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 253 | ```kotlin |
| 254 | fun CoroutineScope.filter(numbers: ReceiveChannel<Int>, prime: Int) = produce<Int> { |
| 255 | for (x in numbers) if (x % prime != 0) send(x) |
| 256 | } |
| 257 | ``` |
| 258 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 259 | </div> |
| 260 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 261 | Now we build our pipeline by starting a stream of numbers from 2, taking a prime number from the current channel, |
| 262 | and launching new pipeline stage for each prime number found: |
| 263 | |
| 264 | ``` |
| 265 | numbersFrom(2) -> filter(2) -> filter(3) -> filter(5) -> filter(7) ... |
| 266 | ``` |
| 267 | |
| 268 | The following example prints the first ten prime numbers, |
| 269 | running the whole pipeline in the context of the main thread. Since all the coroutines are launched in |
| 270 | the scope of the main [runBlocking] coroutine |
| 271 | we don't have to keep an explicit list of all the coroutines we have started. |
Roman Elizarov | 0950dfa | 2018-07-13 10:33:25 +0300 | [diff] [blame] | 272 | We use [cancelChildren][kotlin.coroutines.CoroutineContext.cancelChildren] |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 273 | extension function to cancel all the children coroutines after we have printed |
| 274 | the first ten prime numbers. |
| 275 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 276 | <!--- CLEAR --> |
| 277 | |
| 278 | <div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3"> |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 279 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 280 | ```kotlin |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 281 | import kotlinx.coroutines.* |
| 282 | import kotlinx.coroutines.channels.* |
| 283 | |
| 284 | fun main() = runBlocking { |
| 285 | //sampleStart |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 286 | var cur = numbersFrom(2) |
Tatsuya Fujisaki | 9be85a4 | 2020-01-12 10:39:33 +0900 | [diff] [blame] | 287 | repeat(10) { |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 288 | val prime = cur.receive() |
| 289 | println(prime) |
| 290 | cur = filter(cur, prime) |
| 291 | } |
| 292 | coroutineContext.cancelChildren() // cancel all children to let main finish |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 293 | //sampleEnd |
| 294 | } |
| 295 | |
| 296 | fun CoroutineScope.numbersFrom(start: Int) = produce<Int> { |
| 297 | var x = start |
| 298 | while (true) send(x++) // infinite stream of integers from start |
| 299 | } |
| 300 | |
| 301 | fun CoroutineScope.filter(numbers: ReceiveChannel<Int>, prime: Int) = produce<Int> { |
| 302 | for (x in numbers) if (x % prime != 0) send(x) |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 303 | } |
| 304 | ``` |
| 305 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 306 | </div> |
| 307 | |
Inego | 69c26df | 2019-04-21 14:51:25 +0700 | [diff] [blame] | 308 | > You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-channel-05.kt). |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 309 | |
| 310 | The output of this code is: |
| 311 | |
| 312 | ```text |
| 313 | 2 |
| 314 | 3 |
| 315 | 5 |
| 316 | 7 |
| 317 | 11 |
| 318 | 13 |
| 319 | 17 |
| 320 | 19 |
| 321 | 23 |
| 322 | 29 |
| 323 | ``` |
| 324 | |
| 325 | <!--- TEST --> |
| 326 | |
Inego | ebe519a | 2019-04-21 13:22:27 +0700 | [diff] [blame] | 327 | Note that you can build the same pipeline using |
Ahmad El-Melegy | 153e21f | 2019-03-23 23:27:36 +0200 | [diff] [blame] | 328 | [`iterator`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/iterator.html) |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 329 | coroutine builder from the standard library. |
Ahmad El-Melegy | 153e21f | 2019-03-23 23:27:36 +0200 | [diff] [blame] | 330 | Replace `produce` with `iterator`, `send` with `yield`, `receive` with `next`, |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 331 | `ReceiveChannel` with `Iterator`, and get rid of the coroutine scope. You will not need `runBlocking` either. |
| 332 | However, the benefit of a pipeline that uses channels as shown above is that it can actually use |
| 333 | multiple CPU cores if you run it in [Dispatchers.Default] context. |
| 334 | |
| 335 | Anyway, this is an extremely impractical way to find prime numbers. In practice, pipelines do involve some |
| 336 | other suspending invocations (like asynchronous calls to remote services) and these pipelines cannot be |
Ahmad El-Melegy | 153e21f | 2019-03-23 23:27:36 +0200 | [diff] [blame] | 337 | built using `sequence`/`iterator`, because they do not allow arbitrary suspension, unlike |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 338 | `produce`, which is fully asynchronous. |
| 339 | |
| 340 | ### Fan-out |
| 341 | |
| 342 | Multiple coroutines may receive from the same channel, distributing work between themselves. |
| 343 | Let us start with a producer coroutine that is periodically producing integers |
| 344 | (ten numbers per second): |
| 345 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 346 | <div class="sample" markdown="1" theme="idea" data-highlight-only> |
| 347 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 348 | ```kotlin |
| 349 | fun CoroutineScope.produceNumbers() = produce<Int> { |
| 350 | var x = 1 // start from 1 |
| 351 | while (true) { |
| 352 | send(x++) // produce next |
| 353 | delay(100) // wait 0.1s |
| 354 | } |
| 355 | } |
| 356 | ``` |
| 357 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 358 | </div> |
| 359 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 360 | Then we can have several processor coroutines. In this example, they just print their id and |
| 361 | received number: |
| 362 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 363 | <div class="sample" markdown="1" theme="idea" data-highlight-only> |
| 364 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 365 | ```kotlin |
| 366 | fun CoroutineScope.launchProcessor(id: Int, channel: ReceiveChannel<Int>) = launch { |
| 367 | for (msg in channel) { |
| 368 | println("Processor #$id received $msg") |
| 369 | } |
| 370 | } |
| 371 | ``` |
| 372 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 373 | </div> |
| 374 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 375 | Now let us launch five processors and let them work for almost a second. See what happens: |
| 376 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 377 | <!--- CLEAR --> |
| 378 | |
| 379 | <div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3"> |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 380 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 381 | ```kotlin |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 382 | import kotlinx.coroutines.* |
| 383 | import kotlinx.coroutines.channels.* |
| 384 | |
| 385 | fun main() = runBlocking<Unit> { |
| 386 | //sampleStart |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 387 | val producer = produceNumbers() |
| 388 | repeat(5) { launchProcessor(it, producer) } |
| 389 | delay(950) |
| 390 | producer.cancel() // cancel producer coroutine and thus kill them all |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 391 | //sampleEnd |
| 392 | } |
| 393 | |
| 394 | fun CoroutineScope.produceNumbers() = produce<Int> { |
| 395 | var x = 1 // start from 1 |
| 396 | while (true) { |
| 397 | send(x++) // produce next |
| 398 | delay(100) // wait 0.1s |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | fun CoroutineScope.launchProcessor(id: Int, channel: ReceiveChannel<Int>) = launch { |
| 403 | for (msg in channel) { |
| 404 | println("Processor #$id received $msg") |
| 405 | } |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 406 | } |
| 407 | ``` |
| 408 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 409 | </div> |
| 410 | |
Inego | 69c26df | 2019-04-21 14:51:25 +0700 | [diff] [blame] | 411 | > You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-channel-06.kt). |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 412 | |
| 413 | The output will be similar to the the following one, albeit the processor ids that receive |
| 414 | each specific integer may be different: |
| 415 | |
| 416 | ``` |
| 417 | Processor #2 received 1 |
| 418 | Processor #4 received 2 |
| 419 | Processor #0 received 3 |
| 420 | Processor #1 received 4 |
| 421 | Processor #3 received 5 |
| 422 | Processor #2 received 6 |
| 423 | Processor #4 received 7 |
| 424 | Processor #0 received 8 |
| 425 | Processor #1 received 9 |
| 426 | Processor #3 received 10 |
| 427 | ``` |
| 428 | |
| 429 | <!--- TEST lines.size == 10 && lines.withIndex().all { (i, line) -> line.startsWith("Processor #") && line.endsWith(" received ${i + 1}") } --> |
| 430 | |
Inego | ebe519a | 2019-04-21 13:22:27 +0700 | [diff] [blame] | 431 | Note that cancelling a producer coroutine closes its channel, thus eventually terminating iteration |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 432 | over the channel that processor coroutines are doing. |
| 433 | |
| 434 | Also, pay attention to how we explicitly iterate over channel with `for` loop to perform fan-out in `launchProcessor` code. |
| 435 | Unlike `consumeEach`, this `for` loop pattern is perfectly safe to use from multiple coroutines. If one of the processor |
| 436 | coroutines fails, then others would still be processing the channel, while a processor that is written via `consumeEach` |
| 437 | always consumes (cancels) the underlying channel on its normal or abnormal completion. |
| 438 | |
| 439 | ### Fan-in |
| 440 | |
| 441 | Multiple coroutines may send to the same channel. |
| 442 | For example, let us have a channel of strings, and a suspending function that |
| 443 | repeatedly sends a specified string to this channel with a specified delay: |
| 444 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 445 | <div class="sample" markdown="1" theme="idea" data-highlight-only> |
| 446 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 447 | ```kotlin |
| 448 | suspend fun sendString(channel: SendChannel<String>, s: String, time: Long) { |
| 449 | while (true) { |
| 450 | delay(time) |
| 451 | channel.send(s) |
| 452 | } |
| 453 | } |
| 454 | ``` |
| 455 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 456 | </div> |
| 457 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 458 | Now, let us see what happens if we launch a couple of coroutines sending strings |
| 459 | (in this example we launch them in the context of the main thread as main coroutine's children): |
| 460 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 461 | <!--- CLEAR --> |
| 462 | |
| 463 | <div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3"> |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 464 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 465 | ```kotlin |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 466 | import kotlinx.coroutines.* |
| 467 | import kotlinx.coroutines.channels.* |
| 468 | |
| 469 | fun main() = runBlocking { |
| 470 | //sampleStart |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 471 | val channel = Channel<String>() |
| 472 | launch { sendString(channel, "foo", 200L) } |
| 473 | launch { sendString(channel, "BAR!", 500L) } |
| 474 | repeat(6) { // receive first six |
| 475 | println(channel.receive()) |
| 476 | } |
| 477 | coroutineContext.cancelChildren() // cancel all children to let main finish |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 478 | //sampleEnd |
| 479 | } |
| 480 | |
| 481 | suspend fun sendString(channel: SendChannel<String>, s: String, time: Long) { |
| 482 | while (true) { |
| 483 | delay(time) |
| 484 | channel.send(s) |
| 485 | } |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 486 | } |
| 487 | ``` |
| 488 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 489 | </div> |
| 490 | |
Inego | 69c26df | 2019-04-21 14:51:25 +0700 | [diff] [blame] | 491 | > You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-channel-07.kt). |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 492 | |
| 493 | The output is: |
| 494 | |
| 495 | ```text |
| 496 | foo |
| 497 | foo |
| 498 | BAR! |
| 499 | foo |
| 500 | foo |
| 501 | BAR! |
| 502 | ``` |
| 503 | |
| 504 | <!--- TEST --> |
| 505 | |
| 506 | ### Buffered channels |
| 507 | |
| 508 | The channels shown so far had no buffer. Unbuffered channels transfer elements when sender and receiver |
| 509 | meet each other (aka rendezvous). If send is invoked first, then it is suspended until receive is invoked, |
| 510 | if receive is invoked first, it is suspended until send is invoked. |
| 511 | |
| 512 | Both [Channel()] factory function and [produce] builder take an optional `capacity` parameter to |
| 513 | specify _buffer size_. Buffer allows senders to send multiple elements before suspending, |
| 514 | similar to the `BlockingQueue` with a specified capacity, which blocks when buffer is full. |
| 515 | |
| 516 | Take a look at the behavior of the following code: |
| 517 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 518 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 519 | <div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3"> |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 520 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 521 | ```kotlin |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 522 | import kotlinx.coroutines.* |
| 523 | import kotlinx.coroutines.channels.* |
| 524 | |
| 525 | fun main() = runBlocking<Unit> { |
| 526 | //sampleStart |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 527 | val channel = Channel<Int>(4) // create buffered channel |
| 528 | val sender = launch { // launch sender coroutine |
| 529 | repeat(10) { |
| 530 | println("Sending $it") // print before sending each element |
| 531 | channel.send(it) // will suspend when buffer is full |
| 532 | } |
| 533 | } |
| 534 | // don't receive anything... just wait.... |
| 535 | delay(1000) |
| 536 | sender.cancel() // cancel sender coroutine |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 537 | //sampleEnd |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 538 | } |
| 539 | ``` |
| 540 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 541 | </div> |
| 542 | |
Inego | 69c26df | 2019-04-21 14:51:25 +0700 | [diff] [blame] | 543 | > You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-channel-08.kt). |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 544 | |
| 545 | It prints "sending" _five_ times using a buffered channel with capacity of _four_: |
| 546 | |
| 547 | ```text |
| 548 | Sending 0 |
| 549 | Sending 1 |
| 550 | Sending 2 |
| 551 | Sending 3 |
| 552 | Sending 4 |
| 553 | ``` |
| 554 | |
| 555 | <!--- TEST --> |
| 556 | |
| 557 | The first four elements are added to the buffer and the sender suspends when trying to send the fifth one. |
| 558 | |
| 559 | ### Channels are fair |
| 560 | |
| 561 | Send and receive operations to channels are _fair_ with respect to the order of their invocation from |
| 562 | multiple coroutines. They are served in first-in first-out order, e.g. the first coroutine to invoke `receive` |
| 563 | gets the element. In the following example two coroutines "ping" and "pong" are |
| 564 | receiving the "ball" object from the shared "table" channel. |
| 565 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 566 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 567 | <div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3"> |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 568 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 569 | ```kotlin |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 570 | import kotlinx.coroutines.* |
| 571 | import kotlinx.coroutines.channels.* |
| 572 | |
| 573 | //sampleStart |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 574 | data class Ball(var hits: Int) |
| 575 | |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 576 | fun main() = runBlocking { |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 577 | val table = Channel<Ball>() // a shared table |
| 578 | launch { player("ping", table) } |
| 579 | launch { player("pong", table) } |
| 580 | table.send(Ball(0)) // serve the ball |
| 581 | delay(1000) // delay 1 second |
| 582 | coroutineContext.cancelChildren() // game over, cancel them |
| 583 | } |
| 584 | |
| 585 | suspend fun player(name: String, table: Channel<Ball>) { |
| 586 | for (ball in table) { // receive the ball in a loop |
| 587 | ball.hits++ |
| 588 | println("$name $ball") |
| 589 | delay(300) // wait a bit |
| 590 | table.send(ball) // send the ball back |
| 591 | } |
| 592 | } |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 593 | //sampleEnd |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 594 | ``` |
| 595 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 596 | </div> |
| 597 | |
Inego | 69c26df | 2019-04-21 14:51:25 +0700 | [diff] [blame] | 598 | > You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-channel-09.kt). |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 599 | |
| 600 | The "ping" coroutine is started first, so it is the first one to receive the ball. Even though "ping" |
| 601 | coroutine immediately starts receiving the ball again after sending it back to the table, the ball gets |
| 602 | received by the "pong" coroutine, because it was already waiting for it: |
| 603 | |
| 604 | ```text |
| 605 | ping Ball(hits=1) |
| 606 | pong Ball(hits=2) |
| 607 | ping Ball(hits=3) |
| 608 | pong Ball(hits=4) |
| 609 | ``` |
| 610 | |
| 611 | <!--- TEST --> |
| 612 | |
Inego | ebe519a | 2019-04-21 13:22:27 +0700 | [diff] [blame] | 613 | Note that sometimes channels may produce executions that look unfair due to the nature of the executor |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 614 | that is being used. See [this issue](https://github.com/Kotlin/kotlinx.coroutines/issues/111) for details. |
| 615 | |
| 616 | ### Ticker channels |
| 617 | |
| 618 | Ticker channel is a special rendezvous channel that produces `Unit` every time given delay passes since last consumption from this channel. |
| 619 | Though it may seem to be useless standalone, it is a useful building block to create complex time-based [produce] |
| 620 | pipelines and operators that do windowing and other time-dependent processing. |
| 621 | Ticker channel can be used in [select] to perform "on tick" action. |
| 622 | |
| 623 | To create such channel use a factory method [ticker]. |
| 624 | To indicate that no further elements are needed use [ReceiveChannel.cancel] method on it. |
| 625 | |
| 626 | Now let's see how it works in practice: |
| 627 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 628 | <div class="sample" markdown="1" theme="idea" data-highlight-only> |
| 629 | |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 630 | ```kotlin |
Prendota | 65e6c8c | 2018-10-17 11:51:08 +0300 | [diff] [blame] | 631 | import kotlinx.coroutines.* |
| 632 | import kotlinx.coroutines.channels.* |
| 633 | |
| 634 | fun main() = runBlocking<Unit> { |
Vsevolod Tolstopyatov | a2d8088 | 2018-09-24 19:51:49 +0300 | [diff] [blame] | 635 | val tickerChannel = ticker(delayMillis = 100, initialDelayMillis = 0) // create ticker channel |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 636 | var nextElement = withTimeoutOrNull(1) { tickerChannel.receive() } |
Vadim Semenov | 171fcc1 | 2020-05-03 18:20:09 +0100 | [diff] [blame^] | 637 | println("Initial element is available immediately: $nextElement") // no initial delay |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 638 | |
Vadim Semenov | 171fcc1 | 2020-05-03 18:20:09 +0100 | [diff] [blame^] | 639 | nextElement = withTimeoutOrNull(50) { tickerChannel.receive() } // all subsequent elements have 100ms delay |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 640 | println("Next element is not ready in 50 ms: $nextElement") |
| 641 | |
| 642 | nextElement = withTimeoutOrNull(60) { tickerChannel.receive() } |
| 643 | println("Next element is ready in 100 ms: $nextElement") |
| 644 | |
| 645 | // Emulate large consumption delays |
| 646 | println("Consumer pauses for 150ms") |
| 647 | delay(150) |
| 648 | // Next element is available immediately |
| 649 | nextElement = withTimeoutOrNull(1) { tickerChannel.receive() } |
| 650 | println("Next element is available immediately after large consumer delay: $nextElement") |
| 651 | // Note that the pause between `receive` calls is taken into account and next element arrives faster |
| 652 | nextElement = withTimeoutOrNull(60) { tickerChannel.receive() } |
| 653 | println("Next element is ready in 50ms after consumer pause in 150ms: $nextElement") |
| 654 | |
| 655 | tickerChannel.cancel() // indicate that no more elements are needed |
| 656 | } |
| 657 | ``` |
| 658 | |
Alexander Prendota | cbeef10 | 2018-09-27 18:42:04 +0300 | [diff] [blame] | 659 | </div> |
| 660 | |
Inego | 69c26df | 2019-04-21 14:51:25 +0700 | [diff] [blame] | 661 | > You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-channel-10.kt). |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 662 | |
| 663 | It prints following lines: |
| 664 | |
| 665 | ```text |
| 666 | Initial element is available immediately: kotlin.Unit |
| 667 | Next element is not ready in 50 ms: null |
| 668 | Next element is ready in 100 ms: kotlin.Unit |
| 669 | Consumer pauses for 150ms |
| 670 | Next element is available immediately after large consumer delay: kotlin.Unit |
| 671 | Next element is ready in 50ms after consumer pause in 150ms: kotlin.Unit |
| 672 | ``` |
| 673 | |
| 674 | <!--- TEST --> |
| 675 | |
| 676 | Note that [ticker] is aware of possible consumer pauses and, by default, adjusts next produced element |
| 677 | delay if a pause occurs, trying to maintain a fixed rate of produced elements. |
| 678 | |
| 679 | Optionally, a `mode` parameter equal to [TickerMode.FIXED_DELAY] can be specified to maintain a fixed |
| 680 | delay between elements. |
| 681 | |
| 682 | |
| 683 | <!--- MODULE kotlinx-coroutines-core --> |
Roman Elizarov | 0950dfa | 2018-07-13 10:33:25 +0300 | [diff] [blame] | 684 | <!--- INDEX kotlinx.coroutines --> |
| 685 | [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html |
| 686 | [runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html |
Vsevolod Tolstopyatov | 706e393 | 2018-10-13 15:40:32 +0300 | [diff] [blame] | 687 | [kotlin.coroutines.CoroutineContext.cancelChildren]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/kotlin.coroutines.-coroutine-context/cancel-children.html |
Roman Elizarov | 0950dfa | 2018-07-13 10:33:25 +0300 | [diff] [blame] | 688 | [Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html |
| 689 | <!--- INDEX kotlinx.coroutines.channels --> |
| 690 | [Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html |
| 691 | [SendChannel.send]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html |
| 692 | [ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html |
| 693 | [SendChannel.close]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/close.html |
| 694 | [produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html |
| 695 | [consumeEach]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/consume-each.html |
| 696 | [Channel()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel.html |
| 697 | [ticker]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/ticker.html |
| 698 | [ReceiveChannel.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/cancel.html |
| 699 | [TickerMode.FIXED_DELAY]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-ticker-mode/-f-i-x-e-d_-d-e-l-a-y.html |
| 700 | <!--- INDEX kotlinx.coroutines.selects --> |
| 701 | [select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html |
hadihariri | 7db5553 | 2018-09-15 10:35:08 +0200 | [diff] [blame] | 702 | <!--- END --> |