Things about Kotlin collaboration -- pipeline Channel

Posted by jeremywesselman on Sun, 27 Feb 2022 07:49:49 +0100

Flow is called cold flow. Since there is cold flow, there is heat flow, and pipe channel is heat flow. Channel is a concurrent and secure queue, which is mainly used to handle the communication between processes

The sender and receiver are two different processes. The data sent and transmitted between them is through this pipeline, a send and a receive

 val channel = Channel<String>{  }

 viewModelScope.launch {
     channel.send("123")
 }

 viewModelScope.launch {
 
     val receive = channel.receive()
     if(receive == "123"){
         Log.e(TAG,"I got it")
     }
 }

Channel creates a channel to send a string. After one data is sent by coroutine 1, it receives this parameter in coroutine 2 and continues to perform subsequent operations

1 Channel capacity

Since the Channel is a queue, there will be capacity. By default, capacity = 1

public fun <E> Channel(
    capacity: Int = RENDEZVOUS,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
    onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> =
	when (capacity) {
	        RENDEZVOUS -> {
	            if (onBufferOverflow == BufferOverflow.SUSPEND)
	                RendezvousChannel(onUndeliveredElement) // an efficient implementation of rendezvous channel
	            else
	                ArrayChannel(1, onBufferOverflow, onUndeliveredElement) // support buffer overflow with buffered channel
	        }

At the same time, there is also a buffer overflow to store the received data. When the buffer is full, send will hang until the buffer has new space; Similarly, if the cache is empty, the receive will hang

2 Channel iteration

The Channel has an iterator. By getting the iterator of the pipeline, you can get the collected data and replace the receive

viewModelScope.launch {

    (1..3).asFlow().collect {

        channel.send("123")
    }

}

viewModelScope.launch {
    
    val iterator = channel.iterator()

    while (iterator.hasNext()){

        Log.e(TAG,"I got it  ${iterator.next()}")
    }
}

3. Channel builder

In the preface, there is a builder used, that is, Channel can directly build a pipeline. There are two ways to build producer or consumer collaboration pipelines

3.1 produce

produce is used to generate a ReceiveChannel, which can be used to receive data. It is equivalent to creating a producer. This is also an extension function of CorotinueScope, which is equivalent to creating a collaboration and a pipeline at the same time

val channel=  viewModelScope.produce<String> {
    send("123")
}

viewModelScope.launch {
    val receive = channel.receive()

    if(receive == "123"){
        Log.e(TAG,"I got it")
    }
}

3.2 actor

In contrast to produce, the actor creates a SendChannel, which is used to send data and can receive data in the scope of the collaboration

val channel = viewModelScope.actor<String> {
    val receive = receive()

    if (receive == "123") {
        Log.e(TAG, "I got it")
    }
}

viewModelScope.launch {
    channel.send("123")
}

4. Channel shutdown

Because the sending and receiving of data are executed in the process of cooperation, when the process of cooperation is completed, the Channel will be closed, which is natural

If you actively call the close method of the Channel, the Channel will immediately stop sending new elements, and the attribute isClosedForSend of the Channel will immediately return true

val channel = viewModelScope.actor<String>(Dispatchers.Main) {
     while (true){
         val receive = receive()
         if (receive == "123") {
             Log.e(TAG, "I got it")
         }
     }
 }
 
 viewModelScope.launch {
     (1..5).asFlow().collect {
         delay(500)
         channel.send("123")
     }
 }

 delay(1000)
 Log.e(TAG, "Ready to close the pipe")
 channel.close()

When the channel is closed after 1s, it will immediately stop sending data, and the receiver throws an exception, ClosedReceiveChannelException, indicating that the current channel has been closed and cannot receive data

5 await multiplexing

Multiplexing is a concept in the network. The bandwidth or capacity of the transmission medium is often greater than the demand of a single transmission signal. We hope to transmit multiple signals in one channel at the same time

In the Channel, there is only one Channel, so I hope the receiver can receive multiple data sources, instead of sending a single String data and receiving a single String data


The above is a scenario. When requesting data, there are usually two data sources, cloud or local, but the speed of obtaining data is inconsistent. The Select function is to make a choice to see which end of the data comes the fastest, so whose data is used for rendering

fun getLocal() = viewModelScope.async {
    delay(2000)
    "This is local data"
}

fun getNet() = viewModelScope.async {
    delay(1000)
    "This is the data of the network"
}

5.1 traditional methods

If you want to decide which data to render according to the speed of the data returned by which api, you can't do it through one collaboration. getLocal and getNet are asynchronous. When you get the data, you can get all the data only after the slowest return. Therefore, you can only open two collaborations to monitor their data returns separately

5.2 select

Then, in the select function, you do not need to open up two coroutines to achieve the effect of two coroutines

viewModelScope.launch {
            
    val result = select<Response<String>> {
        getLocal().onAwait{Response(it)}
        getNet().onAwait{Response(it)}
    }
    Log.e("TAG","${result.t}")
}

Look, we don't use await here, but onAwait function, which is equivalent to registering a callback in select. No matter which await returns data first, it will call back the data

class Response<T>(var t:T)

In this way, one collaborative process is used to complete what can be done by two collaborative processes

5.3 multi Channel multiplexing

Similar to await multiplexing, multiple channels send data, and the data will be printed if any channel is detected to send data

fun CoroutineScope.channel1() = produce<String> {
    delay(200)
    send("123")
}

fun CoroutineScope.channel2() = produce<String> {
    delay(400)
    send("123456")
}
viewModelScope.launch {

    val result = select<Response<String>> {
        channel1().onReceive { Response(it) }
        channel2().onReceive { Response(it) }
    }
    Log.e("TAG", "${result.t}")
}

This is a bit similar to await. Instead of using receive, onReceive callback is used

5.4 SelectClause type resolution

Since the select function is used above, which events can be selected?

From SelectBuilder, you can see that events of type SelectClause0, SelectClause1 and SelectClause2 can be selected

public interface SelectBuilder<in R> {
    /**
     * Registers a clause in this [select] expression without additional parameters that does not select any value.
     */
    public operator fun SelectClause0.invoke(block: suspend () -> R)

    /**
     * Registers clause in this [select] expression without additional parameters that selects value of type [Q].
     */
    public operator fun <Q> SelectClause1<Q>.invoke(block: suspend (Q) -> R)

    /**
     * Registers clause in this [select] expression with additional parameter of type [P] that selects value of type [Q].
     */
    public operator fun <P, Q> SelectClause2<P, Q>.invoke(param: P, block: suspend (Q) -> R)

    /**
     * Registers clause in this [select] expression with additional nullable parameter of type [P]
     * with the `null` value for this parameter that selects value of type [Q].
     */
    public operator fun <P, Q> SelectClause2<P?, Q>.invoke(block: suspend (Q) -> R): Unit = invoke(null, block)

    /**
     * Clause that selects the given [block] after a specified timeout passes.
     * If timeout is negative or zero, [block] is selected immediately.
     *
     * **Note: This is an experimental api.** It may be replaced with light-weight timer/timeout channels in the future.
     *
     * @param timeMillis timeout time in milliseconds.
     */
    @ExperimentalCoroutinesApi
    public fun onTimeout(timeMillis: Long, block: suspend () -> R)
}

SelectClause0 event: event with no return value. For example, join, the corresponding onJoin is the SelectClause0 event, which is a parameterless constructor

fun CoroutineScope.co1() = launch {
    delay(100)
    Log.e(TAG,"co1 finish")
}

fun CoroutineScope.co2() = launch {
    delay(10)
    Log.e(TAG,"co2 finish")
}

If there is no return value, it is of type Unit

select<Unit> {

    co1().onJoin{ print("co1 finish")}
    co2().onJoin{ print("co2 finish")}
}

SelectClause1 event: like await, receive has a return value, and the corresponding onAwait and onReceive get the return value through callback

SelectClause2 event: the corresponding event has a return value and requires an additional parameter. For example, the send function of Channel needs to send a data, and the corresponding onSend needs to send a data

select<Unit?> {

    launch {
        delay(200)
        channel1().onSend("2000"){
            print("send in $it")
        }
    }
    launch {
        delay(100)
        channel2().onSend("1000"){
            print("send in $it")
        }
    }
}

6 multiplexing of flow

For example, in the previous example, to get data from the local and network, you need to start two processes to get data. In fact, you can combine the two streams into one stream output through the merge operator

viewModelScope.launch {
     listOf(::getLocal,::getNet)
         .map { kFunction0 ->
         	 //Function executed
             kFunction0.invoke()
         }.map { function ->
             flow {
                 emit(function.await())
             }
         }.merge().collect {
             Log.e(TAG,"result: $it")
         }
}

7 concurrency security of collaborative processes

Like multithreading in Java, the concurrency of multiple processes will also bring security problems

Solutions: Channel, Mutex (locking), semaphore, etc., which are consistent with the processing methods in Java

Topics: Android kotlin Channel