I. Definition of coroutines

1. Description of official documents

Coroutines simplify asynchronous programming by putting complexity into libraries. The program logic can be expressed sequentially in coroutines, and the underlying library takes care of the asynchrony for us. The library can wrap the relevant parts of user code as callbacks, subscribing to related events, and scheduling execution on different threads (or even different machines), while keeping the code as simple as sequential execution.


Roman Elizarov, the developer of coroutines, describes them this way: coroutines are like very lightweight threads. Threads are scheduled by the system, and the overhead of thread switching or thread blocking is high. Coroutines depend on threads, but do not block threads when they are suspended, and are virtually costless. The coroutines are controlled by the developer. So coroutines are also like user threads, very lightweight, you can create any coroutines in one thread.

In summary: Coroutines simplify asynchronous programming by expressing programs sequentially, and they also provide a way to avoid blocking threads and replace it with a cheaper, more controllable operation — coroutine suspend.

2. Basic concepts of coroutines

(1) Suspend function

Suspended functions can take arguments and return values in the same way as normal functions, but calling functions may suspend coroutines (if the results of the related call are already available, the library can decide to proceed without suspending them). Suspended functions suspend coroutines without blocking the thread in which they are located. After the suspended function completes execution, the coroutine is resumed before the rest of the code continues. But suspended functions can only be called in coroutines or other suspended functions. In fact, to start a coroutine, you need at least one suspended function, which is usually a suspended lambda expression. So the suspend modifier can mark normal functions, extension functions, and lambda expressions.

(2) Launch function

Coroutines are created by creating a coroutine. The most common coroutine creation method is coroutinescope.launch {}. The key source code is as follows:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    ...
    coroutine.start(start, coroutine, block)
    return coroutine
}Copy the code

Some important concepts of coroutines can be seen from the function definition above:

The CoroutineScope, understood as the coroutine itself, contains the CoroutineContext.

CoroutineContext, a CoroutineContext, is a collection of elements, mainly Job and CoroutineDispatcher elements, that can represent a coroutine scene.

EmptyCoroutineContext represents an EmptyCoroutineContext.

CoroutineDispatcher, a CoroutineDispatcher that determines the thread or thread pool in which the coroutine resides. It can specify that the coroutine will run on a specific thread, a thread pool, or no thread at all (so that the coroutine will run on the current thread). Default, Dispatchers.IO, Dispatchers.Main and Dispatchers.Unconfined. Unconfined means no thread is specified.

If no CoroutineDispatcher is specified or no other ContinuationInterceptor is available, the Default coroutine scheduler is Dispatchers.Default is a coroutine scheduler. The specified thread is a common thread pool. The number of threads must be at least 2 and the maximum number is the same as that of cpus.

A Job encapsulates the code logic that needs to be executed in a coroutine. A Job can be cancelled and has a simple life cycle. It can be isActive, isCompleted, or isCancelled. Deferred

: Job Public Interface Deferred

: Job Public Interface Deferred

: Job Public Interface Deferred

: Job



The coroutinescope.launch function belongs to the Coroutine builders. It does not block the current thread, creates a new Coroutine in the background, or specifies the Coroutine scheduler. For example, globalScope.launch (dispatchers.main) {} is commonly used in Android.

(3) Async function

Coroutinescope.async {} can achieve the same effect as launch Builder by creating a new coroutine in the background, the only difference being that it returns a value because coroutinescope.async {} returns a Deferred type.

To get the return value of coroutinescope.async {}, we need to await() the function, which is also a pending function that suspends the current coroutine until the async code has finished executing and returns some value.

(4) runBlocking function

RunBlocking {} creates a new coroutine and blocks the current thread until the coroutine terminates. This should not be used in coroutines, but is primarily designed for main functions and tests.

(5) withContext function

WithContext {} does not create a new coroutine, runs the pending block on the specified coroutine, and suspends the coroutine until the block is complete.

The relationship between coroutines

It is mentioned in official documents that there may be parent-child relationship between coroutines, and all child coroutines will be cancelled when the parent coroutine is cancelled. Therefore, parent-child relationship between coroutines has three effects:

  • A parent coroutine that manually calls Cancel () or ends with an exception immediately cancels all of its children.

  • The parent coroutine must wait for all child coroutines to complete (in completed or canceled state) before completing.

  • When a child coroutine throws an uncaught exception, its parent is cancelled by default.

launchand
asyncWhen you build a coroutine, first of all
newCoroutineContext(context)The CoroutineContext context of the new coroutine is inherited from the CoroutineContext of the original CoroutineScope.
GlobalScopeAnd ordinary coroutines
CoroutineScopeThe difference between,
GlobalScopeJob is empty,
GlobalScope.launch{}and
GlobalScope.async{}The newly created coroutine has no parent coroutine.

For the cancellation of coroutines,
cancel()Just change the state of coroutine to cancelled state, and cannot cancel the operation logic of coroutine, many suspended functions in the coroutine library will detect the state of coroutine, if you want to cancel the operation of coroutine in time, it is best to use
isActiveDetermine the coroutine state.


3. Exception handling in coroutines

First of all, all uncaught exceptions in the coroutine operation are actually caught in the second wrapper, so
When an uncaught exception occurs, all child coroutines are first canceled, and then the parent coroutines may be canceled.There are cases where the parent coroutine is not cancelled, either when the exception is a CancellationException or when the parent coroutine is used
SupervisorJoband
supervisorScope, the parent coroutine will not be affected if the child coroutine encounters an uncaught exception. Their principle is to rewrite childCancelled() as

override fun childCancelled(cause: Throwable): Boolean = false

Both launch and Async coroutines automatically propagate exceptions up and cancel the parent coroutine.

Unless it’s the container handling the job or container handling the container handling the container handling the container, it’s the container handling the container handling the container handling the container.

The default
handleJobExceptionIs empty, so if Root Coroutine is
asyncType coroutine, will not have any abnormal print operation, nor crash, but for
launchCoroutine or coroutine
actorIs called by a coroutine
handleExceptionViaHandler()Handle exceptions.

By default, launch type coroutines for an uncaught exception is just print the exception stack information, if in the Android will call uncaughtExceptionPreHandler handle exceptions. But if use the CoroutineExceptionHandler, can only use a custom exception handling CoroutineExceptionHandler.

asyncThe coroutine can only pass
await()Rethrows the exception, but it can pass
try { deffered.await() } catch () { ... }To catch exceptions


The container is designed to handle exceptions with the coroutineScope or container container for async calls. It is designed to wrap the async {} container around the coroutineScope container. When an exception occurs inside the async {} container, it will cancel all other coroutines created inside the container without affecting the outer container.

Concurrent processing of coroutines

Locks in threads are blocking, and no other logic can be executed without the lock being acquired. Coroutines can be solved by suspending functions, suspending the coroutine without the lock being acquired, and resuming the coroutine after the lock is acquired. The thread is not blocking when the coroutine is suspended and can execute other logic. This mutex is
MutexIt with
synchronizedThe keywords are somewhat similar and also provided
withLockExtension functions instead of common ones
mutex.lock; try {... } finally { mutex.unlock() }:

fun main(args: Array<String>) = runBlocking<Unit> {
    val mutex = Mutex()
    var counter = 0
    repeat(10000) {
        GlobalScope.launch {
            mutex.withLock {
                counter ++
            }
        }
    }
    println("The final count is $counter")}Copy the code

Encapsulate Retrofit network request framework via coroutines

Introducing JakeWharton’s open source library lets Retrofit return Deferred

directly:

implementation 'com. Squareup. Retrofit2: converter - gson: 2.4.0'
implementation 'com. Squareup. Retrofit2: retrofit: 2.6.1'
implementation 'com. Jakewharton. Retrofit: retrofit2 - kotlin coroutines -- adapter: 0.9.2'
Copy the code

/** * user login interface ** @param data * @return 
*/
@POST("/api/login/")
fun loginAsync(@Body data: LoginBean): Deferred<Response<LoginResponse>>Copy the code

fun buildRetrofit(host: String): Retrofit {
    return Retrofit.Builder()
        .baseUrl(host)
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .client(okHttpClient!!)
        .build()
}Copy the code

Design the BaseViewModel common base class and inherit LifecycleObserver:

open class BaseViewModel : ViewModel(), LifecycleObserver { private val viewModelJob = SupervisorJob() protected val uiScope = CoroutineScope(Dispatchers.Main +  viewModelJob) protected val ioScope = CoroutineScope(Dispatchers.IO + viewModelJob) var httpStatus: MutableLiveData<HttpStatus> = MutableLiveData()suspendfun <T>invokeHttpRequest(job: Deferred<Response<T>>): T? { var body: T? = null try { job.await().apply {if (isSuccessful){
                    if(body() == null){
                        uiScope.launch {
                            Toast.makeText(App.getInstance().applicationContext,
                                "Server has no return value!",Toast.LENGTH_SHORT).show()
                        }
                    }else{
                        body = body()
                    }
                }else{
                    val status = HttpStatus(code(),message())
                    httpStatus.postValue(status)
                    uiScope.launch {
                        Toast.makeText(App.getInstance(),"HTTP status code${status.code}:${status.message}",
                            Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }catch (e: Exception){
            println(e.message)
            uiScope.launch {
                Toast.makeText(App.getInstance(),"Network connection failed!", Toast.LENGTH_SHORT).show()
            }
        }
        return body
    }


    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    open fun onStop() {
        viewModelJob.cancelChildren()
    }
}Copy the code

Implement the relevant business logic in a subclass of BaseViewModel

class LoginViewModel: BaseViewModel() { var status: MutableLiveData<Boolean> = MutableLiveData() fun login(account: String? , password: String?) { uiScope.launch { App.getInstance().run {if (account == null || account.trim() == ""){
                    Toast.makeText(this, getString(R.string.empty_account), Toast.LENGTH_SHORT).show()
                    status.value = false
                    return@launch
                }
                if (password == null || password.trim() == ""){
                    Toast.makeText(this, getString(R.string.empty_password), Toast.LENGTH_SHORT).show()
                    status.value = false
                    return@launch } } ioScope.launch { val result = invokeHttpRequest(BaseApiService.instance .getApiImp(EduApi::class.java) .loginAsync(LoginBean(account? .trim(),password? .trim()))) result? .run { status.postValue(status_code == 200)if(status.value == true){
                        saveJwtToken(this)
                    }
                }
            }
        }
    }
}Copy the code

The observer mode of the ViewModel is applied in the view layer to implement network request invocation and receiving of returned data, and the ViewModel is bound to LifecycleOwner’s lifecycle to cancel the network request when the page is destroyed.

private fun initViewModel(){
    viewModel = ViewModelProvider(this).get(LoginViewModel::class.java)
    lifecycle.addObserver(viewModel)
    viewModel.status.observe(this, Observer{
        if (it){
            startActivity(Intent(this,MainActivity::class.java))
            finish()
        }else{
            Toast.makeText(this, getString(R.string.login_fail), Toast.LENGTH_SHORT).show()
        }
        ivLoading.visibility = View.INVISIBLE
    })
}Copy the code

6. Further understanding of CoroutineDispatcher

The CoroutineDispatcher defines the thread on which the Coroutine executes. The CoroutineDispatcher can limit the execution of a Coroutine to a single thread, assign it to a thread pool, or do not limit the number of threads on which it is executed.

CoroutineDispatcher is an abstract class that all dispatchers should inherit to implement their functions. The standard library provides the following common implementations:

  • Dispatchers.Default– If dispatcher is not specified when a Coroutine is created, it is used by default. The Default Dispatcher uses a shared pool of background threads to run its tasks.
  • Dispatchers.IO— As the name implies, this is used to perform blocking IO operations, and also uses a shared thread pool to perform tasks inside. Depending on the number of tasks running at the same time, additional threads are created as needed and unneeded threads are released when the tasks are completed. Through system propertykotlinx.coroutines.io.parallelismYou can configure the maximum number of threads you can create, and we generally don’t need to do any extra configuration in the Android environment.
  • Dispatchers.Unconfined– Immediately start executing the Coroutine on the thread that started it until the first Coroutine is encounteredsuspension point. In other words,coroutine builderThe function encounters the first onesuspension point“Will return. The thread that Coroutine restores depends onsuspension functionThe thread where.Generally we do not use Unconfined devices.
  • throughnewSingleThreadContextnewFixedThreadPoolContextThe Dispatcher function creates dispatchers that run in a private thread pool. Since creating a thread consumes system resources, you need to use the close function to close a temporary thread pool and release resources after using it.
  • throughasCoroutineDispatcherExtension functions can be added to JavaExecutorObject is converted to a Dispatcher for use.
  • Dispatchers.Main– It is executed in the Android UI thread.

Since the child Coroutine inherits the context of the parent Coroutine, we usually set a Dispatcher on the parent Coroutine for ease of use, and then all children Coroutine automatically use this Dispatcher.

7. CoroutineContext

In each of the coroutine Builder functions described earlier, a CoroutineContext parameter is required. CoroutineContext is an important part of this.

CoroutineContext contains user-defined collections of data associated with the current Coroutine. CoroutineContext is similar to thread-local variables in that thread-local can be modified but CoroutineContext is immutable. Since CoroutineContext is a very lightweight implementation, if you need to change CoroutineContext, you only need to create a new Coroutine using the new context.

CoroutineContext is an indexed set of elements in which each Element has a unique Key. It is defined as a mixture of a set and a map, so that each element has a key like the map, and each key is directly associated with the element like the set.

CoroutineContext has two very important elements – the Job, which is the current instance of the Coroutine, and the Dispatcher, which determines the current thread of execution of the Coroutine.

CoroutineContext defines four core operations:

  • Operator OperatorgetThe Element can be retrieved by the key. Since this is a GET operator, it can be used just like accessing elements in a mapcontext[key]This is called in parentheses.
  • functionfoldCollection.foldExtension functions are similar, providing the ability to facilitate all elements in the current context.
  • The operatorplusSet.plusThe extension function returns a new context object that contains all of the elements in both elements+The Element on the right of the symbol replaces the one on the left.
  • functionminusKeyReturns the context for deleting an Element.

Using these functions, context can be easily combined. For example, one library defines an Auth Element that holds the id of the user that has logged in, while another library defines a threadPool Element that contains some execution information. You can use the + sign to combine the two contexts: launch(auth + threadPool) {… }, so the code looks more intuitive.

The library contains an empty implementation EmptyCoroutineContext with no functionality. General inherit AbstractCoroutineContextElement this class to implement custom context.

Controlling the thread of execution of a Coroutine is a very important function, and this function is implemented through the CoroutineDispatcher context interface.

If you need to create a child Coroutine of a different context within a Coroutine, you can use the withContext() function to do so.