Channel practice of Kotlin

Posted by cillosis on Thu, 06 Jan 2022 07:27:19 +0100

  • passageway

    • Know Channel
    • Capacity and iteration
    • Production and actor
    • Channel shutdown
    • BroadcastChannel
  • Multiplexing

    • What is multiplexing
    • Multiplexing multiple await s
    • Multiplexing multiple channels
    • SelectClause
    • Flow multiplexing
  • Concurrent security

    • Concurrent tools for collaborative processes
    • Mutex
    • Semaphore

    Know Channel

    Channel is actually a concurrent secure queue, which can be used to connect processes and realize the communication of different processes.

    Look at an example of producers and consumers:

      @Test
      fun testChannel() = runBlocking {
          val channel = Channel<Int>()
          // producer
          val producer = GlobalScope.launch {
              var i = 1
              while (true) {
                  delay(1000)
                  println("sending $i")
                  channel.send(i++)
              }
          }
          // consumer
          val consumer = GlobalScope.launch {
              while (true) {
                  val element = channel.receive()
                  println("receive: $element")
              }
          }
          joinAll(producer, consumer)
      }

    The producer produces an element every second and is immediately consumed by the consumer.

Channel capacity

Channel is actually a queue. There must be a buffer in the queue. Once the buffer is full and no one has called receive and taken away the elements, send needs to be suspended. If you deliberately slow down the receiver, you will find that send will always hang and will not continue until you know the receive.

public fun <E> Channel(
    capacity: Int = RENDEZVOUS,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
    onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E>

By default, the Channel has a capacity of RENDEZVOUS, with a value of 0. If the above example slows down the pace of consumers, the producer will produce one every time consumers consume an element according to the pace of consumers. send will always hang up and wait for consumers to consume.

    @Test
    fun testChannel() = runBlocking {
        val channel = Channel<Int>()
        val start = System.currentTimeMillis()
        // producer
        val producer = GlobalScope.launch {
            var i = 1
            while (true) {
                delay(1000)
                println("sending $i, ${System.currentTimeMillis() - start}")
                channel.send(i++)
            }
        }
        // consumer
        val consumer = GlobalScope.launch {
            while (true) {
                delay(5000)
                val element = channel.receive()
                println("receive: $element, ${System.currentTimeMillis() - start}")
            }
        }
        joinAll(producer, consumer)
    }
sending 1, 1018
receive: 1, 5017
sending 2, 6022
receive: 2, 10021
sending 3, 11024
receive: 3, 15026
...

Since the buffer is 0 by default, the producer has to wait for the consumer to consume the element before producing.

Iterative Channel

The Channel itself is like a sequence. When reading, you can directly obtain the iterator of a Channel.

    @Test
    fun testChannelIterator() = runBlocking {
//        val channel = Channel<Int>()
        val channel = Channel<Int>(Channel.UNLIMITED)
        val start = System.currentTimeMillis()
        // producer
        val producer = GlobalScope.launch {
            for (i in 1..5) {
                println("sending $i, ${System.currentTimeMillis() - start}")
                channel.send(i)
            }
        }
        // consumer
        val consumer = GlobalScope.launch {
            val it = channel.iterator()
            while (it.hasNext()) {
                val element = it.next()
                println("receive: $element, ${System.currentTimeMillis() - start}")
                delay(2000)
            }
        }
        joinAll(producer, consumer)
    }
sending 1, 8
sending 2, 12
sending 3, 12
sending 4, 12
sending 5, 12
receive: 1, 15
receive: 2, 2023
receive: 3, 4026
receive: 4, 6031
receive: 5, 8037

The above is the iterative method. Set the buffer to UNLIMITED. See that the producer produces and sends out five elements at once, and the consumers consume one by one according to their own rhythm. If the buffer is still the default, as in the previous example, consume one and then produce another.

Production and actor

  • A convenient way to construct producers and consumers
  • You can start a producer process through the produce method and return a ReceiveChannel, which can be used by other processes to receive data. Conversely, an actor can be used to start a consumer collaboration.
    Take an example, create a receiveChannel using produce, and then start a collaborative process to consume the elements in the receiveChannel.

      @Test
      fun testProducer() = runBlocking {
          val receiveChannel = GlobalScope.produce(capacity = 50) {
              repeat(5) {
                  delay(1000)
                  println("produce $it")
                  send(it)
              }
          }
          val consumer = GlobalScope.launch {
              for (i in receiveChannel) {
                  delay(3000)
                  println("consume: $i")
              }
          }
          consumer.join()
      }
    produce 0
    produce 1
    produce 2
    consume: 0
    produce 3
    produce 4
    consume: 1
    consume: 2
    consume: 3
    consume: 4
    
    Process finished with exit code 0

    The source code of produce is as follows, and the capacity is 0 by default. Therefore, in the above example, if the capacity is not set when creating a receiveChannel, it will become: producing an element, consuming an element, and alternating. After 50 capacities are set, multiple elements can be generated at once. Of course, the time for consumers to consume elements in this example is delayed for 3 seconds, so during each delay of 3 seconds, the producer (simulating the production of 1 element per second) produces 3 elements.

    public fun <E> CoroutineScope.produce(
      context: CoroutineContext = EmptyCoroutineContext,
      capacity: Int = 0,
      @BuilderInference block: suspend ProducerScope<E>.() -> Unit
    ): ReceiveChannel<E>

    Let's look at actor:

    public fun <E> CoroutineScope.actor(
      context: CoroutineContext = EmptyCoroutineContext,
      capacity: Int = 0, // todo: Maybe Channel.DEFAULT here?
      start: CoroutineStart = CoroutineStart.DEFAULT,
      onCompletion: CompletionHandler? = null,
      block: suspend ActorScope<E>.() -> Unit
    ): SendChannel<E> 

    Using actor, you can create a sendChannel, and then start the process to send elements using sendChannel. example:

      @Test
      fun testActor() = runBlocking<Unit> {
          val sendChannel = GlobalScope.actor<Int> {
              while (true) {
                  val element = receive()
                  println("receive: $element")
              }
          }
    
          val producer = GlobalScope.launch {
              for (i in 1..3) {
                  println("send: $i")
                  sendChannel.send(i)
              }
          }
    
          producer.join()
      }

    Channel shutdown

  • The channels returned by both the production and the actor will be closed after the execution of the corresponding collaboration. That's why the channel is called hot data flow.
  • For a Channel, if its close method is called, it will immediately stop receiving new elements, that is, its isClosedForSend will immediately return true. Due to the existence of Channel buffer, some elements may not be processed at this time. Therefore, isClosedForReceive will return true only after all elements in the buffer are read.
  • The life cycle of the Channel is best maintained by the leading Party. It is recommended that the leading Party close it.

      @Test
      fun testClose() = runBlocking<Unit> {
          val channel = Channel<Int>(3)
          val producer = GlobalScope.launch {
              List(3) {
                  channel.send(it)
                  println("send $it")
              }
              channel.close()
              println("close channel. closeForSend: ${channel.isClosedForSend}, closeFoReceive: ${channel.isClosedForReceive}")
          }
          val consumer = GlobalScope.launch {
              for (e in channel) {
                  println("receive: $e")
                  delay(1000)
              }
              println("after consuming. closeForSend: ${channel.isClosedForSend}, closeFoReceive: ${channel.isClosedForReceive}")
          }
          joinAll(producer, consumer)
      }
    send 0
    send 1
    send 2
    receive: 0
    close channel. closeForSend: true, closeFoReceive: false
    receive: 1
    receive: 2
    after consuming. closeForSend: true, closeFoReceive: true
    
    Process finished with exit code 0

    As can be seen from the above example, the consumer consumes an element every second and has a processing time of 1 second. During this period, the producer sends out all three elements and then closes the Channel. At this time, an element has just been consumed, so closeForSend is true and closeForReceive is false. After consuming all elements, the value is true.

    Broadcast channel

    As mentioned earlier, there is a one to many situation between the sender and receiver in the Channel. From the perspective of data processing, although there are multiple receivers, the same element will only be read by one receiver. Broadcasting is not the case, and multiple receivers do not have mutually exclusive behavior.

      @Test
      fun testBroadcast() = runBlocking {
          val broadcastChannel = BroadcastChannel<Int>(Channel.BUFFERED)
          val producer = GlobalScope.launch {
              List(3) {
                  delay(100)
                  broadcastChannel.send(it)
              }
              broadcastChannel.close()
          }
          List(3) { index ->
              GlobalScope.launch {
                  val receiveChannel = broadcastChannel.openSubscription()
                  for (i in receiveChannel) {
                      println("#$index received $i")
                  }
              }
          }.joinAll()
      }
    #2 received 0
    #0 received 0
    #1 received 0
    #1 received 1
    #0 received 1
    #2 received 1
    #1 received 2
    #0 received 2
    #2 received 2
    
    Process finished with exit code 0

    Create a BroadcastChannel to broadcast data, enable multiple collaborations to subscribe to broadcasting, and each collaboration can receive broadcast data.
    Channel can be converted to BroadcastChannel, with the same effect as the following:

      @Test
      fun testBroadcast2() = runBlocking {
          //val broadcastChannel = BroadcastChannel<Int>(Channel.BUFFERED)
          val channel = Channel<Int>()
          val broadcastChannel = channel.broadcast(Channel.BUFFERED)
          val producer = GlobalScope.launch {
              List(3) {
                  delay(100)
                  broadcastChannel.send(it)
              }
              broadcastChannel.close()
          }
          List(3) { index ->
              GlobalScope.launch {
                  val receiveChannel = broadcastChannel.openSubscription()
                  for (i in receiveChannel) {
                      println("#$index received $i")
                  }
              }
          }.joinAll()
      }

    What's multiplexing

    In the data communication system or computer network system, the bandwidth or capacity of the transmission medium is often greater than the demand for transmitting a single signal. In order to make effective use of the communication line, one channel is expected to transmit multiple signals at the same time, which is multiplexing technology.

    Multiplexing multiple await s

    The two API s obtain data from the network and local caches respectively, and display which one is expected to return first.

    See example:

      private suspend fun CoroutineScope.getUserFromLocal(name: String) = async {
          delay(1000)
          "local $name"
      }
    
      private suspend fun CoroutineScope.getUserFromRemote(name: String) = async {
          delay(2000)
          "remote $name"
      }
    
      data class Response<T>(val value: T, val isLocal: Boolean = false)
    
      @Test
      fun testSelectAwait() = runBlocking<Unit> {
          GlobalScope.launch {
              val localUser = getUserFromLocal("Jack")
              val remoteUser = getUserFromRemote("Mike")
              val userResponse = select<Response<String>> {
                  localUser.onAwait { Response(it, true) }
                  remoteUser.onAwait { Response(it, false) }
              }
              println("render on UI: ${userResponse.value}")
          }.join()
      }
    render on UI: local Jack
    
    Process finished with exit code 0

    The local method has a short delay time and returns first, so the select selects local data. If the remote time is shortened, select will select the data returned by remote.

    Multiplexing multiple channels

    Similar to await, it will receive the fastest Channel message. Take the following example:

      @Test
      fun testSelectChannel() = runBlocking<Unit> {
          val channels = listOf(Channel<Int>(), Channel<Int>())
          GlobalScope.launch {
              delay(100)
              channels[0].send(100)
          }
          GlobalScope.launch {
              delay(50)
              channels[1].send(50)
          }
          val result = select<Int?> {
              channels.forEach { channel -> channel.onReceive { it } }
          }
          println("result $result")
          delay(1000)
      }
    result 50
    
    Process finished with exit code 0

    Two channels send data elements. The first one is sent with a delay of 100 ms and the second one is sent with a delay of 50 ms. when receiving with select, the one sent with a delay of 50 ms is received.

    SelectClause

  • How do you know which events can be selected? In fact, all events that can be selected are of SelectClauseN type, including:

    • SelectClause0: the corresponding event has no return value. For example, if the join has no return value, onJoin is of type SelectClauseN. When used, the onJoin parameter is a parameterless function.
    • SelectClause1: the corresponding event has a return value. The previous onAwait and onReceive are similar.
    • SelectClause2: the corresponding event has a return value. In addition, an additional parameter is required, such as Channel Onsend has two parameters. The first is the value of Channel data type, which indicates the value to be sent, and the second is the callback parameter for successful sending.
  • If you want to confirm whether select is supported when suspending a function, you only need to check whether the corresponding SelectClauseN type callbacks exist.
    Take an example of a nonparametric function:

      @Test
      fun testSelectClause0() = runBlocking<Unit> {
          val job1 = GlobalScope.launch {
              delay(100)
              println("job 1")
          }
    
          val job2 = GlobalScope.launch {
              delay(10)
              println("job 2")
          }
          select<Unit> {
              job1.onJoin { println("job 1 onJoin") }
              job2.onJoin { println("job 2 onJoin") }
          }
          delay(1000)
      }
    job 2
    job 2 onJoin
    job 1
    
    Process finished with exit code 0

    Start two processes job1 and job2. Job2 has less delay and prints first. Therefore, job2 is selected in the select (job 2 onjoin is printed). Because the two processes have no return value, or the return value is Unit, it is declared after the select.

Let's take a look at an example of two parameters. The first is the value and the second is the callback parameter.

    @Test
    fun testSelectClause2() = runBlocking<Unit> {
        val channels = listOf(Channel<Int>(), Channel<Int>())
        println(channels)
        launch(Dispatchers.IO) {
            select<Unit> {
                launch {
                    delay(10)
                    channels[1].onSend(10) { sendChannel ->
                        println("sent on $sendChannel")
                    }
                }
                launch {
                    delay(100)
                    channels[0].onSend(100) { sendChannel ->
                        println("sent on $sendChannel")
                    }
                }
            }
        }
        GlobalScope.launch {
            println(channels[0].receive())
        }
        GlobalScope.launch {
            println(channels[1].receive())
        }
        delay(1000)
    }
[RendezvousChannel@78aab498{EmptyQueue}, RendezvousChannel@7ee955a8{EmptyQueue}]
10
sent on RendezvousChannel@7ee955a8{EmptyQueue}

Process finished with exit code 0

Two Channel objects 78aab498 and 7ee955a8, which start two co processes with a delay of 10 ms and 100 ms respectively, and use onSend to send data of 10 and 100 respectively. The second parameter is callback;
Then start the two processes to receive the data sent by the two Channel objects respectively. You can see that the process with less delay is selected.

Let's look at the meaning of "whether there is a corresponding SelectClauseN type" above:

public val onSend: SelectClause2<E, SendChannel<E>>
public val onJoin: SelectClause0

You can see that in the source code, both onSend and onJoin have SelectClauseN interfaces, so both support select.

Multiplexing using Flow

In most cases, the multiplexing effect can be realized by constructing an appropriate Flow.

Topics: kotlin Channel